How to smooth curve in UIBezierPath symmetrically - swift

I am writing a custom component in Swift with UIBezierPath and I am facing difficulties to correctly find the control points to create a perfect path around a circle. Please have a look of the component below:
Please note that the circle is not following the yellow ball perfectly in the blue circle area, I tried to set the control points to the right position but they are not resulting a good result:
The function used to describe the path above is partially this:
//private let MAGIC_NUMBER: CGFloat = 0.552284749831
private let MAGIC_NUMBER: CGFloat = 0.5
//...
let secondPointEnd: CGPoint = CGPoint(x: middleLeft.x + ballRadius/2, y: middleLeft.y + ballRadius/2)
path.addCurve(
to: secondPointEnd,
controlPoint1: CGPoint(x: firstPointEnd.x, y: secondPointEnd.y - (ballRadius * MAGIC_NUMBER)),
controlPoint2: CGPoint(x: firstPointEnd.x, y: secondPointEnd.y))
let thirdPointStart: CGPoint = CGPoint(x: secondPointEnd.x + ballRadius/2, y: secondPointEnd.y - ballRadius/2)
path.addCurve(
to: thirdPointStart,
controlPoint1: CGPoint(x: thirdPointStart.x, y: secondPointEnd.y),
controlPoint2: CGPoint(x: thirdPointStart.x, y: secondPointEnd.y - (ballRadius * MAGIC_NUMBER)))
For the full source code (Playground): https://gist.github.com/ppamorim/feb3318ac30e20a75d9c62e8abdc2efa
I tried to fix it with multiple configurations, I had no success so far, I also tried to apply the magic number 0.552284749831 but it makes it worse. Am I missing some control point configuration? Is there any invalid control point there?
Could you guys please help me to find what is wrong with this bezier path?
Regards,
Pedro
With help from #Bob, I was able to complete the UIBezierPath like he described: https://gist.github.com/ppamorim/9390708c455a950a65621715ce7b6c82

I wouldn’t recommend rendering the circular bit with beziers. They’re extremely close, but not spot on. It’s fine approximation for corner rounding of a rectangle (or any polygon), but for a true circular portion of the path you should use addArc(withCenter:radius:startAngle:endAngle:clockwise:).
You can use beziers leading into and out of the arc, if you want, but for the circular portion, use an arc. E.g. see https://stackoverflow.com/a/60862388/1271826.

Related

ARKit - 2D Projection Result for a Point in 3D Space Changes Abruptly at Certain Device Position/Orientation

I'm working on an ARKit app that uses SceneKit as the rendering engine. It does some typical ARKit things like allowing the user to place 3D virtual content in the scene, detect and draw horizontal and vertical planes, etc.
The app also includes a feature to create measurement objects like those found in Apple's Measure app. The user taps an "add endpoint" button to add the first point of the measurement, moves the device to target the location for the second, and then completes the measurement by tapping the "add endpoint" button again.
Measurement Working
The issue is that with certain device orientations and distances from the measurement objects, the measurement objects behave erratically. The first end point will suddenly change position and the line and first end point will appear to "flip" about the second endpoint.
Measurement "Flipping"
The measurement feature creates SpriteKit assets and adds them to the overlaySKScene of the sceneView. These measurements record their start and end point positions in 3D space and project these points to 2D to reposition their SpriteKit components.
This is how I'm currently updating the position of the measurement items:
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
measurementNodes.forEach { $0.updateComponentPositions(renderer: sceneView) }
//...
}
The method updateComponentPositions is called every frame using the SCNSceneRendererDelegate method renderer(_, updateAtTime).
func updateComponentPositions(renderer: SCNSceneRenderer) {
// update start circle
let projectedStartPointScreenPosition = renderer.projectPoint(startPointPosition)
let startPointSKPosition = CGPoint(x: CGFloat(projectedStartPointScreenPosition.x),
y: scene.size.height - CGFloat(projectedStartPointScreenPosition.y) - startPointNode.frame.height/2)
startPointNode.position = startPointSKPosition
// update end circle
let projectedEndPointScreenPosition = renderer.projectPoint(endPointPosition)
let endPointSKPosition = CGPoint(x: CGFloat(projectedEndPointScreenPosition.x), y: scene.size.height - CGFloat(projectedEndPointScreenPosition.y) - endPointNode.frame.height/2)
endPointNode.position = endPointSKPosition
// update label
labelBackgroundNode.position = CGPoint(x: (startPointSKPosition.x + endPointSKPosition.x) / 2,
y: (startPointSKPosition.y + endPointSKPosition.y) / 2)
// reset line path
lineNode.path = getLinePath(from: startPointSKPosition,
to: endPointSKPosition)
}
This is the updateComponentPositions method of MeasurementNode which takes a renderer as an argument, projects the 3D points to 2D locations, and then updates the position of its geometries.
When attempting to debug this problem, I focused on the result of renderer.projectPoint(startPointPosition). The startPointPosition is a static SCNVector3 that holds the 3D position when the first endpoint was created. However, the result of this projectPoint method is what becomes erratic at this specific device orientation.
Here is a sample of the log of renderer.projectPoint(startPointPosition) when I hit that "flipping" point with the device:
SCNVector3(x: -6692.817, y: -13238.885, z: 0.49546456)
SCNVector3(x: -8706.296, y: -17154.777, z: 0.49417993)
SCNVector3(x: -12774.009, y: -25042.207, z: 0.491588)
SCNVector3(x: -24064.623, y: -46880.246, z: 0.48439798)
SCNVector3(x: -164018.86, y: -317208.25, z: 0.39538962) // magnitude becomes very large for x and y
SCNVector3(x: 17587.36, y: 33517.477, z: 0.510834) // x and y "flip"
SCNVector3(x: 9163.234, y: 17199.012, z: 0.50547314)
SCNVector3(x: 7649.4766, y: 14249.298, z: 0.5045147)
SCNVector3(x: 6688.1143, y: 12365.713, z: 0.5039065)
I tried replacing the updateComponentPositions method with one that takes the ARCamera as an argument in order to see if I would have better luck with its projectPoint(_ point: simd_float3, orientation: UIInterfaceOrientation, viewportSize: CGSize) method. I called it in that same renderer(_, updateAtTime) method, but achieved the same result.

Rounding positional data from ARKit

I have this code that gets X, Y, Z positions from each frame in ARKit.
let CamPosition = SCNVector3(transform.m41, transform.m42, transform.m43)
How would I round the numbers down because they output occasionally in scientific notation like this?
SCNVector3(x: 7.276927e-09, y: 2.4679738e-09, z: 3.395949e-10)
Instead of the desired output like this:
SCNVector3(x: 0.026048008, y: 0.0069037788, z: 0.010655182)
Any help is greatly appreciated!
Rounding to Meters
For that you can use three holy methods: round(_:), and ceil(_:), and floor(_:).
import SceneKit
import Foundation
let node = SCNNode()
node.position = SCNVector3(x: floor(12.856288),
y: ceil(67.235459),
z: round(34.524305))
node.position.x // 12
node.position.y // 68
node.position.z // 35
Rounding XYZ values to integer, you make them to translate intermittently (discretely) in meters.
Rounding to Centimeters
Rounding XYZ values to 2 decimal places:
node.position = SCNVector3(x: round(12.856288 * 100) / 100.0,
y: round(67.235459 * 100) / 100.0,
z: round(34.524305 * 100) / 100.0)
node.position.x // 12.86 (hundredths)
node.position.y // 67.24
node.position.z // 34.52
It seems to me like you're trying to fix that doesn't need to be fixed.
As you can see from this very comprehensive answer, computers are just not good at storing some decimal numbers, so what you're getting is the nearest neighbour that can be represented in binary.
Unless this is causing you functional issues, I would recommend ignoring it. If the problem is that your debug logs have numbers that are not easy to parse, use a number formatter.

SpriteKit: SKPhysicsJointLimit not respecting 'maxLength'

I'm trying to create a chain-like structure in SpriteKit and I'm having trouble understanding the behavior of SKPhysicsJointLimit's maxLength property. It seems not to do anything at all.
This question didn't solve my problem.
According to the documentation, maxLength is The maximum distance allowed between the two physics bodies connected by the limit joint.
However, my two nodes become oriented much farther apart than their maxLength value. It's true that I'm setting their initial positions to be farther apart than maxLength -- but I would expect the nodes to pull together during the simulation, as if tied together by a stretchy rope. Instead, the nodes remain far apart.
So, here's some code that sets a joint between two SKSpriteNodes.
let screen = UIScreen.main.bounds
let bodyA = SKSpriteNode(imageNamed: "box.png")
let bodyB = SKSpriteNode(imageNamed: "box.png")
bodyA.size = CGSize(width: 20, height: 20)
bodyB.size = CGSize(width: 20, height: 20)
bodyA.position = CGPoint(x: screen.width*0.4, y: screen.height*0.8)
bodyB.position = CGPoint(x: screen.width*0.6, y: screen.height*0.8)
bodyA.physicsBody = SKPhysicsBody(circleOfRadius: 20)
bodyB.physicsBody = SKPhysicsBody(circleOfRadius: 20)
addChild(bodyA)
addChild(bodyB)
let pinJoint = SKPhysicsJointLimit.joint(withBodyA: bodyA.physicsBody!, bodyB: bodyB.physicsBody!, anchorA: CGPoint(x: 0.5, y: 0.5), anchorB: CGPoint(x: 0.5, y: 0.5))
//This doesn't seem to do anything:
pinJoint.maxLength = 5.0
scene?.physicsWorld.add(pinJoint)
In the simulation, it's clear that there is a physics joint connecting the two nodes -- it's just that the nodes are much farther apart than they should be.
Why doesn't my maxLength value change the behavior of my two nodes, and how do I fix the problem? What am I not understanding?
Thanks for your input!
Be sure that the anchor points are in scene coordinates, as described in the documentation. The (0.5, 0.5) is likely intended to be "center of the sprite" or something like that, but that's not correct for a joint.

Can You Add SKPhysicsBody To Only One Side Of A Rectangular Node?

So let's say I have this rectangular SKNode. Can I add SKPhysicsBody to only one side of the rectangle i.e. only the top side, so that only the top side could detect collision and not the other sides (then the physics indicator blue line would only appear on the top and not anywhere else)?
I wanted to create a game, but I don't know whether something like this could work.
So if I could, how?
If I can't, is there a way around this issue?
I wanted to add a normal rectangle with physicsBody on only one side.
See the rectangle image here, the blue part is the place where I want the physicsBody
So I've tried adding an edgeBased physicsBody, but it doesn't seem to work (either the physicsBody didn't get created or it is in the wrong position).
let rectangle = SKSpriteNode(imageNamed: "Rectangle")
rectangle.size = CGSize(width: 128, height: 128)
rectangle.position = CGPoint(x: frame.midX, y: frame.midY)
rectangle.physicsBody = SKPhysicsBody(edgeFrom: CGPoint(x: rectangle.position.x - rectangle.size.width/2, y: rectangle.position.y + rectangle.size.width/2), to: CGPoint(x: rectangle.position.x + rectangle.size.width/2, y: rectangle.position.y + rectangle.size.width/2))
rectangle.physicsBody!.restitution = 0.0
rectangle.physicsBody!.categoryBitMask = physicsCategories.groundCategory
rectangle.physicsBody!.collisionBitMask = physicsCategories.squareCategory
addChild(rectangle)
Thanks!
You could, but it would also detect collision coming from the 'wrong' side i.e. with an object that has traveled through the node.
Physics bodies can be volume or edge based, but even an edge-based one that was only a pixel thick still has 4 sides.
If you could supply a drawing of exactly what you want, we could help better as there are probably several different approaches.

SceneKit – Rotate and animate a SCNNode

I'm trying to display a pyramid that points following the z axis and then rotates on itself around z too.
As my camera is on the z axis, I'm expecting to see the pyramid from above. I managed to rotate the pyramid to see it this way but when I add the animation it seems to rotate on multiple axis.
Here is my code:
// The following create the pyramid and place it how I want
let pyramid = SCNPyramid(width: 1.0, height: 1.0, length: 1.0)
let pyramidNode = SCNNode(geometry: pyramid)
pyramidNode.position = SCNVector3(x: 0, y: 0, z: 0)
pyramidNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(M_PI / 2))
scene.rootNode.addChildNode(pyramidNode)
// But the animation seems to rotate aroun 2 axis and not just z
var spin = CABasicAnimation(keyPath: "rotation")
spin.byValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: 2*Float(M_PI)))
spin.duration = 3
spin.repeatCount = HUGE
pyramidNode.addAnimation(spin, forKey: "spin around")
Trying to both manually set and animate the same property can cause issues. Using a byValue animation makes the problem worse -- that concatenates to the current transform, so it's harder to keep track of whether the current transform is what the animation expects to start with.
Instead, separate the fixed orientation of the pyramid (its apex is in the -z direction) from the animation (it spins around the axis it points in). There's two good ways to do this:
Make pyramidNode the child of another node that gets the one-time rotation (π/2 around x-axis), and apply the spin animation directly to pyramidNode. (In this case, the apex of the pyramid will still point in the +y direction of its local space, so you'll want to spin around that axis instead of the z-axis.)
Use the pivot property to transform the local space of pyramidNode's contents, and animate pyramidNode relative to its containing space.
Here's some code to show the second approach:
let pyramid = SCNPyramid(width: 1.0, height: 1.0, length: 1.0)
let pyramidNode = SCNNode(geometry: pyramid)
pyramidNode.position = SCNVector3(x: 0, y: 0, z: 0)
// Point the pyramid in the -z direction
pyramidNode.pivot = SCNMatrix4MakeRotation(CGFloat(M_PI_2), 1, 0, 0)
scene.rootNode.addChildNode(pyramidNode)
let spin = CABasicAnimation(keyPath: "rotation")
// Use from-to to explicitly make a full rotation around z
spin.fromValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: 0))
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: CGFloat(2 * M_PI)))
spin.duration = 3
spin.repeatCount = .infinity
pyramidNode.addAnimation(spin, forKey: "spin around")
Some unrelated changes to improve code quality:
Use CGFloat when explicit conversion is required to initialize an SCNVector component; using Float or Double specifically will break on 32 or 64 bit architecture.
Use .infinity instead of the legacy BSD math constant HUGE. This type-infers to whatever the type of spin.repeatCount is, and uses a constant value that's defined for all floating-point types.
Use M_PI_2 for π/2 to be pedantic about precision.
Use let instead of var for the animation, since we never assign a different value to spin.
More on the CGFloat error business: In Swift, numeric literals have no type until the expression they're in needs one. That's why you can do things like spin.duration = 3 -- even though duration is a floating-point value, Swift lets you pass an "integer literal". But if you do let d = 3; spin.duration = d you get an error. Why? Because variables/constants have explicit types, and Swift doesn't do implicit type conversion. The 3 is typeless, but when it gets assigned to d, type inference defaults to choosing Int because you haven't specified anything else.
If you're seeing type conversion errors, you probably have code that mixes literals, constants, and/or values returned from functions. You can probably just make the errors go away by converting everything in the expression to CGFloat (or whatever the type you're passing that expression to is). Of course, that'll make your code unreadable and ugly, so once you get it working you might start removing conversions one at a time until you find the one that does the job.
SceneKit includes animation helpers which are much simpler & shorter to use than CAAnimations. This is ObjC but gets across the point:
[pyramidNode runAction:
[SCNAction repeatActionForever:
[SCNAction rotateByX:0 y:0 z:2*M_PI duration:3]]];
I changed byValue to toValue and this worked for me. So change the line...
spin.byValue = NSValue(SCNVector4: SCNVector4(...
Change it to...
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 0, y:0, z:1, w: 2*float(M_PI))