swiftlabelpositionscnnodescnview

print label on 3d scnnode cylinder in a horizontal and vertical way


in my code right now you can add a cylinder. What I would like to do is display a label that says height and the corresponding slider value. If the cylinder is too wide I would eventually like to to be displayed horizontally but when it gets too tall or the aspect ratio is even display it vertically like I have with the corresponding gif. The current code does not print a label on it. Please print a label on it similar to the one shown in the gif.

Current Code Current Output

Desired Output Desired Output

import UIKit
import SceneKit;import CoreData

class one: UIViewController {
    
    
    var cylinderButton = UIButton()
    var onOfSwitch = UISwitch()
    var scnView = SCNView()
    
    var cameraNode = SCNNode()
    var selectedNode: SCNNode?
    
    
    var sL = UISlider(), sW = UISlider(), sH = UISlider()
    var sLLabel = UILabel(), sWLabel = UILabel(), sHLabel = UILabel()
    
 
    
    var pinchGesture: UIPinchGestureRecognizer?
    
    var DragLabel = UILabel()
    
    
   
    

    
    
    override func viewDidLoad() {
        super.viewDidLoad()
       
        
        [ onOfSwitch, scnView,cylinderButton,sL,sW,sH,sLLabel,sWLabel,sHLabel,DragLabel].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
            $0.layer.borderWidth = 1
            $0.backgroundColor = UIColor(
                red: .random(in: 0.5...0.7),
                green: .random(in: 0.0...1),
                blue: .random(in: 0.3...0.5),
                alpha: 1
            )
            if let button = $0 as? UIButton {
                button.setTitleColor(.black, for: .normal)
            }
        }
        [sL, sW, sH].forEach {
            $0.minimumValue = 0.2
            $0.maximumValue = 2.0
            $0.value = 1.0
            $0.addTarget(self, action: #selector(updateBoxSize), for: .valueChanged)
            
        }
        NSLayoutConstraint.activate([
            
         
            
            
            
            scnView.topAnchor.constraint(equalTo: view.topAnchor),
            scnView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.55),
            scnView.widthAnchor.constraint(equalTo: view.widthAnchor),
            scnView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            
            
            sL.topAnchor.constraint(equalTo: scnView.bottomAnchor),
            sL.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
            sL.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 2/3),
            sL.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            
            
            
            sLLabel.topAnchor.constraint(equalTo: scnView.bottomAnchor),
            sLLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
            sLLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/5),
            sLLabel.leadingAnchor.constraint(equalTo: sL.trailingAnchor),
            
            
          
            
            
            
            
            sW.topAnchor.constraint(equalTo: sL.bottomAnchor),
            sW.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
            sW.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 2/3),
            sW.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            
            
          
            
            sWLabel.topAnchor.constraint(equalTo: sL.bottomAnchor),
            sWLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
            sWLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/5),
            sWLabel.leadingAnchor.constraint(equalTo: sW.trailingAnchor),
            
            sH.topAnchor.constraint(equalTo: sW.bottomAnchor),
            sH.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
            sH.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 2/3),
            sH.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            
            
            
        
            
            
            
            sHLabel.topAnchor.constraint(equalTo: sW.bottomAnchor),
            sHLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.04),
            sHLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/5),
            sHLabel.leadingAnchor.constraint(equalTo: sH.trailingAnchor),
            
           
            
            cylinderButton.topAnchor.constraint(equalTo: sH.bottomAnchor),
            cylinderButton.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.10),
            cylinderButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/3),
            cylinderButton.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            
            DragLabel.topAnchor.constraint(equalTo: sH.bottomAnchor),
            DragLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.05),
            DragLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/3),
            DragLabel.leadingAnchor.constraint(equalTo: cylinderButton.trailingAnchor),
            
            onOfSwitch.topAnchor.constraint(equalTo: DragLabel.bottomAnchor),
            onOfSwitch.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.05),
            onOfSwitch.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1/3),
            onOfSwitch.leadingAnchor.constraint(equalTo: cylinderButton.trailingAnchor),
            
            
            
        
        ])
        
        
      
        
        
        DragLabel.text = "Drag to Rotate"
        let scene = SCNScene()
        scnView.scene = scene
        scnView.backgroundColor = .gray
        
        
        DragLabel.textAlignment = .center
        setupCamera()
        setupLight()
        cylinderButton.setTitle("Circle", for: .normal)
      
        cylinderButton.addTarget(self, action: #selector(addCylinder), for: .touchUpInside)
        
        onOfSwitch.addTarget(self, action: #selector(toggleCameraControl), for: .valueChanged)
        onOfSwitch.isOn = true
        scnView.allowsCameraControl = true
        
        
        
        sWLabel.text = "Width"
        
        sHLabel.text = "Height"
        
        
        sHLabel.textAlignment = .center
        sWLabel.textAlignment = .center
        sLLabel.textAlignment = .center
        
      
        
        
        
        sL.value = 0.5
        sH.value = 0.5
        sW.value = 0.5
        
        sL.minimumValue = 1
        sL.maximumValue = 25
        
        sH.minimumValue = 1
        sH.maximumValue = 25
        
        sW.minimumValue = 1
        sW.maximumValue = 25
        
        
        onOfSwitch.isOn = false
        
      
        

        
        
       
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        scnView.addGestureRecognizer(tapGesture)
        
        //3
        
        let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
        scnView.addGestureRecognizer(rotationGesture)
    }
   
    

    
    @objc func copyM() {
        guard let originalNode = selectedNode else { return }
        
        let clonedNode = originalNode.clone()
        clonedNode.position = SCNVector3(
            originalNode.position.x + 1.0,
            originalNode.position.y + 1.0,
            originalNode.position.z
        )
        scnView.scene?.rootNode.addChildNode(clonedNode)
        selectedNode = clonedNode
    }
    
    



    @objc func depthTextChanged(_ sender: UITextField) {
        guard let currentView = selectedNode else { return }
        guard let text = sender.text, let newDepth = Float(text) else { return }
        
        if let box = currentView.geometry as? SCNBox {
            box.length = CGFloat(newDepth)
            applyTextAsTexture55(to: box, width: sW.value, height: sH.value, depth: newDepth)
        } else if let cylinder = currentView.geometry as? SCNCylinder {
            // Treat "depth" as diameter here
            cylinder.radius = CGFloat(newDepth / 2)
            applyTextAsTextureToCylinder(cylinder, width: newDepth, height: sH.value)
        }
    }
    @objc func libraryBUttonFunction() {
        
        print("Library Button Selected")
        // Replace YourViewController with the name of the view controller you want to present
        let destinationVC = Second()
          
          // Customize the modal presentation style (optional)
          destinationVC.modalPresentationStyle = .fullScreen // or .overFullScreen, .pageSheet, etc.
          
          // Present the view controller modally
          self.present(destinationVC, animated: true, completion: nil)
    }
    
    @objc func heightTextChanged(_ sender: UITextField) {
        guard let currentView = selectedNode else { return }
        guard let text = sender.text, let newHeight = Float(text) else { return }
        
        if let box = currentView.geometry as? SCNBox {
            box.height = CGFloat(newHeight)
            applyTextAsTexture55(to: box, width: sW.value, height: newHeight, depth: sL.value)
        } else if let cylinder = currentView.geometry as? SCNCylinder {
            cylinder.height = CGFloat(newHeight)
            applyTextAsTextureToCylinder(cylinder, width: sW.value, height: newHeight)
        }
    }
    
    @objc func widthTextChanged(_ sender: UITextField) {
        guard let currentView = selectedNode else { return }
        guard let text = sender.text, let newWidth = Float(text) else { return }
        
        if let box = currentView.geometry as? SCNBox {
            box.width = CGFloat(newWidth)
            applyTextAsTexture55(to: box, width: newWidth, height: sH.value, depth: sL.value)
        } else if let cylinder = currentView.geometry as? SCNCylinder {
            cylinder.radius = CGFloat(newWidth / 2)
            applyTextAsTextureToCylinder(cylinder, width: newWidth, height: sH.value)
        }
    }
    
    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        let location = gesture.location(in: scnView)
        let hitResults = scnView.hitTest(location, options: nil)
        if let result = hitResults.first {
            selectedNode = result.node
            print("Selected a node: \(selectedNode!)")
        } else {
            selectedNode = nil
            print("No node selected.")
        }
    }
    
  
    
    func setupCamera() {
        cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(0, 0, 10)
        scnView.scene?.rootNode.addChildNode(cameraNode)
        
        // Set the camera as the active point of view
        scnView.pointOfView = cameraNode
    }
    
    
    func setupLight() {
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light?.type = .omni
        lightNode.position = SCNVector3(0, 10, 10)
        scnView.scene?.rootNode.addChildNode(lightNode)
        
        let ambientLight = SCNNode()
        ambientLight.light = SCNLight()
        ambientLight.light?.type = .ambient
        ambientLight.light?.color = UIColor.darkGray
        scnView.scene?.rootNode.addChildNode(ambientLight)
    }
    @objc func toggleCameraControl() {
        if onOfSwitch.isOn {
            scnView.allowsCameraControl = true
            
            // Remove pan and pinch gestures
            if let pan = panGesture {
                scnView.removeGestureRecognizer(pan)
                panGesture = nil
            }
            if let pinch = pinchGesture {
                scnView.removeGestureRecognizer(pinch)
                pinchGesture = nil
            }
        } else {
            scnView.allowsCameraControl = false
            
            if panGesture == nil {
                let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
                scnView.addGestureRecognizer(pan)
                panGesture = pan
            }
            
            if pinchGesture == nil {
                let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
                scnView.addGestureRecognizer(pinch)
                pinchGesture = pinch
            }
        }
    }
    @objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
        guard let camera = scnView.pointOfView else { return }
        
        if gesture.state == .changed {
            let scale = Float(gesture.scale)
            var position = camera.position
            position.z /= scale  // Zoom in or out
            position.z = max(min(position.z, 100), 1)  // Clamp zoom
            camera.position = position
            gesture.scale = 1
        }
    }
    
    
    
    
    @objc func addCylinder() {
        
        
        let radius = CGFloat(sW.value / 2)
        let height = CGFloat(sH.value * 3)
        
        
        
        let cylinder = SCNCylinder(radius: radius, height: height)
        applyTextAsTextureToCylinder(cylinder, width: sW.value, height: sH.value)
        
        let cylinderNode = SCNNode(geometry: cylinder)
        cylinderNode.position = SCNVector3(0, 0, 0)
        scnView.scene?.rootNode.addChildNode(cylinderNode)
        selectedNode = cylinderNode
        
    }
    
    
    var panGesture: UIPanGestureRecognizer?
    
    
    @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
        let location = gesture.location(in: scnView)
        
        switch gesture.state {
        case .began:
            let hitResults = scnView.hitTest(location, options: nil)
            if let hitNode = hitResults.first?.node,
               let geometry = hitNode.geometry,
               geometry is SCNBox || geometry is SCNCylinder {
                
                selectedNode = hitNode
                
                
            }
            
        case .changed:
            guard let selectedNode = selectedNode else { return }
            let translation = gesture.translation(in: scnView)
            let deltaX = Float(translation.x) * 0.01
            let deltaY = Float(-translation.y) * 0.01
            selectedNode.position.x += deltaX
            selectedNode.position.y += deltaY
            gesture.setTranslation(.zero, in: scnView)
            
        default:
            break
        }
    }
    
    
    
    
    
    
    @objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
        
        
        guard let node = selectedNode else { return }
        if gesture.state == .changed {
            node.eulerAngles.y += Float(gesture.rotation)
            gesture.rotation = 0
        }
        
        
        
    }
    func createCylinder(radius: CGFloat, height: CGFloat) -> SCNNode {
        let cylinder = SCNCylinder(radius: radius, height: height)
        cylinder.firstMaterial?.diffuse.contents = UIColor.lightGray
        cylinder.firstMaterial?.isDoubleSided = true
        cylinder.radialSegmentCount = 20  // Boxier look
        
        let node = SCNNode(geometry: cylinder)
        node.position = SCNVector3(0, 0, 0)
        return node
    }
    
    func applyTextAsTextureToCylinder(_ cylinder: SCNCylinder, width: Float, height: Float) {
        let sideImage = drawCylinderHeightLabel(height: height)
        
        
        let topImage = drawCylinderTopWidthLabel(width: width)
        
        let sideMaterial = SCNMaterial()
        sideMaterial.diffuse.contents = sideImage
        sideMaterial.isDoubleSided = true
        
        let topMaterial = SCNMaterial()
        topMaterial.diffuse.contents = topImage
        topMaterial.isDoubleSided = true
        
        let blankMaterial = SCNMaterial()
        blankMaterial.diffuse.contents = UIColor.lightGray
        
        cylinder.materials = [
            sideMaterial, // side
            topMaterial,  // top
            blankMaterial // bottom (optional)
        ]
    }
    

    func drawCylinderHeightLabel(height: Float) -> UIImage {
        let size = CGSize(width: 256, height: 512)
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }

        UIColor.white.setFill()
        context.fill(CGRect(origin: .zero, size: size))

        let width = sW.value
        let aspectRatio = width / height
        let heightText = "height: \(sH.value)"

        // New: Reasonable bounds for font size
        let baseFontSize: CGFloat = 16
        let minFontSize: CGFloat = 8
        let maxFontSize: CGFloat = 32

        var fontSize: CGFloat
        if aspectRatio > 3 {
            print("a")
         //   fontSize = max(minFontSize, baseFontSize * CGFloat((height / width)) * 8)
            
            fontSize = max(minFontSize, baseFontSize  * CGFloat(sW.value))
        } else {
            
            print("b")
            fontSize = max(minFontSize, baseFontSize * CGFloat((height / width)) * 2.5)
        }
        fontSize = min(fontSize, maxFontSize)

        let font = UIFont.systemFont(ofSize: fontSize)
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.black
        ]

        if aspectRatio > 3 {
            
            print("1")
            // Wide, flat cylinder – draw horizontal centered
            let textSize = (heightText as NSString).size(withAttributes: attributes)
            let rect = CGRect(
                x: (size.width - textSize.width) / 2 ,
                y: (size.height - textSize.height) / 2,
                width: textSize.width,
                height: textSize.height * 5
            )
            (heightText as NSString).draw(in: rect, withAttributes: attributes)
        } else {
            print("2")
            // Taller cylinder – draw vertically rotated
            context.saveGState()
            context.translateBy(x: size.width / 2, y: size.height / 2)
            context.rotate(by: -.pi / 2)

            let rect = CGRect(x: -128, y: -20, width: 256, height: 40)
            (heightText as NSString).draw(in: rect, withAttributes: attributes)

            context.restoreGState()
        }

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image ?? UIImage()
    }

    func drawCylinderTopWidthLabel(width: Float) -> UIImage {
        let size = CGSize(width: 256, height: 256)
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
        
        // Fill background
        UIColor.white.setFill()
        context.fill(CGRect(origin: .zero, size: size))
        
        // Text attributes
        let font = UIFont.systemFont(ofSize: 28, weight: .bold)
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.black
        ]
        
        let widthText = "width: \(Int(width * 1))"
        let textSize = (widthText as NSString).size(withAttributes: attributes)
        
        // Move to center, flip horizontally
        context.translateBy(x: size.width / 2, y: size.height / 2)
        context.scaleBy(x: -1.0, y: 1.0)  // 🔁 Horizontal mirror
        
        context.rotate(by: (3 * .pi) / 2)  // 180 degrees
        
        // Draw centered mirrored text
        let rect = CGRect(
            x: -textSize.width / 2,
            y: -textSize.height / 2,
            width: textSize.width,
            height: textSize.height
        )
        
        (widthText as NSString).draw(in: rect, withAttributes: attributes)
        
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image ?? UIImage()
    }
    
   
    func drawTopWithDepth(depth: Float) -> UIImage {
        let size = CGSize(width: 256, height: 256)
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
        
        UIColor.white.setFill()
        context.fill(CGRect(origin: .zero, size: size))
        
        let font = UIFont.systemFont(ofSize: 28)
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.black
        ]
        
        let depthText = "depth: \(Int(depth * 1))"
        
        // Rotate and place on bottom right corner of top face
        context.saveGState()
        context.translateBy(x: size.width - 40, y: size.height - 20)
        context.rotate(by: -.pi / 2)
        
        let rect = CGRect(x: 0, y: 0, width: 200, height: 40)
        (depthText as NSString).draw(in: rect, withAttributes: attributes)
        
        context.restoreGState()
        
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image ?? UIImage()
    }
    
    func applyTextAsTexture55(to box: SCNBox, width: Float, height: Float, depth: Float) {
        let blankMaterial = SCNMaterial()
        blankMaterial.diffuse.contents = UIColor.lightGray
        blankMaterial.isDoubleSided = true
        
        // Front face: width + height
        let frontImage = drawFrontWithWidthAndHeight(width: width, height: height)
        let frontMaterial = SCNMaterial()
        frontMaterial.diffuse.contents = frontImage
        frontMaterial.isDoubleSided = true
        
        // Top face: depth
        let topImage = drawTopWithDepth(depth: depth)
        
        let topMaterial = SCNMaterial()
        topMaterial.diffuse.contents = topImage
        topMaterial.isDoubleSided = true
        
        box.materials = [
            frontMaterial,     // front
            blankMaterial,     // right
            blankMaterial,     // back
            blankMaterial,     // left
            topMaterial,       // top
            blankMaterial      // bottom
        ]
    }
    
    func drawFrontWithWidthAndHeight(width: Float, height: Float) -> UIImage {
        let size = CGSize(width: 256, height: 256)
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
        
        // Background
        UIColor.white.setFill()
        context.fill(CGRect(origin: .zero, size: size))
        
        // Font and style
        let font = UIFont.systemFont(ofSize: 28)
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.black
        ]
        
        // 1. Draw width at top-left
        let widthText = "width: \(Int(width * 10))"
        let widthRect = CGRect(x: 10, y: 10, width: 200, height: 40)
        (widthText as NSString).draw(in: widthRect, withAttributes: attributes)
        
        // 2. Draw rotated height on right edge
        let heightText = "height: \(Int(height * 10))"
        
        context.saveGState()
        // Translate to bottom-right corner, then rotate counter-clockwise
        context.translateBy(x: size.width - 40, y: size.height - 20)
        context.rotate(by: -.pi / 2)
        
        let heightRect = CGRect(x: 0, y: 0, width: 200, height: 40)
        (heightText as NSString).draw(in: heightRect, withAttributes: attributes)
        context.restoreGState()
        
        // Debug border (optional)
        // UIColor.black.setStroke()
        // context.stroke(CGRect(origin: .zero, size: size), width: 2)
        
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image ?? UIImage()
    }
    
    @objc func updateBoxSize() {
        guard let selectedNode = selectedNode else { return }
        
        let width = sW.value
        let height = sH.value
        let depth = sL.value
        
        if let box = selectedNode.geometry as? SCNBox {
            box.width = CGFloat(width)
            box.height = CGFloat(height)
            box.length = CGFloat(depth)
            applyTextAsTexture55(to: box, width: width, height: height, depth: depth)
        } else if let cylinder = selectedNode.geometry as? SCNCylinder {
            cylinder.radius = CGFloat(width / 2) // radius = width / 2 for diameter control
            cylinder.height = CGFloat(height)
            applyTextAsTextureToCylinder(cylinder, width: width, height: height)
            
        }
    }
    
    
    
}

Solution

  • When the aspectRatio is very high (wide, flat cylinder), your code multiplies the font size by a large factor (180), and then draws the text inside a rect where the height is textSize.height * 20, leading to stretched, unreadable text.

    Instead of scaling font size so dramatically when aspectRatio > 3, you should

    Updated/ Revised code is as below.

    func drawCylinderHeightLabel(height: Float) -> UIImage {
        let size = CGSize(width: 256, height: 512)
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }
    
        UIColor.white.setFill()
        context.fill(CGRect(origin: .zero, size: size))
    
        let width = sW.value
        let aspectRatio = width / height
        let heightText = "height: \(Int(height))"
    
        // Reasonable font sizes
        let baseFontSize: CGFloat = 28
        let minFontSize: CGFloat = 14
        let maxFontSize: CGFloat = 36
    
        // Adjust font size based on aspect ratio, but within safe bounds
        var fontSize: CGFloat
        if aspectRatio > 3 {
            fontSize = baseFontSize * (3 / aspectRatio) // decrease size as aspect ratio increases
        } else {
            fontSize = baseFontSize * (height / width) * 2.5
        }
        fontSize = min(max(fontSize, minFontSize), maxFontSize)
    
        let font = UIFont.systemFont(ofSize: fontSize)
        let attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.black
        ]
    
        if aspectRatio > 3 {
            // Horizontal label
            let textSize = (heightText as NSString).size(withAttributes: attributes)
            let rect = CGRect(
                x: (size.width - textSize.width) / 2,
                y: (size.height - textSize.height) / 2,
                width: textSize.width,
                height: textSize.height
            )
            (heightText as NSString).draw(in: rect, withAttributes: attributes)
        } else {
            // Vertical rotated label
            context.saveGState()
            context.translateBy(x: size.width / 2, y: size.height / 2)
            context.rotate(by: -.pi / 2)
    
            let textSize = (heightText as NSString).size(withAttributes: attributes)
            let rect = CGRect(
                x: -textSize.width / 2,
                y: -textSize.height / 2,
                width: textSize.width,
                height: textSize.height
            )
            (heightText as NSString).draw(in: rect, withAttributes: attributes)
    
            context.restoreGState()
        }
    
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image ?? UIImage()
    }