Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get the distance to a CGPath for hit-testing?

I have an open CGPath/UIBezierPath for which I want to detect if the user touches it, i.e. whether a point is within a certain distance from the path. The path is open (i.e. a line/curve, not a shape). It can contain both straight & curved elements. How do I get the distance to the path to hit-test it?

like image 315
Patrick Pijnappel Avatar asked Feb 14 '23 22:02

Patrick Pijnappel


2 Answers

It seems neither CGPath/UIBezierPath has a function to do this. EDIT: As per @nielsbot's suggestion, you could write a custom implementation using CGPathApply(…). Computing the distance to the curved parts is not so trivial though.

However I have found a neat way to achieve my original goal, hit-testing the path: CGPathCreateCopyByStrokingPath(…).

- (BOOL)isPoint:(CGPoint)p withinDistance:(CGFloat)distance ofPath:(CGPathRef)path
{
    CGPathRef hitPath = CGPathCreateCopyByStrokingPath(path, NULL, distance*2, kCGLineCapRound, kCGLineJoinRound, 0);
    BOOL isWithinDistance = CGPathContainsPoint(hitPath, NULL, p, false);
    CGPathRelease(hitPath);
    return isWithinDistance;
}

For better performance, you can cache hitPath. Could also be used for closed paths by adding the original path to hitPath using CGPathAddPath(…)

like image 93
Patrick Pijnappel Avatar answered Feb 19 '23 06:02

Patrick Pijnappel


for swift 3.0:

final func isPoint(point: CGPoint, withinDistance distance: CGFloat, ofPath path: CGPath) -> Bool {

    if let hitPath = CGPath( __byStroking: path,
                             transform: nil,
                             lineWidth: distance,
                             lineCap: CGLineCap.round,
                             lineJoin: CGLineJoin.miter,
                             miterLimit: 0) {

        let isWithinDistance = hitPath.contains(point)
        return isWithinDistance
    }
    return false
}

use:

let inside = self.isPoint(point: location, withinDistance: 4, ofPath: myPath!)
if inside{...

of course, as Patrick noted, is better to cache hitPath, instead of creating it every time. In case You have UIBezierPath, simply:

 let inside = self.isPoint(point: location, withinDistance: 4, ofPath: myUIPath.cgPath!)
like image 22
ingconti Avatar answered Feb 19 '23 04:02

ingconti