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:
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:
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:
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 addLine
s 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 ofUIView
, but this is not really a view class. You could just declareLine
as subclass ofNSObject
.
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
Alpha for Background Color Not Working in iOS
Swift 2 Unused Constant Warning
Periodically Call an API with Rxswift
Settitletextattributes Doesn't Work for Uitabbaritem When It Is Unselected in Swift
iOS + Swift, How to Redirect to Itunes Purchase Page
Swift 3 Imessage Extension Doesn't Open Url
How to Remove the Black Border of a Textfield
How to Call Presentviewcontroller in Uiview Class
How to Use Po Command in Console (Debug Area)
Xcode 8 How to Show Description of Function While Typing
Determine If Mkmapview Was Dragged/Moved in Swift 2.0
Get Index of Object from Array of Dictionary with Key Value
Getting Error Ambiguous Use of Tableview(_:Numberofrowsinsection:)
In iOS Avplayer, Addperiodictimeobserverforinterval Seems to Be Missing
How to Get Date from String in Swift 3