From 679f7356fb19d190cdc93986848fabefd43a2c68 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Wed, 28 Nov 2018 22:06:02 -0800 Subject: [PATCH 01/18] moved coded that is only needed for exceptional conditions into a conditional. --- BezierKit/Library/AugmentedGraph.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift index ffa2e59b..d696c7ac 100644 --- a/BezierKit/Library/AugmentedGraph.swift +++ b/BezierKit/Library/AugmentedGraph.swift @@ -389,10 +389,9 @@ internal class AugmentedGraph { break } - unvisitedCrossings = unvisitedCrossings.filter { $0 !== v } - if !v.isCrossing { print("consistency error detected -- bailing out. Needs debugging.") + unvisitedCrossings = unvisitedCrossings.filter { $0 !== v } v = v.intersectionInfo.neighbor! // jump back to avoid infinite loop isOnFirstCurve = !isOnFirstCurve } From 2d14adf5bd30eb24fd4eab84b528deab3bcb8ddd Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Mon, 3 Dec 2018 09:35:40 -0800 Subject: [PATCH 02/18] changed case of TODO so it would show up. --- BezierKit/Library/BezierCurve.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BezierKit/Library/BezierCurve.swift b/BezierKit/Library/BezierCurve.swift index 0f2ec335..c80756e5 100644 --- a/BezierKit/Library/BezierCurve.swift +++ b/BezierKit/Library/BezierCurve.swift @@ -194,7 +194,7 @@ extension BezierCurve { */ public func reduce() -> [Subcurve] { - // todo: handle degenerate case of Cubic with all zero points better! + // TODO: handle degenerate case of Cubic with all zero points better! let step: CGFloat = 0.01 var extrema: [CGFloat] = self.extrema().values From 5dc6b376f5cd8686d7b3bc5fcbf8aaee8978154b Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Mon, 3 Dec 2018 10:32:31 -0800 Subject: [PATCH 03/18] new test for edge case. --- BezierKit/BezierKitTests/PathTests.swift | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/BezierKit/BezierKitTests/PathTests.swift b/BezierKit/BezierKitTests/PathTests.swift index ec9aa4df..e795b493 100644 --- a/BezierKit/BezierKitTests/PathTests.swift +++ b/BezierKit/BezierKitTests/PathTests.swift @@ -524,4 +524,44 @@ class PathTests: XCTestCase { XCTAssertTrue(componentsEqualAsideFromElementOrdering(crossingsRemoved.subpaths[0], contour.subpaths[0])) } + func testCrossingsRemovedEqualVertices() { + + // this tests an edge case of crossingsRemoved() when vertices of the path are exactly equal + // the path does a complete loop in the middle + + let cgPath = CGMutablePath() + + cgPath.move(to: CGPoint.zero) + cgPath.addLine(to: CGPoint(x: 2.0, y: 0.0)) + + // loop in a complete circle back to 2, 0 + cgPath.addArc(tangent1End: CGPoint(x: 3.0, y: 0.0), tangent2End: CGPoint(x: 3.0, y: 1.0), radius: 1) + cgPath.addArc(tangent1End: CGPoint(x: 3.0, y: 2.0), tangent2End: CGPoint(x: 2.0, y: 2.0), radius: 1) + cgPath.addArc(tangent1End: CGPoint(x: 1.0, y: 2.0), tangent2End: CGPoint(x: 1.0, y: 1.0), radius: 1) + cgPath.addArc(tangent1End: CGPoint(x: 1.0, y: 0.0), tangent2End: CGPoint(x: 2.0, y: 0.0), radius: 1) + + // proceed around to close the shape (grazing the loop at (2,2) + cgPath.addLine(to: CGPoint(x: 4.0, y: 0.0)) + cgPath.addLine(to: CGPoint(x: 4.0, y: 2.0)) + cgPath.addLine(to: CGPoint(x: 2.0, y: 2.0)) + cgPath.addLine(to: CGPoint(x: 0.0, y: 2.0)) + cgPath.closeSubpath() + + + let path = Path(cgPath: cgPath) + + // Quartz 'addArc' function creates some terrible near-zero length line segments + var curves2 = path.subpaths[0].curves.map { + return BezierKit.createCurve(from: $0.points.map { point in + let rounded = CGPoint(x: round(point.x), y: round(point.y)) + return distance(point, rounded) < 1.0e-3 ? rounded : point + })! + }.filter { $0.length() > 0.0 } + + let path2 = Path(subpaths: [PathComponent(curves: curves2)]) + + let result = path2.crossingsRemoved() + + } + } From 0bf1200ca8c8b54c74ddf54f46295af07f699632 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Mon, 3 Dec 2018 13:04:45 -0800 Subject: [PATCH 04/18] fixed an edge case of line / curve intersection where the discriminant is almost exactly equal to zero. --- .../BezierKitTests/CubicBezierCurveTests.swift | 13 +++++++++++++ BezierKit/Library/Utils.swift | 16 ++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/BezierKit/BezierKitTests/CubicBezierCurveTests.swift b/BezierKit/BezierKitTests/CubicBezierCurveTests.swift index 3180e9b4..a6f0ed19 100644 --- a/BezierKit/BezierKitTests/CubicBezierCurveTests.swift +++ b/BezierKit/BezierKitTests/CubicBezierCurveTests.swift @@ -360,6 +360,19 @@ class CubicBezierCurveTests: XCTestCase { XCTAssertEqual(i[2].t1, 1.0, accuracy: epsilon) } + func testCubicIntersectsLineEdgeCase() { + // this example caused issues in practice because it has a discriminant that is nearly equal to zero (but not exactly) + let c = CubicBezierCurve(p0: CGPoint(x: 3, y: 1), + p1: CGPoint(x: 3, y: 1.5522847498307932), + p2: CGPoint(x: 2.5522847498307932, y: 2), + p3: CGPoint(x: 2, y: 2)) + let l = LineSegment(p0: CGPoint(x: 2, y: 2), p1: CGPoint(x: 0, y: 2)) + let i = c.intersects(curve: l) + XCTAssertEqual(i.count, 1) + XCTAssertEqual(i[0].t1, 1) + XCTAssertEqual(i[0].t2, 0) + } + // MARK: - func testEquatable() { diff --git a/BezierKit/Library/Utils.swift b/BezierKit/Library/Utils.swift index 8960944d..e4e31288 100644 --- a/BezierKit/Library/Utils.swift +++ b/BezierKit/Library/Utils.swift @@ -235,7 +235,7 @@ internal class Utils { let q = (2*a*a*a - 9*a*b + 27*c)/27 let q2 = q/2 let discriminant = q2*q2 + p3*p3*p3 - if discriminant < 0 { + if discriminant < -epsilon { let mp3 = -p/3 let mp33 = mp3*mp3*mp3 let r = sqrt( mp33 ) @@ -249,18 +249,18 @@ internal class Utils { let x3 = t1 * cos((phi+2*tau)/3) - a/3 return [x1, x2, x3].compactMap(clamp) } - else if discriminant == 0 { - let u1 = q2 < 0 ? crt(-q2) : -crt(q2) - let x1 = 2*u1-a/3 - let x2 = -u1 - a/3 - return [x1,x2].compactMap(clamp) - } - else { + else if discriminant > epsilon { let sd = sqrt(discriminant) let u1 = crt(-q2+sd) let v1 = crt(q2+sd) return [u1-v1-a/3].compactMap(clamp) } + else { + let u1 = q2 < 0 ? crt(-q2) : -crt(q2) + let x1 = 2*u1-a/3 + let x2 = -u1 - a/3 + return [x1,x2].compactMap(clamp) + } } else { fatalError("unsupported") From 3a491c790855f7cdb294059cc11b450de1da605e Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Mon, 3 Dec 2018 13:33:10 -0800 Subject: [PATCH 05/18] fix logic error in the fix of logic error. --- BezierKit/Library/PathComponent.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BezierKit/Library/PathComponent.swift b/BezierKit/Library/PathComponent.swift index b5ec059f..e7bd19cf 100644 --- a/BezierKit/Library/PathComponent.swift +++ b/BezierKit/Library/PathComponent.swift @@ -122,12 +122,13 @@ public final class PathComponent: NSObject, NSCoding { else if i1 < i2 { // we are intersecting two distinct path elements elementIntersections = c1.intersects(curve: c2, threshold: threshold).filter { - if i1 == Utils.mod(i2+1, self.curves.count) && $0.t1 == 0.0 { - return false // exclude intersections of i and i-1 at t=0 - } if i1 == Utils.mod(i2-1, self.curves.count) && $0.t1 == 1.0 { return false // exclude intersections of i and i+1 at t=1 } + if $0.t1 == 0.0 || $0.t2 == 0.0 { + // use the intersection with the prior path element at t=1.0 instead + return false + } return true } } From a0c12a26b01b93098616c926fb27018a0c8501a0 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Mon, 3 Dec 2018 14:08:29 -0800 Subject: [PATCH 06/18] finsihed writing unit test. Expect this test to fail until fix is committed. --- BezierKit/BezierKitTests/PathTests.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/BezierKit/BezierKitTests/PathTests.swift b/BezierKit/BezierKitTests/PathTests.swift index e795b493..065ed1de 100644 --- a/BezierKit/BezierKitTests/PathTests.swift +++ b/BezierKit/BezierKitTests/PathTests.swift @@ -524,7 +524,7 @@ class PathTests: XCTestCase { XCTAssertTrue(componentsEqualAsideFromElementOrdering(crossingsRemoved.subpaths[0], contour.subpaths[0])) } - func testCrossingsRemovedEqualVertices() { + func testCrossingsRemovedEdgeCaseInnerLoop() { // this tests an edge case of crossingsRemoved() when vertices of the path are exactly equal // the path does a complete loop in the middle @@ -546,22 +546,25 @@ class PathTests: XCTestCase { cgPath.addLine(to: CGPoint(x: 2.0, y: 2.0)) cgPath.addLine(to: CGPoint(x: 0.0, y: 2.0)) cgPath.closeSubpath() - let path = Path(cgPath: cgPath) // Quartz 'addArc' function creates some terrible near-zero length line segments - var curves2 = path.subpaths[0].curves.map { + // let's eliminate those + let curves2 = path.subpaths[0].curves.map { return BezierKit.createCurve(from: $0.points.map { point in let rounded = CGPoint(x: round(point.x), y: round(point.y)) return distance(point, rounded) < 1.0e-3 ? rounded : point })! }.filter { $0.length() > 0.0 } + let cleanPath = Path(subpaths: [PathComponent(curves: curves2)]) - let path2 = Path(subpaths: [PathComponent(curves: curves2)]) - - let result = path2.crossingsRemoved() - + let result = cleanPath.crossingsRemoved(threshold: 1.0e-4) + + // check that the inner loop was eliminated by checking the winding count in the middle + XCTAssertEqual(result.windingCount(CGPoint(x: 0.5, y: 1)), 1) + XCTAssertEqual(result.windingCount(CGPoint(x: 2.0, y: 1)), 1) // if the inner loop wasn't eliminated we'd have a winding count of 2 here + XCTAssertEqual(result.windingCount(CGPoint(x: 3.5, y: 1)), 1) } } From 026a0d8c5e0a21ebd779c657146e326ffad3ac0e Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Mon, 3 Dec 2018 14:46:37 -0800 Subject: [PATCH 07/18] no longer use derivative when determing crossings. The main reason is that in certain edge cases the derivative might fall exactly on the edge and its only the 2nd derivative that causes the curve to bend through the edge. --- BezierKit/Library/AugmentedGraph.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift index d696c7ac..9825c922 100644 --- a/BezierKit/Library/AugmentedGraph.swift +++ b/BezierKit/Library/AugmentedGraph.swift @@ -145,11 +145,11 @@ internal class PathLinkedListRepresentation { let previous = v.emitPrevious() let next = v.emitNext() - let n1 = v.intersectionInfo.neighbor!.emitPrevious().derivative(0) - let n2 = v.intersectionInfo.neighbor!.emitNext().derivative(0) + let n1 = v.intersectionInfo.neighbor!.emitPrevious().compute(0.5) - v.location + let n2 = v.intersectionInfo.neighbor!.emitNext().compute(0.5) - v.location - let v1 = previous.derivative(0) - let v2 = next.derivative(0) + let v1 = previous.compute(0.5) - v.location + let v2 = next.compute(0.5) - v.location let side1 = between(v1, n1, n2) let side2 = between(v2, n1, n2) From f4a3eb4c590ebc8faca0473a2e1cc1b030289ad8 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Mon, 3 Dec 2018 15:57:21 -0800 Subject: [PATCH 08/18] use a smaller delta when computing approximation of derivative. --- BezierKit/Library/AugmentedGraph.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift index 9825c922..8fab639f 100644 --- a/BezierKit/Library/AugmentedGraph.swift +++ b/BezierKit/Library/AugmentedGraph.swift @@ -145,11 +145,15 @@ internal class PathLinkedListRepresentation { let previous = v.emitPrevious() let next = v.emitNext() - let n1 = v.intersectionInfo.neighbor!.emitPrevious().compute(0.5) - v.location - let n2 = v.intersectionInfo.neighbor!.emitNext().compute(0.5) - v.location + // we used to use the derivative here but in important cases derivatives can be exactly tangent + // at intersections! + let smallNumber: CGFloat = 0.01 - let v1 = previous.compute(0.5) - v.location - let v2 = next.compute(0.5) - v.location + let n1 = v.intersectionInfo.neighbor!.emitPrevious().compute(smallNumber) - v.location + let n2 = v.intersectionInfo.neighbor!.emitNext().compute(smallNumber) - v.location + + let v1 = previous.compute(smallNumber) - v.location + let v2 = next.compute(smallNumber) - v.location let side1 = between(v1, n1, n2) let side2 = between(v2, n1, n2) From c3c79177507a27580df32f64af92994466e3eed8 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Tue, 4 Dec 2018 14:35:19 -0800 Subject: [PATCH 09/18] fixed note about logic. --- BezierKit/Library/AugmentedGraph.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift index 8fab639f..14646419 100644 --- a/BezierKit/Library/AugmentedGraph.swift +++ b/BezierKit/Library/AugmentedGraph.swift @@ -241,6 +241,7 @@ internal class PathLinkedListRepresentation { } } if !hasCrossing { + // TODO: fix logic here, we should do this after we are done and use .isCrossing nonCrossingComponents.append(self.path.subpaths[i]) } } From 7adc5779050139387922a0a28bb7bcdb54e428b8 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Tue, 4 Dec 2018 15:13:49 -0800 Subject: [PATCH 10/18] some refactoring and removal of obsolete comments. --- BezierKit/Library/AugmentedGraph.swift | 61 ++++++++++++-------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift index 14646419..777f2791 100644 --- a/BezierKit/Library/AugmentedGraph.swift +++ b/BezierKit/Library/AugmentedGraph.swift @@ -131,11 +131,27 @@ internal class PathLinkedListRepresentation { self.lists = p.subpaths.map { self.createListFor(component: $0) } } - fileprivate func markEntryExit(_ path: Path, _ nonCrossingComponents: inout [PathComponent], useRelativeWinding: Bool = false) { + fileprivate func nonCrossingComponents() -> [PathComponent] { + // returns the components of this path that do not cross the path passed as the argument to markEntryExit(_:) + var result: [PathComponent] = [] + for i in 0.. Vertex { @@ -316,9 +320,9 @@ internal class AugmentedGraph { } // mark each intersection as either entry or exit let useRelativeWinding = (list1 === list2) - list1.markEntryExit(path2, &nonCrossingComponents1, useRelativeWinding: useRelativeWinding) + list1.markEntryExit(path2, useRelativeWinding: useRelativeWinding) if useRelativeWinding == false { - list2.markEntryExit(path1, &nonCrossingComponents2, useRelativeWinding: useRelativeWinding) + list2.markEntryExit(path1, useRelativeWinding: useRelativeWinding) } } @@ -336,14 +340,18 @@ internal class AugmentedGraph { } internal func booleanOperation(_ operation: BooleanPathOperation) -> Path { - // handle components that have no crossings + + // special cases for components which do not cross + let nonCrossingComponents1: [PathComponent] = self.list1.nonCrossingComponents() + let nonCrossingComponents2: [PathComponent] = self.list2.nonCrossingComponents() + func anyPointOnComponent(_ c: PathComponent) -> CGPoint { return c.curves[0].startingPoint } var pathComponents: [PathComponent] = [] switch operation { case .removeCrossings: - pathComponents += nonCrossingComponents1 // TODO: hmm7 + pathComponents += nonCrossingComponents1 case .union: pathComponents += nonCrossingComponents1.filter { path2.contains(anyPointOnComponent($0)) == false } pathComponents += nonCrossingComponents2.filter { path1.contains(anyPointOnComponent($0)) == false } @@ -354,7 +362,8 @@ internal class AugmentedGraph { pathComponents += nonCrossingComponents1.filter { path2.contains(anyPointOnComponent($0)) == true } pathComponents += nonCrossingComponents2.filter { path1.contains(anyPointOnComponent($0)) == true } } - // handle components that have crossings + + // handle components that have crossings (the main algorithm) var unvisitedCrossings: [Vertex] = [] list1.forEachVertex { if $0.isCrossing && shouldMoveForwards(fromVertex: $0, forOperation: operation, isOnFirstCurve: true) { @@ -371,10 +380,6 @@ internal class AugmentedGraph { var isOnFirstCurve = true var v = start repeat { - // if isOnFirstCurve && unvisitedCrossings.contains(v) == false { - // print("already visited this crossing! bailing out to avoid infinite loop! Needs debugging.") - // break - // } let movingForwards = shouldMoveForwards(fromVertex: v, forOperation: operation, isOnFirstCurve: isOnFirstCurve) unvisitedCrossings = unvisitedCrossings.filter { $0 !== v } repeat { @@ -393,17 +398,9 @@ internal class AugmentedGraph { } break } - - if !v.isCrossing { - print("consistency error detected -- bailing out. Needs debugging.") - unvisitedCrossings = unvisitedCrossings.filter { $0 !== v } - v = v.intersectionInfo.neighbor! // jump back to avoid infinite loop - isOnFirstCurve = !isOnFirstCurve - } } while v !== start pathComponents.append(PathComponent(curves: curves)) } - // TODO: non-deterministic behavior from usage of Set when choosing starting vertex return Path(subpaths: pathComponents) } } From 46021934f9d0ed166a2e5154045dee37e83de118 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Tue, 4 Dec 2018 15:31:41 -0800 Subject: [PATCH 11/18] added more commentary about an edge case in the demo. --- BezierKit/MacDemos/Demos.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/BezierKit/MacDemos/Demos.swift b/BezierKit/MacDemos/Demos.swift index 3567016e..7f7581c6 100644 --- a/BezierKit/MacDemos/Demos.swift +++ b/BezierKit/MacDemos/Demos.swift @@ -432,11 +432,11 @@ class Demos { if let mouse = demoState.lastInputLocation { - //let m2 = CGPoint(x: -21.19140625, y: 131.38671875) - //let me2 = CGPoint(x: 24.34375, y: 110.703125) - - //let me3 = CGPoint(x: -7.78515625, y: 161.7265625) // seems to cause an issue because intersections[5].t = 0.99999422905833845, clamping the t values when they are appropximately 1 or 0 seems to work (but fix not applied) - // let me4 = CGPoint(x: 22.41796875, y: 168.48046875) // caused an infinite loop or graphical glitches +// let m2 = CGPoint(x: -21.19140625, y: 131.38671875) +// let me2 = CGPoint(x: 24.34375, y: 110.703125) +// +// let me3 = CGPoint(x: -7.78515625, y: 161.7265625) // seems to cause an issue because intersections[5].t = 0.99999422905833845, clamping the t values when they are appropximately 1 or 0 seems to work (but fix not applied) +// let me4 = CGPoint(x: 22.41796875, y: 168.48046875) // caused an infinite loop or graphical glitches (increasing precision of boolean operation appears to resolve) var translation = CGAffineTransform.init(translationX: mouse.x, y: mouse.y) let cgPath2: CGPath = CTFontCreatePathForGlyph(font, glyph2, &translation)! From fd9a8d11d3aa6e1c1fe196c6cc07d9feb7e04dad Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Tue, 4 Dec 2018 16:20:41 -0800 Subject: [PATCH 12/18] instead of logging and error and bailing out when boolean ops go wrong, throw the error. --- BezierKit/Library/AugmentedGraph.swift | 10 ++++------ BezierKit/Library/BezierCurve.swift | 1 + BezierKit/Library/Path.swift | 25 ++++++++++++++----------- BezierKit/MacDemos/Demos.swift | 2 +- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift index 777f2791..b5cc086a 100644 --- a/BezierKit/Library/AugmentedGraph.swift +++ b/BezierKit/Library/AugmentedGraph.swift @@ -7,6 +7,7 @@ // import CoreGraphics +import Foundation public func signedAngle(_ a: CGPoint, _ b: CGPoint) -> CGFloat { return atan2(CGPoint.cross(a, b), a.dot(b)) @@ -339,7 +340,7 @@ internal class AugmentedGraph { } } - internal func booleanOperation(_ operation: BooleanPathOperation) -> Path { + internal func booleanOperation(_ operation: BooleanPathOperation) throws -> Path { // special cases for components which do not cross let nonCrossingComponents1: [PathComponent] = self.list1.nonCrossingComponents() @@ -392,11 +393,8 @@ internal class AugmentedGraph { isOnFirstCurve = !isOnFirstCurve if isOnFirstCurve && unvisitedCrossings.contains(v) == false && v !== start { - print("already visited this crossing! bailing out to avoid infinite loop! Needs debugging.") - if let last = curves.last?.endingPoint, let first = curves.first?.startingPoint, last != first { - curves.append(LineSegment(p0: last, p1: first)) // close the component before we bail out - } - break + let userInfo = [NSLocalizedDescriptionKey: "Boolean operation failed, try more accurate threshold?"] + throw NSError(domain: BezierKit.errorDomain, code: -1, userInfo: userInfo) } } while v !== start pathComponents.append(PathComponent(curves: curves)) diff --git a/BezierKit/Library/BezierCurve.swift b/BezierKit/Library/BezierCurve.swift index c80756e5..7cf027be 100644 --- a/BezierKit/Library/BezierCurve.swift +++ b/BezierKit/Library/BezierCurve.swift @@ -578,6 +578,7 @@ extension BezierCurve { } public let defaultIntersectionThreshold = CGFloat(0.5) +public let errorDomain = "BezierKit" // MARK: factory diff --git a/BezierKit/Library/Path.swift b/BezierKit/Library/Path.swift index 79644d66..3512fcb2 100644 --- a/BezierKit/Library/Path.swift +++ b/BezierKit/Library/Path.swift @@ -22,6 +22,9 @@ internal func windingCountImpliesContainment(_ count: Int, using rule: PathFillR } } +@objc(BezierKitPathError) public class PathError: NSError { +} + @objc(BezierKitPath) public class Path: NSObject, NSCoding { private class PathApplierFunctionContext { @@ -201,33 +204,33 @@ internal func windingCountImpliesContainment(_ count: Int, using rule: PathFillR return windingCountImpliesContainment(count, using: rule) } - private func performBooleanOperation(_ operation: BooleanPathOperation, withPath other: Path, threshold: CGFloat) -> Path { + private func performBooleanOperation(_ operation: BooleanPathOperation, withPath other: Path, threshold: CGFloat) throws -> Path { let intersections = self.intersects(path: other, threshold: threshold) let augmentedGraph = AugmentedGraph(path1: self, path2: other, intersections: intersections) - return augmentedGraph.booleanOperation(operation) + return try augmentedGraph.booleanOperation(operation) } - @objc(subtractingPath:threshold:) public func subtracting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path { - return self.performBooleanOperation(.difference, withPath: other.reversed(), threshold: threshold) + @objc(subtractingPath:threshold:error:) public func subtracting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { + return try self.performBooleanOperation(.difference, withPath: other.reversed(), threshold: threshold) } - @objc(unionedWithPath:threshold:) public func `union`(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path { + @objc(unionedWithPath:threshold:error:) public func `union`(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { guard self.isEmpty == false else { return other } guard other.isEmpty == false else { return self } - return self.performBooleanOperation(.union, withPath: other, threshold: threshold) + return try self.performBooleanOperation(.union, withPath: other, threshold: threshold) } - @objc(intersectedWithPath:threshold:) public func intersecting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path { - return self.performBooleanOperation(.intersection, withPath: other, threshold: threshold) + @objc(intersectedWithPath:threshold:error:) public func intersecting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { + return try self.performBooleanOperation(.intersection, withPath: other, threshold: threshold) } - @objc(crossingsRemovedWithThreshold:) public func crossingsRemoved(threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path { + @objc(crossingsRemovedWithThreshold:error:) public func crossingsRemoved(threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { // assert(self.subpaths.count <= 1, "todo: support multi-component paths") - return self.subpaths.reduce(Path(), { result, component in + return try self.subpaths.reduce(Path(), { result, component in // TODO: this won't work properly if components intersect let intersections = component.intersects(threshold: threshold).map {(i: PathComponentIntersection) -> PathIntersection in return PathIntersection(indexedPathLocation1: IndexedPathLocation(componentIndex: 0, elementIndex: i.indexedComponentLocation1.elementIndex, t: i.indexedComponentLocation1.t), @@ -238,7 +241,7 @@ internal func windingCountImpliesContainment(_ count: Int, using rule: PathFillR } let singleComponentPath = Path(subpaths: [component]) let augmentedGraph = AugmentedGraph(path1: singleComponentPath, path2: singleComponentPath, intersections: intersections) - let path = augmentedGraph.booleanOperation(.removeCrossings) + let path = try augmentedGraph.booleanOperation(.removeCrossings) return Path(subpaths: result.subpaths + path.subpaths) }) } diff --git a/BezierKit/MacDemos/Demos.swift b/BezierKit/MacDemos/Demos.swift index 7f7581c6..5c934016 100644 --- a/BezierKit/MacDemos/Demos.swift +++ b/BezierKit/MacDemos/Demos.swift @@ -464,7 +464,7 @@ class Demos { // v = v.next // } while v !== first - let subtracted = path1.intersecting(path2) + let subtracted = (try? path1.intersecting(path2)) ?? path1 Draw.drawPath(context, subtracted) } }) From 165a975adbb541fa866ffaf432f0fa667b1d713a Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Tue, 4 Dec 2018 16:46:29 -0800 Subject: [PATCH 13/18] marked boolean operations with "try!" --- BezierKit/BezierKitTests/PathTests.swift | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/BezierKit/BezierKitTests/PathTests.swift b/BezierKit/BezierKitTests/PathTests.swift index 065ed1de..8b1345db 100644 --- a/BezierKit/BezierKitTests/PathTests.swift +++ b/BezierKit/BezierKitTests/PathTests.swift @@ -342,7 +342,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let subtracted = square1.subtracting(square2) + let subtracted = try! square1.subtracting(square2) XCTAssertEqual(subtracted.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(subtracted.subpaths[0], expectedResult.subpaths[0]) @@ -364,7 +364,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let unioned = square1.union(square2) + let unioned = try! square1.union(square2) XCTAssertEqual(unioned.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(unioned.subpaths[0], expectedResult.subpaths[0]) @@ -382,7 +382,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let intersected = square1.intersecting(square2) + let intersected = try! square1.intersecting(square2) XCTAssertEqual(intersected.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(intersected.subpaths[0], expectedResult.subpaths[0]) @@ -394,7 +394,7 @@ class PathTests: XCTestCase { // the order of the hole is reversed so that it is not contained in the shape when using .winding fill rule let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 3, height: 3), transform: nil)) let hole = Path(cgPath: CGPath(ellipseIn: CGRect(x: 1, y: 1, width: 1, height: 1), transform: nil)) - let donut = circle.subtracting(hole) + let donut = try! circle.subtracting(hole) XCTAssertTrue(donut.contains(CGPoint(x: 0.5, y: 0.5), using: .winding)) // inside the donut (but not the hole) XCTAssertFalse(donut.contains(CGPoint(x: 1.5, y: 1.5), using: .winding)) // center of donut hole } @@ -403,7 +403,7 @@ class PathTests: XCTestCase { // this is a specific test of `subtracting` to ensure that if a path component is entirely contained in the subtracting path that it gets removed let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: -1, y: -1, width: 2, height: 2), transform: nil)) let biggerCircle = Path(cgPath: CGPath(ellipseIn: CGRect(x: -2, y: -2, width: 4, height: 4), transform: nil)) - XCTAssertEqual(circle.subtracting(biggerCircle).subpaths.count, 0) + XCTAssertEqual(try! circle.subtracting(biggerCircle).subpaths.count, 0) } func testSubtractingEdgeCase1() { @@ -415,7 +415,7 @@ class PathTests: XCTestCase { let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 4, height: 4), transform: nil)) // the circle intersects the rect at (0,2) and (3, 0.26792) ... the last number being exactly 2 - sqrt(3) - let difference = rectangle.subtracting(circle) + let difference = try! rectangle.subtracting(circle) XCTAssertEqual(difference.subpaths.count, 1) XCTAssertFalse(difference.contains(CGPoint(x: 2.0, y: 2.0))) } @@ -434,7 +434,7 @@ class PathTests: XCTestCase { square2CGPath.closeSubpath() let square2 = Path(cgPath: square2CGPath) - let result = square1.subtracting(square2) + let result = try! square1.subtracting(square2) let expectedResultCGPath = CGMutablePath() expectedResultCGPath.move(to: CGPoint.zero) @@ -474,7 +474,7 @@ class PathTests: XCTestCase { XCTAssertTrue(path.contains(CGPoint(x: 1.5, y: 1.25), using: .winding)) XCTAssertFalse(path.contains(CGPoint(x: 1.5, y: 1.25), using: .evenOdd)) - let result = path.crossingsRemoved() + let result = try! path.crossingsRemoved() XCTAssertEqual(result.subpaths.count, 1) XCTAssertTrue(componentsEqualAsideFromElementOrdering(result.subpaths[0], expectedResult.subpaths[0])) @@ -483,7 +483,7 @@ class PathTests: XCTestCase { cgPathAlt.addLines(between: Array(points[3.. 0.0 } let cleanPath = Path(subpaths: [PathComponent(curves: curves2)]) - let result = cleanPath.crossingsRemoved(threshold: 1.0e-4) + let result = try! cleanPath.crossingsRemoved(threshold: 1.0e-4) // check that the inner loop was eliminated by checking the winding count in the middle XCTAssertEqual(result.windingCount(CGPoint(x: 0.5, y: 1)), 1) From da309247eea64141d626ff59aa7d2676cadcb640 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Fri, 7 Dec 2018 16:48:12 -0800 Subject: [PATCH 14/18] disabled some code and added a TODO: comment. --- BezierKit/Library/PathComponent.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BezierKit/Library/PathComponent.swift b/BezierKit/Library/PathComponent.swift index e7bd19cf..9510d336 100644 --- a/BezierKit/Library/PathComponent.swift +++ b/BezierKit/Library/PathComponent.swift @@ -113,20 +113,21 @@ public final class PathComponent: NSObject, NSCoding { let c1 = o1 as! BezierCurve let c2 = o2 as! BezierCurve var elementIntersections: [Intersection] = [] - if i1 == i2 { + // TODO: fix behavior for `crossingsRemoved` when there are self intersections and re-enable + /*if i1 == i2 { // we are intersecting a path element against itself if let c = c1 as? CubicBezierCurve { elementIntersections = c.intersects(threshold: threshold) } } - else if i1 < i2 { + else*/ if i1 < i2 { // we are intersecting two distinct path elements elementIntersections = c1.intersects(curve: c2, threshold: threshold).filter { if i1 == Utils.mod(i2-1, self.curves.count) && $0.t1 == 1.0 { return false // exclude intersections of i and i+1 at t=1 } if $0.t1 == 0.0 || $0.t2 == 0.0 { - // use the intersection with the prior path element at t=1.0 instead + // use the intersection with the prior path element at t=1 instead return false } return true From 6c853a2c40c5abc8dfa03b4e6b5ed190090fa8f8 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Fri, 7 Dec 2018 16:49:27 -0800 Subject: [PATCH 15/18] clarify comment --- BezierKit/Library/PathComponent.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BezierKit/Library/PathComponent.swift b/BezierKit/Library/PathComponent.swift index 9510d336..d7154640 100644 --- a/BezierKit/Library/PathComponent.swift +++ b/BezierKit/Library/PathComponent.swift @@ -113,7 +113,7 @@ public final class PathComponent: NSObject, NSCoding { let c1 = o1 as! BezierCurve let c2 = o2 as! BezierCurve var elementIntersections: [Intersection] = [] - // TODO: fix behavior for `crossingsRemoved` when there are self intersections and re-enable + // TODO: fix behavior for `crossingsRemoved` when there are self intersections at t=0 or t=1 and re-enable /*if i1 == i2 { // we are intersecting a path element against itself if let c = c1 as? CubicBezierCurve { From f6a05e15a7081a21c96c9adb0a9224198be282ce Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Fri, 7 Dec 2018 16:50:27 -0800 Subject: [PATCH 16/18] update readme and podspec. --- BezierKit.podspec | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BezierKit.podspec b/BezierKit.podspec index 31db2d72..5ec53477 100644 --- a/BezierKit.podspec +++ b/BezierKit.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |s| s.name = "BezierKit" - s.version = "0.1.13" + s.version = "0.1.14" s.summary = "comprehensive Bezier curve library written in Swift" s.homepage = "https://github.com/hfutrell/BezierKit" s.license = "MIT" diff --git a/README.md b/README.md index 0b1dd703..f1928fe4 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ To integrate BezierKit into your Xcode project using CocoaPods, add it to your t ```ruby target '' do - pod 'BezierKit', '>= 0.1.13' + pod 'BezierKit', '>= 0.1.14' end ``` From ee9e89ad78945d8afcaa9a6c57eba0b0afcd6ae0 Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Fri, 7 Dec 2018 16:52:59 -0800 Subject: [PATCH 17/18] Revert "marked boolean operations with "try!"" This reverts commit 165a975adbb541fa866ffaf432f0fa667b1d713a. --- BezierKit/BezierKitTests/PathTests.swift | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/BezierKit/BezierKitTests/PathTests.swift b/BezierKit/BezierKitTests/PathTests.swift index 8b1345db..065ed1de 100644 --- a/BezierKit/BezierKitTests/PathTests.swift +++ b/BezierKit/BezierKitTests/PathTests.swift @@ -342,7 +342,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let subtracted = try! square1.subtracting(square2) + let subtracted = square1.subtracting(square2) XCTAssertEqual(subtracted.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(subtracted.subpaths[0], expectedResult.subpaths[0]) @@ -364,7 +364,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let unioned = try! square1.union(square2) + let unioned = square1.union(square2) XCTAssertEqual(unioned.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(unioned.subpaths[0], expectedResult.subpaths[0]) @@ -382,7 +382,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let intersected = try! square1.intersecting(square2) + let intersected = square1.intersecting(square2) XCTAssertEqual(intersected.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(intersected.subpaths[0], expectedResult.subpaths[0]) @@ -394,7 +394,7 @@ class PathTests: XCTestCase { // the order of the hole is reversed so that it is not contained in the shape when using .winding fill rule let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 3, height: 3), transform: nil)) let hole = Path(cgPath: CGPath(ellipseIn: CGRect(x: 1, y: 1, width: 1, height: 1), transform: nil)) - let donut = try! circle.subtracting(hole) + let donut = circle.subtracting(hole) XCTAssertTrue(donut.contains(CGPoint(x: 0.5, y: 0.5), using: .winding)) // inside the donut (but not the hole) XCTAssertFalse(donut.contains(CGPoint(x: 1.5, y: 1.5), using: .winding)) // center of donut hole } @@ -403,7 +403,7 @@ class PathTests: XCTestCase { // this is a specific test of `subtracting` to ensure that if a path component is entirely contained in the subtracting path that it gets removed let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: -1, y: -1, width: 2, height: 2), transform: nil)) let biggerCircle = Path(cgPath: CGPath(ellipseIn: CGRect(x: -2, y: -2, width: 4, height: 4), transform: nil)) - XCTAssertEqual(try! circle.subtracting(biggerCircle).subpaths.count, 0) + XCTAssertEqual(circle.subtracting(biggerCircle).subpaths.count, 0) } func testSubtractingEdgeCase1() { @@ -415,7 +415,7 @@ class PathTests: XCTestCase { let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 4, height: 4), transform: nil)) // the circle intersects the rect at (0,2) and (3, 0.26792) ... the last number being exactly 2 - sqrt(3) - let difference = try! rectangle.subtracting(circle) + let difference = rectangle.subtracting(circle) XCTAssertEqual(difference.subpaths.count, 1) XCTAssertFalse(difference.contains(CGPoint(x: 2.0, y: 2.0))) } @@ -434,7 +434,7 @@ class PathTests: XCTestCase { square2CGPath.closeSubpath() let square2 = Path(cgPath: square2CGPath) - let result = try! square1.subtracting(square2) + let result = square1.subtracting(square2) let expectedResultCGPath = CGMutablePath() expectedResultCGPath.move(to: CGPoint.zero) @@ -474,7 +474,7 @@ class PathTests: XCTestCase { XCTAssertTrue(path.contains(CGPoint(x: 1.5, y: 1.25), using: .winding)) XCTAssertFalse(path.contains(CGPoint(x: 1.5, y: 1.25), using: .evenOdd)) - let result = try! path.crossingsRemoved() + let result = path.crossingsRemoved() XCTAssertEqual(result.subpaths.count, 1) XCTAssertTrue(componentsEqualAsideFromElementOrdering(result.subpaths[0], expectedResult.subpaths[0])) @@ -483,7 +483,7 @@ class PathTests: XCTestCase { cgPathAlt.addLines(between: Array(points[3.. 0.0 } let cleanPath = Path(subpaths: [PathComponent(curves: curves2)]) - let result = try! cleanPath.crossingsRemoved(threshold: 1.0e-4) + let result = cleanPath.crossingsRemoved(threshold: 1.0e-4) // check that the inner loop was eliminated by checking the winding count in the middle XCTAssertEqual(result.windingCount(CGPoint(x: 0.5, y: 1)), 1) From c05402a5da8dcf73ba7d9c7ea3d6a760491697ce Mon Sep 17 00:00:00 2001 From: Holmes Futrell Date: Fri, 7 Dec 2018 17:00:49 -0800 Subject: [PATCH 18/18] instead of throwing, just return nil since the error was not useful anyway. --- BezierKit/BezierKitTests/PathTests.swift | 24 +++++++++---------- BezierKit/Library/AugmentedGraph.swift | 6 ++--- BezierKit/Library/BezierCurve.swift | 1 - BezierKit/Library/Path.swift | 30 +++++++++++++----------- BezierKit/MacDemos/Demos.swift | 2 +- 5 files changed, 31 insertions(+), 32 deletions(-) diff --git a/BezierKit/BezierKitTests/PathTests.swift b/BezierKit/BezierKitTests/PathTests.swift index 065ed1de..2aff8275 100644 --- a/BezierKit/BezierKitTests/PathTests.swift +++ b/BezierKit/BezierKitTests/PathTests.swift @@ -342,7 +342,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let subtracted = square1.subtracting(square2) + let subtracted = square1.subtracting(square2)! XCTAssertEqual(subtracted.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(subtracted.subpaths[0], expectedResult.subpaths[0]) @@ -364,7 +364,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let unioned = square1.union(square2) + let unioned = square1.union(square2)! XCTAssertEqual(unioned.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(unioned.subpaths[0], expectedResult.subpaths[0]) @@ -382,7 +382,7 @@ class PathTests: XCTestCase { )]) let square1 = createSquare1() let square2 = createSquare2() - let intersected = square1.intersecting(square2) + let intersected = square1.intersecting(square2)! XCTAssertEqual(intersected.subpaths.count, 1) XCTAssert( componentsEqualAsideFromElementOrdering(intersected.subpaths[0], expectedResult.subpaths[0]) @@ -394,7 +394,7 @@ class PathTests: XCTestCase { // the order of the hole is reversed so that it is not contained in the shape when using .winding fill rule let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 3, height: 3), transform: nil)) let hole = Path(cgPath: CGPath(ellipseIn: CGRect(x: 1, y: 1, width: 1, height: 1), transform: nil)) - let donut = circle.subtracting(hole) + let donut = circle.subtracting(hole)! XCTAssertTrue(donut.contains(CGPoint(x: 0.5, y: 0.5), using: .winding)) // inside the donut (but not the hole) XCTAssertFalse(donut.contains(CGPoint(x: 1.5, y: 1.5), using: .winding)) // center of donut hole } @@ -403,7 +403,7 @@ class PathTests: XCTestCase { // this is a specific test of `subtracting` to ensure that if a path component is entirely contained in the subtracting path that it gets removed let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: -1, y: -1, width: 2, height: 2), transform: nil)) let biggerCircle = Path(cgPath: CGPath(ellipseIn: CGRect(x: -2, y: -2, width: 4, height: 4), transform: nil)) - XCTAssertEqual(circle.subtracting(biggerCircle).subpaths.count, 0) + XCTAssertEqual(circle.subtracting(biggerCircle)!.subpaths.count, 0) } func testSubtractingEdgeCase1() { @@ -415,7 +415,7 @@ class PathTests: XCTestCase { let circle = Path(cgPath: CGPath(ellipseIn: CGRect(x: 0, y: 0, width: 4, height: 4), transform: nil)) // the circle intersects the rect at (0,2) and (3, 0.26792) ... the last number being exactly 2 - sqrt(3) - let difference = rectangle.subtracting(circle) + let difference = rectangle.subtracting(circle)! XCTAssertEqual(difference.subpaths.count, 1) XCTAssertFalse(difference.contains(CGPoint(x: 2.0, y: 2.0))) } @@ -434,7 +434,7 @@ class PathTests: XCTestCase { square2CGPath.closeSubpath() let square2 = Path(cgPath: square2CGPath) - let result = square1.subtracting(square2) + let result = square1.subtracting(square2)! let expectedResultCGPath = CGMutablePath() expectedResultCGPath.move(to: CGPoint.zero) @@ -474,7 +474,7 @@ class PathTests: XCTestCase { XCTAssertTrue(path.contains(CGPoint(x: 1.5, y: 1.25), using: .winding)) XCTAssertFalse(path.contains(CGPoint(x: 1.5, y: 1.25), using: .evenOdd)) - let result = path.crossingsRemoved() + let result = path.crossingsRemoved()! XCTAssertEqual(result.subpaths.count, 1) XCTAssertTrue(componentsEqualAsideFromElementOrdering(result.subpaths[0], expectedResult.subpaths[0])) @@ -483,7 +483,7 @@ class PathTests: XCTestCase { cgPathAlt.addLines(between: Array(points[3.. 0.0 } let cleanPath = Path(subpaths: [PathComponent(curves: curves2)]) - let result = cleanPath.crossingsRemoved(threshold: 1.0e-4) + let result = cleanPath.crossingsRemoved(threshold: 1.0e-4)! // check that the inner loop was eliminated by checking the winding count in the middle XCTAssertEqual(result.windingCount(CGPoint(x: 0.5, y: 1)), 1) diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift index b5cc086a..2c541320 100644 --- a/BezierKit/Library/AugmentedGraph.swift +++ b/BezierKit/Library/AugmentedGraph.swift @@ -7,7 +7,6 @@ // import CoreGraphics -import Foundation public func signedAngle(_ a: CGPoint, _ b: CGPoint) -> CGFloat { return atan2(CGPoint.cross(a, b), a.dot(b)) @@ -340,7 +339,7 @@ internal class AugmentedGraph { } } - internal func booleanOperation(_ operation: BooleanPathOperation) throws -> Path { + internal func booleanOperation(_ operation: BooleanPathOperation) -> Path? { // special cases for components which do not cross let nonCrossingComponents1: [PathComponent] = self.list1.nonCrossingComponents() @@ -393,8 +392,7 @@ internal class AugmentedGraph { isOnFirstCurve = !isOnFirstCurve if isOnFirstCurve && unvisitedCrossings.contains(v) == false && v !== start { - let userInfo = [NSLocalizedDescriptionKey: "Boolean operation failed, try more accurate threshold?"] - throw NSError(domain: BezierKit.errorDomain, code: -1, userInfo: userInfo) + return nil } } while v !== start pathComponents.append(PathComponent(curves: curves)) diff --git a/BezierKit/Library/BezierCurve.swift b/BezierKit/Library/BezierCurve.swift index 7cf027be..c80756e5 100644 --- a/BezierKit/Library/BezierCurve.swift +++ b/BezierKit/Library/BezierCurve.swift @@ -578,7 +578,6 @@ extension BezierCurve { } public let defaultIntersectionThreshold = CGFloat(0.5) -public let errorDomain = "BezierKit" // MARK: factory diff --git a/BezierKit/Library/Path.swift b/BezierKit/Library/Path.swift index 3512fcb2..f68bca47 100644 --- a/BezierKit/Library/Path.swift +++ b/BezierKit/Library/Path.swift @@ -22,9 +22,6 @@ internal func windingCountImpliesContainment(_ count: Int, using rule: PathFillR } } -@objc(BezierKitPathError) public class PathError: NSError { -} - @objc(BezierKitPath) public class Path: NSObject, NSCoding { private class PathApplierFunctionContext { @@ -204,34 +201,37 @@ internal func windingCountImpliesContainment(_ count: Int, using rule: PathFillR return windingCountImpliesContainment(count, using: rule) } - private func performBooleanOperation(_ operation: BooleanPathOperation, withPath other: Path, threshold: CGFloat) throws -> Path { + private func performBooleanOperation(_ operation: BooleanPathOperation, withPath other: Path, threshold: CGFloat) -> Path? { let intersections = self.intersects(path: other, threshold: threshold) let augmentedGraph = AugmentedGraph(path1: self, path2: other, intersections: intersections) - return try augmentedGraph.booleanOperation(operation) + return augmentedGraph.booleanOperation(operation) } - @objc(subtractingPath:threshold:error:) public func subtracting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { - return try self.performBooleanOperation(.difference, withPath: other.reversed(), threshold: threshold) + @objc(subtractingPath:threshold:) public func subtracting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path? { + return self.performBooleanOperation(.difference, withPath: other.reversed(), threshold: threshold) } - @objc(unionedWithPath:threshold:error:) public func `union`(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { + @objc(unionedWithPath:threshold:) public func `union`(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path? { guard self.isEmpty == false else { return other } guard other.isEmpty == false else { return self } - return try self.performBooleanOperation(.union, withPath: other, threshold: threshold) + return self.performBooleanOperation(.union, withPath: other, threshold: threshold) } - @objc(intersectedWithPath:threshold:error:) public func intersecting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { - return try self.performBooleanOperation(.intersection, withPath: other, threshold: threshold) + @objc(intersectedWithPath:threshold:) public func intersecting(_ other: Path, threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path? { + return self.performBooleanOperation(.intersection, withPath: other, threshold: threshold) } - @objc(crossingsRemovedWithThreshold:error:) public func crossingsRemoved(threshold: CGFloat=BezierKit.defaultIntersectionThreshold) throws -> Path { + @objc(crossingsRemovedWithThreshold:) public func crossingsRemoved(threshold: CGFloat=BezierKit.defaultIntersectionThreshold) -> Path? { // assert(self.subpaths.count <= 1, "todo: support multi-component paths") - return try self.subpaths.reduce(Path(), { result, component in + return self.subpaths.reduce(Path(), { (result: Path?, component: PathComponent) -> Path? in // TODO: this won't work properly if components intersect + guard let result = result else { + return nil + } let intersections = component.intersects(threshold: threshold).map {(i: PathComponentIntersection) -> PathIntersection in return PathIntersection(indexedPathLocation1: IndexedPathLocation(componentIndex: 0, elementIndex: i.indexedComponentLocation1.elementIndex, t: i.indexedComponentLocation1.t), indexedPathLocation2: IndexedPathLocation(componentIndex: 0, elementIndex: i.indexedComponentLocation2.elementIndex, t: i.indexedComponentLocation2.t)) @@ -241,7 +241,9 @@ internal func windingCountImpliesContainment(_ count: Int, using rule: PathFillR } let singleComponentPath = Path(subpaths: [component]) let augmentedGraph = AugmentedGraph(path1: singleComponentPath, path2: singleComponentPath, intersections: intersections) - let path = try augmentedGraph.booleanOperation(.removeCrossings) + guard let path = augmentedGraph.booleanOperation(.removeCrossings) else { + return nil + } return Path(subpaths: result.subpaths + path.subpaths) }) } diff --git a/BezierKit/MacDemos/Demos.swift b/BezierKit/MacDemos/Demos.swift index 5c934016..a7474705 100644 --- a/BezierKit/MacDemos/Demos.swift +++ b/BezierKit/MacDemos/Demos.swift @@ -464,7 +464,7 @@ class Demos { // v = v.next // } while v !== first - let subtracted = (try? path1.intersecting(path2)) ?? path1 + let subtracted = path1.intersecting(path2) ?? path1 Draw.drawPath(context, subtracted) } })