Rounding positional data from ARKit - swift

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.

Related

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.

How to smooth curve in UIBezierPath symmetrically

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.

Swift plot coordinates on uiimageview with world map

I am trying to plot some coordinates on the earth on an UIImage which contains a map of the world. (I don't want to use maps)
See an example of the UIImageView below below:
As you see it's working out pretty well but the mapping from coordinates and X Y are incorrect!
Amsterdam's coordinates are: (52.36666489, 4.883333206) and the Center's are (0,0).
I've done the following things to try to make this happen but unfortunately this isn't working out:
I've tried first to 'normalize' the coordinates since latitude ranges from -90 to 90 and latitude -180 to 180. This is done by adding 90 to the real latitude and 180 to the real longitude which yiels the 'normalized' versions:
let normalizedLat = location.coordinate.latitude + 90.0.
let normalizedLng = location.coordinate.longitude + 180.0
After that I've calculated the scale factor where the normalizedLat and normalizedLng should scale with:
let heightScaleFactor = mapImageView.frame.height / 180.0
let widthScaleFActor = mapImageView.frame.width / 360.0
And 3. After that i've got the scaling factors I finally can calculate the coordinates by:
let x = Double(widthScaleFActor * CGFloat(normalizedLng))
let y = Double(heightScaleFactor * CGFloat(normalizedLat))
dot.frame = CGRect(x: x, y: y, width: Double(dot.frame.width), height: Double(dot.frame.height))
But for some strange reason Amsterdam is not on the Amsterdam spot and the Center is not on the Center spot.
I am quite sure that my calculations has gone wrong. Any ideas?
Remember, in iOS the origin is in the top-left, not the bottom-left. Positive-y goes down, not up.
You need to factor that in.
dot.frame = CGRect(x: x, y: mapImageView.frame.height - y, width: Double(dot.frame.width), height: Double(dot.frame.height))
Also note that the equator in your image is not in the middle. It's lower in the image so you need to add an additional offset in your calculation of the y value based on the equator's offset in the image.
dot.frame = CGRect(x: x, y: mapImageView.frame.height - y + equatorOffset, width: Double(dot.frame.width), height: Double(dot.frame.height))
It's also possible that your map projection doesn't have a simple linear latitude scale. 0-10 degrees might be 12 pixels while 10-20 degrees might be 11 pixels, etc. and 80-90 is only 3 pixels (or whatever).

Adding multiple nodes on different positions on Y axis

I am trying to add many nodes on different positions on Y axis. The problem is that for some reason the positions are always (0:0).
I've went through pretty much every SO question related to this but couldn't find answer.
I am generating random number between the maximum and minimum value:
func randomBetweenTwoNumbers(firstNumber: CGFloat, secondNumber: CGFloat) -> CGFloat{
return CGFloat(arc4random())/CGFloat(UINT32_MAX) * abs(firstNumber - secondNumber) + min(firstNumber, secondNumber)
}
Now I am trying to add them like this:
func addLeftSparks(){
let randomNumber = Helper().randomBetweenTwoNumbers(firstNumber: leftSparkMinimumY, secondNumber: leftSparkMaximumY)
print(randomNumber)
let positions = [CGPoint(x: -275, y: randomNumber), CGPoint(x: -275, y: randomNumber)]
print(positions) // this is (0:0)
positions.enumerated().forEach { (index, point) in
let spriteNode = SKNode()
spriteNode.position = point
let sprite = SKSpriteNode(imageNamed: "Spark")
sprite.name = "Spark"
spriteNode.addChild(sprite)
}
}
The thing I am trying to achieve in picture:
Any help is appreciated, really.
You generated a random number, but you used it twice
let randomNumber = Helper().randomBetweenTwoNumbers(firstNumber: leftSparkMinimumY, secondNumber: leftSparkMaximumY)
print(randomNumber)
let positions = [CGPoint(x: -275, y: randomNumber), CGPoint(x: -275, y: randomNumber)]
positions is 2 points with the same y, so you will only see one node. You need to call the random number function for each new random number you want.
(I know you said it was 0,0, but I don't think that's right, so I am ignoring that part -- if you really believe that, put more information in the print statement so that you know you aren't being fooled by some other output)

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))