Why does calculateAccumulatedFrame give an unexpected CGRect? - ios

I'm trying to determine the right scaling factor for my node tree to make it fit exactly in my presentation rectangle, so I'm trying to find the smallest bounding rectangle around all my nodes. Apple's docs say that calculateAccumulatedFrame "Calculates a rectangle in the parent’s coordinate system that contains the content of the node and all of its descendants." That sounds like what I need, but it's not giving me the tight fit that I expect. My complete playground code is:
import SpriteKit
import PlaygroundSupport
let view:SKView = SKView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = view
let scene:SKScene = SKScene(size: CGSize(width: 1000, height: 800))
scene.scaleMode = SKSceneScaleMode.aspectFill
view.presentScene(scene)
let yellowBox = SKSpriteNode(color: .yellow, size:CGSize(width: 300, height: 300))
yellowBox.position = CGPoint(x: 400, y: 500)
yellowBox.zRotation = CGFloat.pi / 10
scene.addChild(yellowBox)
let greenCircle = SKShapeNode(circleOfRadius: 100)
greenCircle.fillColor = .green
greenCircle.position = CGPoint(x: 300, y: 50)
greenCircle.frame
yellowBox.addChild(greenCircle)
let uberFrame = yellowBox.calculateAccumulatedFrame()
let blueBox = SKShapeNode(rect: uberFrame)
blueBox.strokeColor = .blue
blueBox.lineWidth = 2
scene.addChild(blueBox)
And the results are:
The left and bottom edges of the blue rectangle look good, but why are there gaps between the blue rectangle and the green circle on the top and right?

The notion "frame" does funny things when you add a transform. The bounding box around the box and circle is a rectangle. You have rotated that rectangle. Therefore its corners stick out. The accumulated frame embraces that rotated rectangle, including the sticking-out corners. It does not magically hug the drawn appearance of the nodes (e.g. the circle).

Related

Understanding CGRect coordinate system and ios drawing

I am really confused about how a CGRect is interpreted and drawn. I have the following routine to draw a rectangle:
let rectangle = CGRect(x: 0, y: 0, width: 100, height: 100)
ctx.cgContext.setFillColor(UIColor.red.cgColor)
ctx.cgContext.setStrokeColor(UIColor.green.cgColor)
ctx.cgContext.setLineWidth(1)
ctx.cgContext.addRect(rectangle)
ctx.cgContext.drawPath(using: .fillStroke)
Now I was expecting this to draw a rectangle at the origin of the screen with the width and height of 100 (pixels/points, not sure about this).
However, I can barely see the drawn rectangle. Most of it seem to lie outside the screen (on the left hand side).
Changing this to:
let rectangle = CGRect(x: 50, y: 50, width: 100, height: 100)
Still around half the rectangle is outside the screen.
Is the origin of this CGRect at the bottom right? I am not sure how this is all getting interpreted.
I thought if the origin is at the center of the rectangle, then surely the second call should show the whole rectangle?
[EDIT]
I am running this on iphone X running ios 14.4.
[EDIT]
So, the full drawing code is as follows. This is part of a View. So, in the end the view image is assigned to the image we draw on
func show(on frame: CGImage) {
// This is of dimension 480 x 640
let dstImageSize = CGSize(width: frame.width, height: frame.height)
let dstImageFormat = UIGraphicsImageRendererFormat()
dstImageFormat.scale = 1
let renderer = UIGraphicsImageRenderer(size: dstImageSize,
format: dstImageFormat)
let dstImage = renderer.image { rendererContext in
// Draw the current frame as the background for the new image.
draw(image: frame, in: rendererContext.cgContext)
// This is where I draw my rectangle
let rectangle = CGRect(x: 0, y: 0, width: 100, height: 100)
rendererContext.cgContext.setAlpha(0.3)
rendererContext.cgContext.setFillColor(UIColor.red.cgColor)
rendererContext.cgContext.setStrokeColor(UIColor.green.cgColor)
//rendererContext.cgContext.setLineWidth(1)
rendererContext.cgContext.addRect(rectangle)
rendererContext.cgContext.drawPath(using: .fill)
}
image = dstImage
}
So, from what I can tell, it should draw it on the context of the image and should not go out of bounds with the parameters I gave.
I mean, the context is that of the bitmap that has been initialized to 480 x 640. I am nt sure why this is shown out of bounds when I view it on the device. Should this bitmap/image not be shown correctly?

How do I constrain an SKShapeNode to the device edges in SpriteKit?

Just to test things out I have created a blue square and placed it at the center of the screen like this:
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myPoint = CGPoint(x: UIScreen.main.nativeBounds.midX, y: UIScreen.main.nativeBounds.midY)
mySquare.position.x = 0
mySquare.position.y = 0
self.addChild(mySquare)
Works great. Now, I would like to use constraints and set up the square constraints to the edges of the device screen. I have tried this, but the blue square doesn't appear, so I think I have the wrong idea on how to capture the CGPoint of the screen edges.
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myPoint = CGPoint(x: UIScreen.main.nativeBounds.maxX, y: UIScreen.main.nativeBounds.maxY)
let range = SKRange(lowerLimit: 10.0, upperLimit: 10.0)
let myConstraints = SKConstraint.distance(range, to: myPoint)
mySquare.constraints = [myConstraints]
self.addChild(mySquare)
How do I capture the screen edges and constrain the square to those?
SKConstraint doesn't work equally as UIKit Constraints.
SKConstraint functionality is really specific:
Please take a look here: https://developer.apple.com/documentation/spritekit/skconstraint
Anyway, can give you some recommendations:
Transform screen position to scene position:
self.view?.convert(myPoint, to: self)
You can start with this example and get node on a corner
let mySquare = SKShapeNode(rectOf: CGSize(width: 50, height: 50))
mySquare.fillColor = SKColor.blue
mySquare.lineWidth = 1
let myScreenPoint = CGPoint(x: UIScreen.main.bounds.maxX, y: UIScreen.main.bounds.maxY)
if let myScenePoint = self.view?.convert(myScreenPoint, to: self) {
mySquare.position = myScenePoint
}
self.addChild(mySquare)
With this logic, you can get each side of the screen and decrease or increase margin and make each 4 sides; or 1 constraint for the center.

SceneKit CALayer unsharp drawing

I'm trying to draw UIBezierPaths and display them on a SCNPlane, though I'm getting a very unsharp result (See in the image) How could I make it sharper?
let displayLayer = CAShapeLayer()
displayLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
displayLayer.path = path.cgPath
displayLayer.fillColor = UIColor.clear.cgColor
displayLayer.strokeColor = stroke.cgColor
displayLayer.lineWidth = lineWidth
let plane = SCNPlane(width:size.width, height: size.height)
let material = SCNMaterial()
material.diffuse.contents = displayLayer
plane.materials = [material]
let node = SCNNode(geometry: plane)
I've tried to increase the displayLayer.frame's size but it only made things smaller.
Thanks for your answer!
Andras
This has been driving me crazy, so hopefully this answer helps someone else who stumbles across this. I'm mapping a CALayer to a SCNMaterial texture for use in an ARKit scene, and everything was blurry/pixelated. Setting the contentsScale property of the CALayer to UIScreen.main.scale didn't do anything, nor playing with shouldRasterize and rasterizationScale.
The solution is to set the layer's frame to the desired frame, multiplied by the screen's scale, otherwise the material is scale transforming the layer to fit the texture. In other words, the layer's size needs to be 3x its desired size.
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 500 * UIScreen.main.scale, height: 750 * UIScreen.main.scale)
Try to change UIBezierPath "flatness" property (0.1, 0.01, 0.001 etc).

Creating circle on the every edge of rectangle

I want to create on the edge of every side of the rectangle a small circle instead of only corner.
I make a label that set :
exampleLabel.layer.borderWidth = 2.0
exampleLabel.layer.borderColor = UIColor.black.cgColor
So to get something like this shape :
You can also draw in each corner a circle.
let circle = UIView(frame: CGRect(x: -2.5, y: -2.5, width: 5.0, height: 5.0))
circle.layer.cornerRadius = 2.5
circle.backgroundColor = UIColor.black
self.exampleLabel.addSubview(circle)
this is a circle in the left top corner. You can do it also for left bottom/right top/ right bottom.
You can play with the position by the x and y value
Let me know if this is what you want

Aligning SCNText with SCNBox in iOS SceneKit with boundingBox?

REVISED PROBLEM:
I don't understand why the white node is centered when it's a box or sphere but not when it's text. You can comment/uncomment the whiteGeometry variable to see how each different geometry is displayed. I was originally thinking that I had to manually center the text by determining the width of the box and the text and calculating the position of the text. Do I need to do that? Why is the text behaving differently?
import SceneKit
import PlaygroundSupport
let scene = SCNScene()
let sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
sceneView.scene = scene
sceneView.backgroundColor = .darkGray
sceneView.autoenablesDefaultLighting = true
sceneView.allowsCameraControl = true
PlaygroundPage.current.liveView = sceneView
// Camera
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 25)
sceneView.pointOfView = cameraNode
let blackGeometry = SCNBox(width: 10.0, height: 10.0, length: 10.0, chamferRadius: 0)
blackGeometry.firstMaterial?.diffuse.contents = UIColor.black
print("blackGeometry min=\(blackGeometry.boundingBox.min)")
print("blackGeometry max=\(blackGeometry.boundingBox.max)")
let blackNode = SCNNode(geometry: blackGeometry)
blackNode.position = SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(blackNode)
// let whiteGeometry = SCNBox(width: 3.0, height: 3.0, length: 3.0, chamferRadius: 0)
let whiteGeometry = SCNText(string: "L", extrusionDepth: 0)
whiteGeometry.alignmentMode = kCAAlignmentLeft
whiteGeometry.font = UIFont.systemFont(ofSize: 8.0)
// let whiteGeometry = SCNSphere(radius: 3.0)
whiteGeometry.firstMaterial?.diffuse.contents = UIColor.white
print("whiteGeometry min=\(whiteGeometry.boundingBox.min)")
print("whiteGeometry max=\(whiteGeometry.boundingBox.max)")
let whiteNode = SCNNode(geometry: whiteGeometry)
let boxWidth = blackGeometry.boundingBox.max.x - blackGeometry.boundingBox.min.x
let boxHeight = blackGeometry.boundingBox.max.y - blackGeometry.boundingBox.min.y
print("boxWidth=\(boxWidth)")
print("boxHeight=\(boxHeight)")
let txtWidth = whiteGeometry.boundingBox.max.x - whiteGeometry.boundingBox.min.x
let txtHeight = whiteGeometry.boundingBox.max.y - whiteGeometry.boundingBox.min.y
print("txtWidth=\(txtWidth)")
print("txtHeight=\(txtHeight)")
whiteNode.position = SCNVector3(x: -5.0, y: -5.0, z: 10)
scene.rootNode.addChildNode(whiteNode)
//blackNode.addChildNode(whiteNode)
print("done")
ORIGINAL PROBLEM (OLD):
Let's say I have two SCNBox nodes (I'm simplifying this to make it clear BUT the solution must work for other geometries). A large black box and a small white box. I want to center the white box in front of the black box.
To do this, I need to determine the width and height of the two nodes. Remember that the node could be something other than a box like a sphere or text. From what I can tell, the only way to determine width/height is via the boundingBox property on the geometry. It has a min and max value that is NOT clearly and fully described in Apple's reference manual. To get the height, it seems like I would calculate it based on boundingBox.max.y and boundingBox.min.y. So looking at the example below of a 10x10x10 box, I can't see how I can get 10.0 as the height because max.y 5.20507812
e.g.
let box = SCNBox(width: 10.0, height: 10.0, length: 10.0, chamferRadius: 0)
print("box min=\(box.boundingBox.min)")
print("box max=\(box.boundingBox.max)")
yields:
box min=SCNVector3(x: -5.0, y: -5.0, z: -5.0)
box max=SCNVector3(x: 5.0, y: 5.20507812, z: 5.0)
Why is max.y=5.20507812? How should I determine the height/width?
See also: SCNBoundingVolume
I think I understand now.
It looks like you're trying to put some text in front of an object, with the text centered on the object relative to the camera. And this question isn't about bounding boxes per se. Right?
You see different behavior with the white geometry being a sphere or SCNText because SCNText's uses a different origin convention than the other concrete SCNGeometry classes. Other geometries put the origin at the center. SCNText puts it at the lower left of the text. If I remember correctly, "lower" means the bottom of the font (lowest descender), not the baseline.
So you'll need to compute the text's bounding box, and account for that when you position the text node, to get it centered. You don't need to do that for the other geometry types. If the covered object or the camera is moving, you'll need to compute the ray from the camera to the covered object in the render loop, and update the text's position.
Swift Scenekit - Centering SCNText - the getBoundingBoxMin:Max issue is relevant.
If you just want overlay text, you might find it easier to put the text in an SKScene overlay. An example of that is at https://github.com/halmueller/ImmersiveInterfaces/tree/master/Tracking%20Overlay (warning, bit rot may have set in, I haven't tried that code in a while).

Resources