Two Uibezierpaths Intersection as a Uibezierpath

Two UIBezierPaths intersection as a UIBezierPath

I wrote a UIBezierPath library that will let you cut a given closed path into sub shapes based on an intersecting path. It'll do essentially exactly what you're looking for: https://github.com/adamwulf/ClippingBezier

NSArray<UIBezierPath*>* componentShapes = [shapePath uniqueShapesCreatedFromSlicingWithUnclosedPath:scissorPath];

Alternatively, you can also just find the intersection points:

NSArray* intersections = [scissorPath findIntersectionsWithClosedPath:shapePath andBeginsInside:nil];

How to subtract the intersection of UIBezierPath A&B from A?

There is no direct way to get the differences of UIBezierPaths as a new path on iOS.

Testcase

private func path2() -> UIBezierPath {
return UIBezierPath(rect: CGRect(x: 100, y: 50, width: 200, height: 200))
}

private func path1() -> UIBezierPath {
return UIBezierPath(rect: CGRect(x: 50, y: 100, width: 200, height: 200))
}

Starting point: to simply show which path represents what: path 1 is yellow and path 2 is green:

starting point

Possiblity 1

You have probably already seen this possibility: In the link you mentioned in your question, there is also a clever solution if you only have to perform a fill operation.

The code was taken from this answer (from the link you posted) https://stackoverflow.com/a/8860341 - only converted to Swift:

func fillDifference(path2: UIBezierPath, path1: UIBezierPath) {
let clipPath = UIBezierPath.init(rect: .infinite)
clipPath.append(path2)
clipPath.usesEvenOddFillRule = true

UIGraphicsGetCurrentContext()?.saveGState()
clipPath.addClip()
path1.fill()
UIGraphicsGetCurrentContext()?.restoreGState()
}

So this fills a path, but does not return a UIBezierPath, which means that you cannot apply outlines, backgrounds, contour widths, etc., because the result is not a UIBezierPath.

It looks like this:

fill only

Possiblity 2

You can use a third-party library, e.g. one from an author named Adam Wulf here: https://github.com/adamwulf/ClippingBezier.

The library is written in Objective-C, but can be called from Swift.

In Swift, it would look like this:

override func draw(_ rect: CGRect) {
let result = self.path1().difference(with: self.path2())

for p in result ?? [] {
p.stroke()
}
}

If you want to use this library, you have to note a small hint: in Other Linker Flags in the project settings, as described by the readme, "-ObjC++ -lstdc++" must be added, otherwise it would be built without complaints, but would silently not load the frameworks and eventually crash, since the UIBEzierPath categories are not found.

The result looks like this:

real bezier path result

So this would actually give your desired result, but you would have to use a 3rd party library.

How to detect intersection of a UIBezierPath and erase it

If I understand correctly you need to loop through path at least. They are independent after all.

An UIBezierPaths has method contains which would tell you if a point is inside the path. If that is usable then what you need to do is simply

paths = paths.filter { !$0.contains(touchPoint) }

But since this is drawing app it is safe to assume you are using stroke and not fill which will most likely not work with contains the way you want it to.

You could do the intersection manually and it should have relatively good performance up until you have too many points. But when you have too many points I wager drawing performance will be more of your concern. Still the intersection may be a bit problematic with including some stroke line width; user would need to cross the center of your line to detect a hit.

There is a way though to convert stroked path to filled path. Doing that will then enable you to use contains method. The method is called replacePathWithStrokedPath but the thing is that it only exists on CGContext (At least I have never managed to find an equivalent on UIBezierPath). So procedure to do so includes creating a context. For instance something like this will work:

static func convertStrokePathToFillPath(_ path: UIBezierPath) throws -> UIBezierPath {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), true, 1.0)
guard let context = UIGraphicsGetCurrentContext() else { throw NSError(domain: "convertStrokePathToFillPath", code: 500, userInfo: ["dev_message":"Could not generate image context"]) }

context.addPath(path.cgPath)
context.setLineWidth(path.lineWidth)
// TODO: apply all possible settings from path to context
context.replacePathWithStrokedPath()

guard let returnedPath = context.path else { throw NSError(domain: "convertStrokePathToFillPath", code: 500, userInfo: ["dev_message":"Could not get path from context"]) }
UIGraphicsEndImageContext()

return UIBezierPath(cgPath: returnedPath)
}

So the result should look something like:

static func filterPaths(_ paths: [UIBezierPath], containingPoint point: CGPoint) -> [UIBezierPath] {
return paths.filter { !(try! convertStrokePathToFillPath($0).contains(point)) }
}

How to find intersection point (CGPoint) between QuadCurve and Line UIBezierPaths in swift?

If your line is always vertical, calculations are quite simple: x-coordinate is known, so you task is to find y-coordinate. Quadratic Bezier curve has parametric representation:

P(t) = P0*(1-t)^2 + 2*P1*(1-t)*t + P2*t^2 = 
t^2 * (P0 - 2*P1 + P2) + t * (-2*P0 + 2*P1) + P0

where P0, P1, P2 are starting, control and ending points.

So you have to solve quadratic equation

t^2 * (P0.X - 2*P1.X + P2.X) + t * (-2*P0.X + 2*P1.X)  + (P0.X - LineX) = 0

for unknown t, get root in range 0..1, and apply t value to the similar expression for Y-coordinate

Y = P0.Y*(1-t)^2 + 2*P1.Y*(1-t)*t + P2.Y*t^2

For arbitrary line make equation system for line parametric representation and curve, and solve that system

UIBezierPath appending overlapping isn't filled

You were on the right track with setting the usesEvenOddFillRule to false. In addition to that, you need to make sure your shapes are both drawn in the same direction (clockwise or counter-clockwise).

I reversed the order of your addLines when drawing the triangle to reverse it.

override func draw(_ rect: CGRect) {
let firstShape = UIBezierPath()

firstShape.move(to: CGPoint(x: 100, y: 100))
firstShape.addLine(to: CGPoint(x: 150, y: 170))
firstShape.addLine(to: CGPoint(x: 100, y: 150))
firstShape.close()

let secondShape = UIBezierPath(rect: CGRect(x: 125, y: 125, width: 75, height: 75))

let combined = UIBezierPath()
combined.append(firstShape)
combined.append(secondShape)
combined.usesEvenOddFillRule = false

UIColor.black.setFill()
combined.fill()
}

UIBezierPath intersect

The problem is in the checkLineIntersection method. With

if (ua > 0.0 && ua < 1.0 && ub > 0.0 && ub < 1.0) { return YES; }

you check only if the interior part of the lines segments intersect. But if the start or endpoint of the first line segment is equal to the start or endpoint of the second line segment, ua and ub will be 0.0 or 1.0.

The solution is to include one end of the interval in the condition:

if (ua > 0.0 && ua <= 1.0 && ub > 0.0 && ub <= 1.0) { return YES; }

This seemed to work as expected in my test program.

Some further remarks:

  • I think you should activate the shortcut

    if (denominator == 0.0f) return NO;

    again to avoid division by zero.

  • In touchesMoved, you could add the new line to the array after checking for intersections. Now the new line is inserted first, which means that it is checked against itself for intersections.

  • You have declared Line as subclass of UIView, but this is not really a view class. You could just declare Line as subclass of NSObject.


ADDED: The following method might work even better, because it avoids the division and therefore possible overflow problems with small denominators:

-(BOOL)checkLineIntersection:(CGPoint)p1 :(CGPoint)p2 :(CGPoint)p3 :(CGPoint)p4
{
CGFloat denominator = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
CGFloat ua = (p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x);
CGFloat ub = (p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x);
if (denominator < 0) {
ua = -ua; ub = -ub; denominator = -denominator;
}
return (ua > 0.0 && ua <= denominator && ub > 0.0 && ub <= denominator);
}

Masking a UIEffectView with intersecting UIBezierPath

You could use addArcWithCenter method to combine two arcs into desired shape.eg:

 #define DEGREES_TO_RADIANS(degrees)((M_PI * degrees)/180)

- (UIBezierPath*)createPath
{
UIBezierPath* path = [[UIBezierPath alloc]init];
[path addArcWithCenter:CGPointMake(25, 25) radius:25 startAngle:DEGREES_TO_RADIANS(30) endAngle:DEGREES_TO_RADIANS(60) clockwise:false];
[path addArcWithCenter:CGPointMake(40, 40) radius:10 startAngle:DEGREES_TO_RADIANS(330) endAngle:DEGREES_TO_RADIANS(120) clockwise:true];
[path closePath];
return path;
}

I wasn't able to find out the exact points but if you could adjust the constants you could create the perfect shape required.



Related Topics



Leave a reply



Submit