Detect if CGPoint within polygon - iphone

I have a set of CGPoints which make up a polygon shape, how can I detect if a single CGPoint is inside or outside of the polygon?
Say, the shape was a triangle and the CGPoint was moving hoizontally, how could I detect when it crossed the triangle line?
I can use CGRectContainsPoint when the shape is a regular 4-sided shape but I can't see how I would do it with an odd shape.

You can create a CG(Mutable)PathRef (or a UIBezierPath that wraps a CGPathRef) from your points and use the CGPathContainsPoint function to check if a point is inside that path. If you use UIBezierPath, you could also use the containsPoint: method.

For that you need to write one method that implements a point inside polygon algorithm.
This method will take an array with N points (the polygon) as an argument and one specific point. It should return true if the point is inside the polygon and false if not.
See this great answer on S.O.

Here is the implementation in Swift:
extension CGPoint {
func isInsidePolygon(vertices: [CGPoint]) -> Bool {
guard vertices.count > 0 else { return false }
var i = 0, j = vertices.count - 1, c = false, vi: CGPoint, vj: CGPoint
while true {
guard i < vertices.count else { break }
vi = vertices[i]
vj = vertices[j]
if (vi.y > y) != (vj.y > y) &&
x < (vj.x - vi.x) * (y - vi.y) / (vj.y - vi.y) + vi.x {
c = !c
}
j = i
i += 1
}
return c
}
}

Swift 3
A simpler way using Swift 3, is using UIBezierPath contains method.
When creating an instance of CAShapeLayer, make sure to set the accessibilityPath
shapeLayer.path = bazierPath.cgPath
shapeLayer.accessibilityPath = bazierPath
Checking if path contains touch location.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else { return }
for shape in layer.sublayers ?? [] where shape is CAShapeLayer {
guard let layer = shape as? CAShapeLayer,
let bazier = layer.accessibilityPath else { continue }
// Handle touch
print(bazier.contains(point))
}
}

Related

Increasing the size of a spriteNode using spriteKit in update function (Swift)

I'm having troubles with animations, physics and stuff like these.
Anyway, what I'm trying to do is creating a pulsing effect for a sphere where the pulse itself is able to collide with elements on the screen, so I created another spriteNode (child of the sphere) that I want to scale continuously, without depending on touches.
I read that in these cases is better not to use SKActions.
My question is: how do I scale or increase the size of a spriteNode via update function?
The code represents how I worked so far for movement for example.
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var entities = [GKEntity]()
var graphs = [String: GKGraph]()
var movingSquareLv1 = SKSpriteNode()
var pulsingSphereLv2 = SKSpriteNode()
var lv2SpherePulse = SKSpriteNode()
var xVeloicty: CGFloat = 100
override func sceneDidLoad() {
movingSquareLv1 = self.childNode(withName: "movingSquare") as!SKSpriteNode
movingSquareLv1.physicsBody = SKPhysicsBody(rectangleOf: movingSquareLv1.size)
movingSquareLv1.physicsBody ? .affectedByGravity = false
pulsingSphereLv2 = self.childNode(withName: "pulsingSphere") as!SKSpriteNode
pulsingSphereLv2.physicsBody ? .affectedByGravity = false
lv2SpherePulse = pulsingSphereLv2.childNode(withName: "lv2SpherePulse") as!SKSpriteNode
lv2SpherePulse.physicsBody ? .affectedByGravity = false
}
func touchDown(atPoint pos: CGPoint) {
}
func touchMoved(toPoint pos: CGPoint) {
}
func touchUp(atPoint pos: CGPoint) {
}
override func touchesBegan(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchDown(atPoint: t.location( in: self))
}
}
override func touchesMoved(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchMoved(toPoint: t.location( in: self))
}
}
override func touchesEnded(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchUp(atPoint: t.location( in: self))
}
}
override func touchesCancelled(_ touches: Set < UITouch > , with event: UIEvent ? ) {
for t in touches {
self.touchUp(atPoint: t.location( in: self))
}
}
override func update(_ currentTime: TimeInterval) {
rectMovement()
}
func rectMovement() {
if movingSquareLv1.frame.maxX >= self.size.width / 2 {
xVeloicty = -100
} else if movingSquareLv1.frame.minX <= -self.size.width / 2 {
xVeloicty = 100
}
let rate: CGFloat = 0.5
let relativeVelocity: CGVector = CGVector(dx: xVeloicty - movingSquareLv1.physicsBody!.velocity.dx, dy: 0)
movingSquareLv1.physicsBody!.velocity = CGVector(dx: movingSquareLv1.physicsBody!.velocity.dx + relativeVelocity.dx * rate, dy: 0)
}
}
This is the object composed of two sprites, the one I want to work on is the largest one.
Whoever told you to not use SKActions is wrong. This is exactly when you want to use an SKAction, and if you have multiple circles pulsing, you use the same action on all circles.
You really want to keep your update function as small as possible. Trust in appleā€™s optimizations because they have better access to the API than you do.
I don't know why whatever you read said that actions aren't appropriate, but if you want to change a node's size from update explicitly, then you can set its scaling.
myNode.setScale(2.5) // 2.5x as big as normal
myNode.setScale(0.5) // half size
myNode.xScale = 1.5
myNode.yScale = myNode.xScale * 2 // Set x and y separately for non-uniform scaling
See the documentation in the "Scaling and Rotation" section of SKNode:
https://developer.apple.com/documentation/spritekit/sknode

Color of pixel in ARSCNView

I am trying to get the color of a pixel at a CGPoint determined by the location of a touch. I have tried the following code but the color value is incorrect.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = event?.allTouches?.first {
let loc:CGPoint = touch.location(in: touch.view)
// in debugger, the image is correct
let image = sceneView.snapshot()
guard let color = image[Int(loc.x), Int(loc.y)] else{
return
}
print(color)
}
}
....
extension UIImage {
subscript (x: Int, y: Int) -> [UInt8]? {
if x < 0 || x > Int(size.width) || y < 0 || y > Int(size.height) {
return nil
}
let provider = self.cgImage!.dataProvider
let providerData = provider!.data
let data = CFDataGetBytePtr(providerData)
let numberOfComponents = 4
let pixelData = ((Int(size.width) * y) + x) * numberOfComponents
let r = data![pixelData]
let g = data![pixelData + 1]
let b = data![pixelData + 2]
return [r, g, b]
}
}
Running this and touching a spot on the screen that is a very large consistent bright orange yields a wide range of RGB values and looking at the color they actually produce yields a completely different color (dark blue in the case of the orange).
I'm guessing that maybe the coordinate systems are different and I'm actually getting a different point on the image possibly?
EDIT: Also I should mention that the part I'm tapping on is a 3D model that is not affected by lighting so the color should and appears to be consistent through run time.
Well it was easier than I thought. I first adjusted some things like making the touch be registered within my sceneview instead:
let loc:CGPoint = touch.location(in: sceneView)
I then reproportioned my cgpoint to adjust to the image view by doing the following:
let image = sceneView.snapshot()
let x = image.size.width / sceneView.frame.size.width
let y = image.size.height / sceneView.frame.size.height
guard let color = image[Int(x * loc.x), Int(y * loc.y)] else{
return
}
This allowed me to have consistent rgb values finally (not just numbers that changed on every touch even if I was on the same color). But the values were still off. For some reason my returned array was in reverse so I changed that with:
let b = data![pixelData]
let g = data![pixelData + 1]
let r = data![pixelData + 2]
I'm not sure why I had to do that last part, so any insight into that would be appreciated!

hitTest(_:options:) don't recognize nodes behind ARKit planes

I place an object on a wall, then try to recognize tap on it, but hit test returns 0 objects. When I change Z position of the object and place it a little bit closer to cam, it's recognized well, but this isn't a solution, because planes are always changing and it can cover the object in any moment. How can I made hitTest work correctly and recognize my nodes behind planes? Or, maybe, I use the wrong method?
fileprivate func addNode(atPoint point: CGPoint) {
let hits = sceneView.hitTest(point, types: .existingPlaneUsingExtent)
if hits.count > 0, let firstHit = hits.first, let originNode = originNode {
let node = originNode.clone()
sceneView.scene.rootNode.addChildNode(node)
node.position = SCNVector3Make(firstHit.worldTransform.columns.3.x, firstHit.worldTransform.columns.3.y, firstHit.worldTransform.columns.3.z)
let resize = simd_float4x4(SCNMatrix4MakeScale(0.2, 0.2, 0.2))
let rotation = simd_float4x4(SCNMatrix4MakeRotation(.pi / 2, -1, 0, 0))
let transform = simd_mul(firstHit.worldTransform, resize)
let finalTransform = simd_mul(transform, rotation)
node.simdTransform = finalTransform
addedNodes.insert(node)
}
}
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
print("Unable to identify touches on any plane. Ignoring interaction...")
return
}
let touchPoint = touch.location(in: sceneView)
let hits = sceneView.hitTest(touchPoint, options: [SCNHitTestOption.boundingBoxOnly: true])
let filtered = hits.filter({ addedNodes.contains($0.node) })
print("\(hits.count) vs \(filtered.count), \(hits.first?.node.name ?? "no name")")
if let node = filtered.first?.node {
node.removeFromParentNode()
addedNodes.remove(node)
return
}
addPictureToPlane(atPoint: touchPoint)
}
addedNodes - set with added objects. When I added translating transform with changing Z coordinate at least on 0.05 (close to the camera) detecting working good. At least before plane changing and moving ahead the node.
I believe what you need to do is change your SCNHitTestSearchModeparameter which allows you to set:
Possible values for the searchMode option used with hit-testing
methods.
static let searchMode: SCNHitTestOption
Whereby:
The value for this key is an NSNumber object containing the raw
integer value of an SCNHitTestSearchMode constant.
From the Apple Docs there are three possible options you can use here:
case all
The hit test should return all possible results, sorted from nearest
to farthest.
case any
The hit test should return only the first object found, regardless of
distance.
case closest
The hit test should return only the closes object found.
Based on your question therefore, you would likely need to to utilise the all case.
As such your hitTest function would probably need to look something like this (remembering that self.augmentedRealityView refers to an ARSCNView):
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Current Touch Location
guard let currentTouchLocation = touches.first?.location(in: self.augmentedRealityView) else { return }
//2. Perform An SCNHitTest Setting The SearchMode To 1 (All) Which Returns A List Of Results Sorted From Nearest To Farthest
if #available(iOS 11.0, *) {
let hitTestResults = self.augmentedRealityView.hitTest(currentTouchLocation, options: [SCNHitTestOption.searchMode: 1])
//3. Loop Through The Results & Get The Nodes
for index in 0..<hitTestResults.count{
let node = hitTestResults[index]
print(node)
}
}
}

How can I limit the range of motion on a custom UIControl Knob?

I'm following the tutorial here, which demonstrates how to implement a custom UIControl. However, I'd like to stop the custom control from spinning endlessly. I've been able to hinder the ability to spin the knob but have been unable to create a definite stopping point.
My Attempt
In the code below I'm using the authors angle variable which ranges from 0 to 360. The logic is a bit wonky, but I'm
override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
super.continueTrackingWithTouch(touch, withEvent: event)
if (angle <= 1) {
angle = 2
return false
} else if angle >= 356 {
angle = 355
return false
} else {
let lastPoint = touch.locationInView(self)
self.moveHandle(lastPoint)
self.sendActionsForControlEvents(UIControlEvents.ValueChanged)
return true
}
}
view code on github
note: I've tried all of the obvious operators and logic. I feel like this is the wrong approach entirely.
Result
Basically, the controls motion will stop at 1, but only if I'm moving slowly. If I quickly drag the knob, it'll spin right past the 1, allowing the control to spin endlessly.
Question
How can I properly limit the UIControls range of motion from 1 to 355?
I've put together a simple working project that you can download and test.
project files
Change continueTrackingWithTouch to:
// Part of UIControl, used to track user input
override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
let lastPoint = touch.locationInView(self)
self.moveHandle(lastPoint)
return super.continueTrackingWithTouch(touch, withEvent: event)
}
Change moveHandle to:
func moveHandle(lastPoint:CGPoint){
let threshholdAngle = 180
let centerPoint:CGPoint = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
let currentAngle:Double = AngleFromNorth(centerPoint, p2: lastPoint, flipped: false)
let angleInt = Int(floor(currentAngle))
let newAngle = Int(360 - angleInt)
if abs(newAngle - angle) > threshholdAngle {
return
}
//Store the new angle
angle = newAngle
sendActionsForControlEvents(UIControlEvents.ValueChanged)
//Update the textfield
//textField!.text = "\(angle)"
//Redraw
setNeedsDisplay()
}

An efficient way to find all SKNodes within a certain distance from a given point?

If I want to know which node or nodes cover certain point of the scene I can do either scene.nodeAtPoint(point) or scene.nodesAtPoint(point). Is there an efficient way to do something similar with a circle? That is to be able to find all nodes that have their positions within certain distance from a given point.
I need this to be able to handle user taps better. The scene is rather scarce (not much user-active sprites on it), so there is no need to demand the user to be very precise with tapping. I want to use some sort a tolerance around the coordinates of the tap and detect the node that is just close enough to tap location.
The best idea that I have so far is to do nodeAtPoint() several times: at the location of the tap, and then at various offsets from it (e.g. up, up-right, rigth, ..., up-left).
Distance
First of all we need an extension to find the distance between 2 CGPoint(s)
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
return CGFloat(hypotf(Float(point.x - self.x), Float(point.y - self.y)))
}
}
The closest child
Now we can add an extension to SKScene to find the child closest to a given point and within a maxDistance.
extension SKScene {
func closestChild(point: CGPoint, maxDistance: CGFloat) -> SKNode? {
return self
.children
.filter { $0.position.distance(point) <= maxDistance }
.minElement { $0.0.position.distance(point) < $0.1.position.distance(point) }
}
}
Time
The computational complexity of the extension above is O(n) where n is the number of direct children of the scene.
To determine if one or more nodes in the scene are within a fixed distance of a touch event, 1) compute the distance between the touch and each node and 2) compare if that distance is less than or equal to a constant. Here's an example of how to do that:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if let touch = touches.first {
let location = touch.locationInNode(self)
let nodes = nodesNearPoint(self, point:location, maxDistance: 30)
if nodes.count > 0 {
print("node is near touch point")
}
}
}
// Returns an array of all SKNode objects in the subtree that near the specified point.
// If no nodes are near the point, an empty array is returned.
func nodesNearPoint(container:SKNode, point:CGPoint, maxDistance:CGFloat) -> [SKNode] {
var array = [SKNode]()
for node in container.children {
// Only test sprite nodes (optional)
if node is SKSpriteNode {
let dx = point.x - node.position.x
let dy = point.y - node.position.y
let distance = sqrt(dx*dx + dy*dy)
if (distance <= maxDistance) {
array.append(node)
}
}
}
return array
}
Thanks to all the answerers, this is what I came up with in the end:
extension SKNode {
private func squareDistanceTo(point: CGPoint) -> CGFloat {
let dx = point.x - position.x
let dy = point.y - position.y
return (dx * dx + dy * dy)
}
func nearestNode(point: CGPoint) -> SKNode {
return children.minElement() {
$0.squareDistanceTo(point) < $1.squareDistanceTo(point)
} ?? self
}
func nodesInVicinity(point: CGPoint, tolerance: CGFloat) -> [SKNode] {
let squareTolerance = tolerance * tolerance
return children.filter() {
$0.squareDistanceTo(point) <= squareTolerance
}
}
}
Above extends SKNode with functionality:
nearestNode to find a child node nearest to the specific point, or give back the parent if there's no child nodes.
nodesInVicinity to get an array of nodes that have their positions within certain distance from a given point.
After some test I came to the conclusion that even nodeAtPoint has O(n) complexity, hence no hope for quick wins.
One way I've handled this before is to create a circle (using a png of a circle), setting the size to the radius I'm interested in and then seeing what nodes fall in that radius.
This is the basic idea assuming otherNodes is an array of the nodes I'm interested in checking:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let radius = 500
let circle = SKSpriteNode(texture: SKTexture(imageNamed: "circle.png"), color: UIColor.clearColor(), size: CGSize(width: radius, height: radius))
circle.position = touches.first!.locationInNode(self)
for node in otherNodes {
if circle.containsPoint(node.position) {
//The center of this node falls in the radius
}
}
}
EDIT:
After looking at an old solution where I used this, I noticed that containsPoint is actually looking to see if the point is within the node's bounding box, so having a png of a circle is actually irrelevant and consequently, matches might fall outside of the radius of the circle, but still within its bounding box.