I'm just a beginner in Swift but I was trying to implement this particular feature in a project of mine (word search puzzle): selecting multiple cells with a pan gesture. Each of my cells contains a label that holds a single character. I'd like to get all the letters the user wants to select through a pan gesture. Currently I'm doing this by calculating the distance from the center of each cell to the line passing though points a (touch began) and b (touch ended) and taking into consideration only the cells that lie close enough to that line. This works fine most of the time but it's still prone to errors and it is sure to lead to user dissatisfaction. Could you suggest a simpler way of doing this?
Thanks.
Some code:
func getGridInfo(){
for i in 0...numberOfCells-1{
let indPath = IndexPath(row:i, section:0);
let cell = collectionView?.cellForItem(at: indPath) as? CustomCell;
let frameRect = collectionView?.convert((cell?.frame)!, to: self.view);
coordinates[i] = (cell?.label.text, frameRect?.midX, frameRect?.midY) as? (String, CGFloat, CGFloat);
}
}
#objc func handlePan(touch: UIPanGestureRecognizer){
let touchPoint = touch.location(in: self.view)
switch touch.state{
case .began:
if let line = curLine{
line.removeFromSuperlayer();
}
firstPoint = touchPoint;
case .ended:
secondPoint = touchPoint;
guard let fP = firstPoint,
let sP = secondPoint else {
print("Something went wrong..");
return;
}
let word = getCloseLetters(start:fP, end:sP);
if (words.contains(word) || words.contains(String(word.reversed()))){
playSound(soundUrl: correctUrlSoundFx);
foundWordsLabel.text = word;
foundWordsLabel.textColor = UIColor.green;
// to do: update score
}else{
playSound(soundUrl: incorrectUrlSoundFx);
let attributedString = NSMutableAttributedString(string:word);
attributedString.addAttribute(NSAttributedStringKey.strikethroughStyle, value:2, range: NSMakeRange(0, attributedString.length));
foundWordsLabel.attributedText = attributedString;
foundWordsLabel.textColor = UIColor.red;
}
curLine = addLine(fromPoint: firstPoint!, toPoint: secondPoint!, width:20);
default:
return;
}
func getCloseLetters(start:CGPoint, end:CGPoint) -> String{
var letters = "";
for i in 0...numberOfCells-1{
let tuple = coordinates[i]!;
let x = tuple.1;
let y = tuple.2;
if (checkIfPointIsCloseOrOnTheLine(start: start, end: end, aPoint: CGPoint(x:x,y:y)) == true){
letters += tuple.0;
}
}
return letters;
}
func distanceFromPoint(p: CGPoint, v: CGPoint, w: CGPoint) -> CGFloat {
let pv_dx = p.x - v.x;
let pv_dy = p.y - v.y;
let wv_dx = w.x - v.x;
let wv_dy = w.y - v.y;
let dot = pv_dx * wv_dx + pv_dy * wv_dy;
let len_sq = wv_dx * wv_dx + wv_dy * wv_dy;
let param = dot / len_sq;
var int_x, int_y: CGFloat;
if param < 0 || (v.x == w.x && v.y == w.y) {
int_x = v.x;
int_y = v.y;
} else if param > 1 {
int_x = w.x;
int_y = w.y;
} else {
int_x = v.x + param * wv_dx;
int_y = v.y + param * wv_dy;
}
let dx = p.x - int_x;
let dy = p.y - int_y;
return sqrt(dx * dx + dy * dy);
}
func checkIfPointIsCloseOrOnTheLine(start:CGPoint, end:CGPoint, aPoint:CGPoint, tollerance:CGFloat = 15) -> Bool{
let barHeight = self.navigationController?.navigationBar.frame.size.height;
let newStartPoint = CGPoint(x:start.x, y:start.y - barHeight!);
let newEndPoint = CGPoint(x:end.x, y:end.y - barHeight!);
let distance = distanceFromPoint(p:aPoint, v:newStartPoint, w:newEndPoint);
if distance > tollerance{
return false;
}
return true;
}
Related
How can i detect if an ARAnchor is currently visible in the camera, i need to test when the camera view changes.
I want to put arrows on the edge of the screen that point in the direction of the anchor when not on screen. I need to know if the node sits to the left or right of the frustum.
I am now doing this but it says pin is visible when it is not and the X values seem not right? Maybe the renderer frustum does not match the screen camera?
var deltaTime = TimeInterval()
public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
deltaTime = time - lastUpdateTime
if deltaTime>1{
if let annotation = annotationsByNode.first {
let node = annotation.key.childNodes[0]
if !renderer.isNode(node, insideFrustumOf: renderer.pointOfView!)
{
print("Pin is not visible");
}else {
print("Pin is visible");
}
let pnt = renderer.projectPoint(node.position)
print("pos ", pnt.x, " ", renderer.pointOfView!.position)
}
lastUpdateTime = time
}
}
Update: The code works to show if node is visible or not, how can i tell which direction left or right a node is in relation to the camera frustum?
update2! as suggested answer from Bhanu Birani
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
let leftPoint = CGPoint(x: 0, y: screenHeight/2)
let rightPoint = CGPoint(x: screenWidth,y: screenHeight/2)
let leftWorldPos = renderer.unprojectPoint(SCNVector3(leftPoint.x,leftPoint.y,0))
let rightWorldPos = renderer.unprojectPoint(SCNVector3(rightPoint.x,rightPoint.y,0))
let distanceLeft = node.position - leftWorldPos
let distanceRight = node.position - rightWorldPos
let dir = (isVisible) ? "visible" : ( (distanceLeft.x<distanceRight.x) ? "left" : "right")
I got it working finally which uses the idea from Bhanu Birani of the left and right of the screen but i get the world position differently, unProjectPoint and also get a scalar value of distance which i compare to get the left/right direction. Maybe there is a better way of doing it but it worked for me
public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
deltaTime = time - lastUpdateTime
if deltaTime>0.25{
if let annotation = annotationsByNode.first {
guard let pointOfView = renderer.pointOfView else {return}
let node = annotation.key.childNodes[0]
let isVisible = renderer.isNode(node, insideFrustumOf: pointOfView)
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
let leftPoint = CGPoint(x: 0, y: screenHeight/2)
let rightPoint = CGPoint(x: screenWidth,y: screenHeight/2)
let leftWorldPos = renderer.unprojectPoint(SCNVector3(leftPoint.x, leftPoint.y,0))
let rightWorldPos = renderer.unprojectPoint(SCNVector3(rightPoint.x, rightPoint.y,0))
let distanceLeft = node.worldPosition.distance(vector: leftWorldPos)
let distanceRight = node.worldPosition.distance(vector: rightWorldPos)
//let pnt = renderer.projectPoint(node.worldPosition)
//guard let pnt = renderer.pointOfView!.convertPosition(node.position, to: nil) else {return}
let dir = (isVisible) ? "visible" : ( (distanceLeft<distanceRight) ? "left" : "right")
print("dir" , dir, " ", leftWorldPos , " ", rightWorldPos)
lastDir=dir
delegate?.nodePosition?(node:node, pos: dir)
}else {
delegate?.nodePosition?(node:nil, pos: lastDir )
}
lastUpdateTime = time
}
extension SCNVector3
{
/**
* Returns the length (magnitude) of the vector described by the SCNVector3
*/
func length() -> Float {
return sqrtf(x*x + y*y + z*z)
}
/**
* Calculates the distance between two SCNVector3. Pythagoras!
*/
func distance(vector: SCNVector3) -> Float {
return (self - vector).length()
}
}
Project the ray from the from the following screen positions:
leftPoint = CGPoint(0, screenHeight/2) (centre left of the screen)
rightPoint = CGPoint(screenWidth, screenHeight/2) (centre right of the screen)
Convert CGPoint to world position:
leftWorldPos = convertCGPointToWorldPosition(leftPoint)
rightWorldPos = convertCGPointToWorldPosition(rightPoint)
Calculate the distance of node from both world position:
distanceLeft = node.position - leftWorldPos
distanceRight = node.position - rightWorldPos
Compare distance to find the shortest distance to the node. Use the shortest distance vector to position direction arrow for object.
Here is the code from tsukimi to check if the object is in right side of screen or on left side:
public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
deltaTime = time - lastUpdateTime
if deltaTime>0.25{
if let annotation = annotationsByNode.first {
guard let pointOfView = renderer.pointOfView else {return}
let node = annotation.key.childNodes[0]
let isVisible = renderer.isNode(node, insideFrustumOf: pointOfView)
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
let leftPoint = CGPoint(x: 0, y: screenHeight/2)
let rightPoint = CGPoint(x: screenWidth,y: screenHeight/2)
let leftWorldPos = renderer.unprojectPoint(SCNVector3(leftPoint.x, leftPoint.y,0))
let rightWorldPos = renderer.unprojectPoint(SCNVector3(rightPoint.x, rightPoint.y,0))
let distanceLeft = node.worldPosition.distance(vector: leftWorldPos)
let distanceRight = node.worldPosition.distance(vector: rightWorldPos)
//let pnt = renderer.projectPoint(node.worldPosition)
//guard let pnt = renderer.pointOfView!.convertPosition(node.position, to: nil) else {return}
let dir = (isVisible) ? "visible" : ( (distanceLeft<distanceRight) ? "left" : "right")
print("dir" , dir, " ", leftWorldPos , " ", rightWorldPos)
lastDir=dir
delegate?.nodePosition?(node:node, pos: dir)
}else {
delegate?.nodePosition?(node:nil, pos: lastDir )
}
lastUpdateTime = time
}
Following is the class to help performing operations on vector
extension SCNVector3 {
init(_ vec: vector_float3) {
self.x = vec.x
self.y = vec.y
self.z = vec.z
}
func length() -> Float {
return sqrtf(x * x + y * y + z * z)
}
mutating func setLength(_ length: Float) {
self.normalize()
self *= length
}
mutating func setMaximumLength(_ maxLength: Float) {
if self.length() <= maxLength {
return
} else {
self.normalize()
self *= maxLength
}
}
mutating func normalize() {
self = self.normalized()
}
func normalized() -> SCNVector3 {
if self.length() == 0 {
return self
}
return self / self.length()
}
static func positionFromTransform(_ transform: matrix_float4x4) -> SCNVector3 {
return SCNVector3Make(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
}
func friendlyString() -> String {
return "(\(String(format: "%.2f", x)), \(String(format: "%.2f", y)), \(String(format: "%.2f", z)))"
}
func dot(_ vec: SCNVector3) -> Float {
return (self.x * vec.x) + (self.y * vec.y) + (self.z * vec.z)
}
func cross(_ vec: SCNVector3) -> SCNVector3 {
return SCNVector3(self.y * vec.z - self.z * vec.y, self.z * vec.x - self.x * vec.z, self.x * vec.y - self.y * vec.x)
}
}
extension SCNVector3{
func distance(receiver:SCNVector3) -> Float{
let xd = receiver.x - self.x
let yd = receiver.y - self.y
let zd = receiver.z - self.z
let distance = Float(sqrt(xd * xd + yd * yd + zd * zd))
if (distance < 0){
return (distance * -1)
} else {
return (distance)
}
}
}
Here is the code snippet to convert tap location or any CGPoint to world transform.
#objc func handleTap(_ sender: UITapGestureRecognizer) {
// Take the screen space tap coordinates and pass them to the hitTest method on the ARSCNView instance
let tapPoint = sender.location(in: sceneView)
let result = sceneView.hitTest(tapPoint, types: ARHitTestResult.ResultType.existingPlaneUsingExtent)
// If the intersection ray passes through any plane geometry they will be returned, with the planes
// ordered by distance from the camera
if (result.count > 0) {
// If there are multiple hits, just pick the closest plane
if let hitResult = result.first {
let finalPosition = SCNVector3Make(hitResult.worldTransform.columns.3.x + insertionXOffset,
hitResult.worldTransform.columns.3.y + insertionYOffset,
hitResult.worldTransform.columns.3.z + insertionZOffset
);
}
}
}
Following is the code to get hit test results when there's no plane found.
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result = hitResults[0]
}
This answer is a bit late but can be useful for someone needing to know where a node is in camera space relatively to the center (e.g. top left corner, centered ...).
You can get your node position in camera space using scene.rootNode.convertPosition(node.position, to: pointOfView).
In camera space,
(isVisible && (x=0, y=0)) means that your node is in front of the camera.
(isVisible && (x=0.1)) means that the node is a little bit on the right.
Some sample code :
public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
deltaTime = time - lastUpdateTime
if deltaTime>0.25{
if let annotation = annotationsByNode.first {
guard let pointOfView = renderer.pointOfView else {return}
let node = annotation.key.childNodes[0]
let isVisible = renderer.isNode(node, insideFrustumOf: pointOfView)
// Translate node to camera space
let nodeInCameraSpace = scene.rootNode.convertPosition(node.position, to: pointOfView)
let isCentered = isVisible && (nodeInCameraSpace.x < 0.1) && (nodeInCameraSpace.y < 0.1)
let isOnTheRight = isVisible && (nodeInCameraSpace.x > 0.1)
// ...
delegate?.nodePosition?(node:node, pos: dir)
}else {
delegate?.nodePosition?(node:nil, pos: lastDir )
}
lastUpdateTime = time
}
With ARKit I create boxes in my room and then I want to be able to exit AR scene to see my boxes and rotate around it.
I tried with allowsCameraControl = true and now I'm able to zoom and drag my objects but I would like the camera to turn around them while for now the object turns around me. I've looked the 604's video of WWDC which explains the option is self.sceneView.defaultCameraController.interactionMode = .orbitTurntable but I can't manage to make it work...
Basically all my 'box' are on the horizontal plane and I have a topBox which is at my ceiling.
What I've tried:
func getCenterBox() -> SCNNode? {
guard let _ = self.sceneView.scene.rootNode.childNode(withName: "box", recursively: true) else {
return nil
}
let edges = self.sceneView.scene.rootNode.childNodes.filter { $0.name == "box" }
let edgesByX = edges.sorted { $0.position.x < $1.position.x }
let minX = edgesByX.first?.position.x
let maxX = edgesByX.last?.position.x
let edgesByZ = edges.sorted { $0.position.z < $1.position.z }
let minZ = edgesByZ.first?.position.z
let maxZ = edgesByZ.last?.position.z
let centerBox = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.5)
let centerNode = SCNNode(geometry: centerBox)
let centerNodePosition = SCNVector3Make((maxX! - minX!)/2 + minX!, ((topBox?.position.y)! - (edges.first?.position.y)!)/2 + (edges.first?.position.y)!, (maxZ! - minZ!)/2 + minZ!)
centerNode.position = centerNodePosition
return SCNNode(geometry: SCNGeometry())
}
#IBAction func stopARTapped(_ sender: UIButton) {
self.sceneView.allowsCameraControl = true
self.sceneView.defaultCameraController.interactionMode = .orbitTurntable
self.sceneView.defaultCameraController.inertiaEnabled = true
self.sceneView.defaultCameraController.maximumHorizontalAngle = 0
self.sceneView.defaultCameraController.maximumVerticalAngle = 0
self.sceneView.defaultCameraController.minimumHorizontalAngle = 0
self.sceneView.defaultCameraController.minimumVerticalAngle = 0
guard let centerNode = getCenterBox() else {
return
}
self.sceneView.scene.rootNode.addChildNode(centerNode)
let lookAtConstraint = SCNLookAtConstraint(target: centerNode)
if self.sceneView.pointOfView?.constraints == nil {
self.sceneView.pointOfView?.constraints = [lookAtConstraint]
} else {
self.sceneView.pointOfView?.constraints?.append(lookAtConstraint)
}
}
I am makeing a game in which I want that the enemies move following a random pattern within a circle. I already made that the enemies spawn randomly in all the sides of the screen, but the problem is that I dont know how to make the enemies move following a random pattern within a circle just like the image.
class GameScene: SKScene, SKPhysicsContactDelegate {
var circuloPrincipal = SKSpriteNode(imageNamed: "circulo")
var enemigoTimer = NSTimer()
}
override func didMoveToView(view: SKView) {
circuloPrincipal.size = CGSize(width: 225, height: 225)
circuloPrincipal.position = CGPoint(x: frame.width / 2, y: frame.height / 2)
circuloPrincipal.color = colorAzul
circuloPrincipal.colorBlendFactor = 1.0
circuloPrincipal.name = "circuloPrincipal"
circuloPrincipal.zPosition = 1.0
self.addChild(circuloPrincipal)
override func touchesBegan(touches: Set, withEvent event: UIEvent?) {
enemigoTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("enemigos"), userInfo: nil, repeats: true)
}
func enemigos() {
let enemigo = SKSpriteNode(imageNamed: "enemigo")
enemigo.size = CGSize(width: 25, height: 25)
enemigo.zPosition = 2.0
enemigo.name = "enemigo"
let posisionRandom = arc4random() % 4
switch posisionRandom {
case 0:
enemigo.position.x = 0
let posisionY = arc4random_uniform(UInt32(frame.size.height))
enemigo.position.y = CGFloat(posisionY)
self.addChild(enemigo)
break
case 1:
enemigo.position.y = 0
let posisionX = arc4random_uniform(UInt32(frame.size.width))
enemigo.position.x = CGFloat(posisionX)
self.addChild(enemigo)
break
case 2:
enemigo.position.y = frame.size.height
let posisionX = arc4random_uniform(UInt32(frame.size.width))
enemigo.position.x = CGFloat(posisionX)
self.addChild(enemigo)
break
case 3:
enemigo.position.x = frame.size.width
let posisionY = arc4random_uniform(UInt32(frame.size.height))
enemigo.position.y = CGFloat(posisionY)
self.addChild(enemigo)
break
default:
break
}
enemigo.runAction(SKAction.moveTo(circuloPrincipal.position, duration: 1.4))
}
Try to add this code:
let randomY = CGFloat(Int.random(-Int(circuloPrincipal.frame.height/2)...Int(circuloPrincipal.frame.height/2)))
let randomX = CGFloat(Int.random(-Int(circuloPrincipal.frame.width/2)...Int(circuloPrincipal.frame.width/2)))
let slopeToCirculoPrincipal = (enemigo.position.y - circuloPrincipal.position.y + randomY ) / (enemigo.position.x - circuloPrincipal.position.x + randomX)
let constant = enemigo.position.y - slopeToCirculoPrincipal * enemigo.position.x
let finalX : CGFloat = enemigo.position.x < circuloPrincipal.position.x ? 1500.0 : -1500.0 // Set it to somewhere outside screen size
let finalY = constant + slopeToCirculoPrincipal * finalX
let distance = (enemigo.position.y - finalY) * (enemigo.position.y - finalY) + (enemigo.position.x - finalX) * (enemigo.position.x - finalX)
let enemigoSpeed : CGFloat = 100.0
let timeToCoverDistance = sqrt(distance) / enemigoSpeed
let moveAction = SKAction.moveTo(CGPointMake(finalX, finalY), duration: NSTimeInterval(timeToCoverDistance))
let removeAction = SKAction.runBlock { () -> Void in
enemigo.removeFromParent()
}
enemigo.runAction(SKAction.sequence([moveAction,removeAction]))
Instead of:
enemigo.runAction(SKAction.moveTo(circuloPrincipal.position, duration: 1.4))
Also you need to put this extension somewhere in your project:
extension Int
{
static func random(range: Range<Int> ) -> Int
{
var offset = 0
if range.startIndex < 0 // allow negative ranges
{
offset = abs(range.startIndex)
}
let mini = UInt32(range.startIndex + offset)
let maxi = UInt32(range.endIndex + offset)
return Int(mini + arc4random_uniform(maxi - mini)) - offset
}
}
I'm the author of a Hearthstone tracker, and I have to move several NSWindow over Hearthstone window.
I get the frame of Hearthstone using CGWindowListCopyWindowInfo.
Then, I have to move my windows at some positions relative to Hearthstone.
The red arrows are over opponent cards, green arrow is over turn button and blue arrows are at the left and right of the window.
My actual screen setup is the following :
which gives me the following frames
// screen 1 : {x 0 y 0 w 1.440 h 900}
// screen 2 : {x 1.440 y -180 w 1.920 h 1.080}
To place the opponent tracker (the left frame) at the right position, which is the most simple case, I use {x 0 y somepadding w 185 h hearthstoneHeight - somepadding} and get the correct frame with this
func relativeFrame(frame: NSRect) -> NSRect {
var relative = frame
relative.origin.x = NSMinX(hearthstoneFrame) + NSMinX(frame)
relative.origin.y = NSMinY(hearthstoneFrame) + NSMinY(frame)
return relative
}
The right tracker is placed using {x hearthstoneWidth - trackerWidth, ...}
For other overlays, I used my current (Hearthstone) resolution to place them and them calculate them using a simple math
x = x / 1404.0 * NSWidth(hearthstoneFrame)
y = y / 840.0 * NSHeight(hearthstoneFrame)
This works pretty well. Except if I use my second screen. In this case, the frames seems to be correct, but the position of the window is not good.
Here is a screenshot of a debug window with {x 0 y 0 w hearthstoneWidth h hearthsoneHeight }. If I compare the frames of Hearthstone and my overlay, they are identical.
The complete function is the following (I'm in a "static class", I only show revelant code). I guess I'm missing something in the calculation but I can't find what.
class frameRelative {
static var hearthstoneFrame = NSZeroRect
static func findHearthstoneFrame() {
let options = CGWindowListOption(arrayLiteral: .ExcludeDesktopElements)
let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
if let info = (windowListInfo as NSArray? as? [[String: AnyObject]])?
.filter({
!$0.filter({ $0.0 == "kCGWindowName" && $0.1 as? String == "Hearthstone" }).isEmpty
})
.first {
var rect = NSRect()
let bounds = info["kCGWindowBounds"] as! CFDictionary
CGRectMakeWithDictionaryRepresentation(bounds, &rect)
rect.size.height -= titleBarHeight // remove the 22px from the title
hearthstoneFrame = rect
}
}
static func frameRelative(frame: NSRect, _ isRelative: Bool = false) -> NSRect {
var relative = frame
var pointX = NSMinX(relative)
var pointY = NSMinY(relative)
if isRelative {
pointX = pointX / 1404.0 * NSWidth(hearthstoneFrame)
pointY = pointY / 840.0 * NSHeight(hearthstoneFrame)
}
let x: CGFloat = NSMinX(hearthstoneFrame) + pointX
let y = NSMinY(hearthstoneFrame) + pointY
relative.origin = NSMakePoint(x, y)
return relative
}
}
// somewhere here
let frame = NSMakeRect(0, 0, hearthstoneWidth, hearthstoneHeight)
let relativeFrame = SizeHelper.frameRelative(frame)
myWindow.setFrame(relativeFrame, display: true)
Any help will be appreciate :)
I eventually solved this issue so I decided to post the answer to close this thread... and maybe if someone face the same issue one day.
The solution was to substract the max y from the first screen with the max y of the Hearthstone window.
The final code of findHearthstoneFrame is
static func findHearthstoneFrame() {
let options = CGWindowListOption(arrayLiteral: .ExcludeDesktopElements)
let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
if let info = (windowListInfo as NSArray? as? [[String: AnyObject]])?.filter({
!$0.filter({ $0.0 == "kCGWindowName"
&& $0.1 as? String == "Hearthstone" }).isEmpty
}).first {
if let id = info["kCGWindowNumber"] as? Int {
self.windowId = CGWindowID(id)
}
var rect = NSRect()
let bounds = info["kCGWindowBounds"] as! CFDictionary
CGRectMakeWithDictionaryRepresentation(bounds, &rect)
if let screen = NSScreen.screens()?.first {
rect.origin.y = NSMaxY(screen.frame) - NSMaxY(rect)
}
self._frame = rect
}
}
And the frameRelative is
static let BaseWidth: CGFloat = 1440.0
static let BaseHeight: CGFloat = 922.0
var scaleX: CGFloat {
return NSWidth(_frame) / SizeHelper.BaseWidth
}
var scaleY: CGFloat {
// 22 is the height of the title bar
return (NSHeight(_frame) - 22) / SizeHelper.BaseHeight
}
func frameRelative(frame: NSRect, relative: Bool = true) -> NSRect {
var pointX = NSMinX(frame)
var pointY = NSMinY(frame)
let width = NSWidth(frame)
let height = NSHeight(frame)
if relative {
pointX = pointX * scaleX
pointY = pointY * scaleY
}
let x: CGFloat = NSMinX(self.frame) + pointX
let y: CGFloat = NSMinY(self.frame) + pointY
let relativeFrame = NSRect(x: x, y: y, width: width, height: height)
return relativeFrame
}
I'm trying to implement a verlet rope in swift. I'm running into the problem where when trying to fix the position of the point masses from constraints, they very rapidly grow apart and then the coordinates become NaNs. Any idea what could be wrong in my code?
import Foundation
private let DEF_POINT_COUNT = 10
private let CONST_ITERATIONS = 5
#objc class PointMass: DebugPrintable {
var point: NSPoint
private var oldPoint: NSPoint
var x: Double {
get { return Double(point.x) }
}
var y: Double {
get { return Double(point.y) }
}
var debugDescription: String {
get { return "\(point)" }
}
init(_ point: NSPoint) {
self.point = point
self.oldPoint = point
}
func updatePosition() {
let dx = point.x - oldPoint.x
let dy = (point.y - oldPoint.y)
oldPoint = point
point = NSPoint(x: point.x + dx, y: point.y + dy)
}
func updatePosition(point: NSPoint) {
let dx = point.x - self.point.x
let dy = point.y - self.point.y
self.oldPoint = NSPoint(x: oldPoint.x + dx, y: oldPoint.y + dy)
self.point = point
}
}
struct Constraint {
var p1: PointMass
var p2: PointMass
var len: Double
func fixPoints() {
let dx = p2.x - p1.x
let dy = p2.y - p1.y
let dist = sqrt(dx*dx + dy*dy)
let diff = (dist - len)/len
p2.updatePosition(NSPoint(x: p2.x - diff*dx*0.5, y: p2.y - diff*dy*0.5))
p1.updatePosition(NSPoint(x: p1.x + diff*dx*0.5, y: p1.y + diff*dy*0.5))
}
}
#objc class Rope: NSObject {
let points: [PointMass]
let constraints: [Constraint]
init(anchor: NSPoint, end: NSPoint, length: Double, count: Int = DEF_POINT_COUNT) {
let anchorPoint = PointMass(anchor)
let endPoint = PointMass(end)
let dx = (anchorPoint.x - endPoint.x)/Double(count)
let dy = (anchorPoint.y - endPoint.y)/Double(count)
let constraintLength = length/Double(count)
var points = [endPoint]
var constraints: [Constraint] = []
for i in 1...count {
let prevPoint = points[i-1]
let newPoint = PointMass(NSPoint(x: prevPoint.x + dx, y: prevPoint.y + dy))
let constraint = Constraint(p1: prevPoint, p2: newPoint, len: constraintLength)
points.append(newPoint)
constraints.append(constraint)
}
self.points = points
self.constraints = constraints
}
func update(anchor: NSPoint, endPoint: NSPoint) {
points.first?.updatePosition(endPoint)
points.last?.updatePosition(anchor)
for point in points {
point.updatePosition()
}
for i in 0...CONST_ITERATIONS {
for constraint in constraints {
constraint.fixPoints()
}
}
}
}
Found it. The problem was in the fixPoints() method, as I suspected.
The line
let diff = (dist - len)/len
should be instead
let diff = (len - dist)/dist