diff --git a/BezierKit.podspec b/BezierKit.podspec index 452c3872..e16ee187 100644 --- a/BezierKit.podspec +++ b/BezierKit.podspec @@ -5,8 +5,8 @@ Pod::Spec.new do |s| s.name = "BezierKit" - s.version = "0.1.7" - s.summary = "comprehensive Bezier curve library written in Swift based on the popular Bezier.js library" + s.version = "0.1.8" + s.summary = "comprehensive Bezier curve library written in Swift" s.homepage = "https://github.com/hfutrell/BezierKit" s.license = "MIT" s.author = { "Holmes Futrell" => "holmesfutrell@gmail.com" } diff --git a/BezierKit/BezierKit.xcodeproj/project.pbxproj b/BezierKit/BezierKit.xcodeproj/project.pbxproj index 31536d9d..976fa1ce 100644 --- a/BezierKit/BezierKit.xcodeproj/project.pbxproj +++ b/BezierKit/BezierKit.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ FD05111820153EAE000D035E /* BoundingBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05111720153EAE000D035E /* BoundingBoxTests.swift */; }; FD05111920153EAE000D035E /* BoundingBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD05111720153EAE000D035E /* BoundingBoxTests.swift */; }; + FD149EBA2135CBFF009E791D /* AugmentedGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD149EB92135CBFF009E791D /* AugmentedGraph.swift */; }; + FD149EBB2135CBFF009E791D /* AugmentedGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD149EB92135CBFF009E791D /* AugmentedGraph.swift */; }; FD26253F1EAC7B9A00C64652 /* BezierKit_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = FD26253D1EAC7B9A00C64652 /* BezierKit_iOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; FD4024502110CF5100FA723C /* QuadraticBezierCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD40244F2110CF5100FA723C /* QuadraticBezierCurveTests.swift */; }; FD4024512110CF5100FA723C /* QuadraticBezierCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD40244F2110CF5100FA723C /* QuadraticBezierCurveTests.swift */; }; @@ -19,8 +21,8 @@ FD4A63FF200AA50B00930E10 /* Shape.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4A63FD200AA50B00930E10 /* Shape.swift */; }; FD4A6403200ACBD200930E10 /* ShapeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4A6400200ACB8900930E10 /* ShapeTests.swift */; }; FD4A6404200ACBD200930E10 /* ShapeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4A6400200ACB8900930E10 /* ShapeTests.swift */; }; - FD4A6408200B11DF00930E10 /* PolyBezierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4A6405200B11DA00930E10 /* PolyBezierTests.swift */; }; - FD4A6409200B11DF00930E10 /* PolyBezierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4A6405200B11DA00930E10 /* PolyBezierTests.swift */; }; + FD4A6408200B11DF00930E10 /* PathComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4A6405200B11DA00930E10 /* PathComponentTests.swift */; }; + FD4A6409200B11DF00930E10 /* PathComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4A6405200B11DA00930E10 /* PathComponentTests.swift */; }; FDA727591ED5035300011871 /* CubicBezierCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA727581ED5035300011871 /* CubicBezierCurveTests.swift */; }; FDA7275A1ED5035300011871 /* CubicBezierCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA727581ED5035300011871 /* CubicBezierCurveTests.swift */; }; FDB6B4021EAFD6DF00001C61 /* BezierCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3F71EAFD6DF00001C61 /* BezierCurve.swift */; }; @@ -31,8 +33,8 @@ FDB6B4071EAFD6DF00001C61 /* Draw.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3F91EAFD6DF00001C61 /* Draw.swift */; }; FDB6B40C1EAFD6DF00001C61 /* CGPoint+Overloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FC1EAFD6DF00001C61 /* CGPoint+Overloads.swift */; }; FDB6B40D1EAFD6DF00001C61 /* CGPoint+Overloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FC1EAFD6DF00001C61 /* CGPoint+Overloads.swift */; }; - FDB6B40E1EAFD6DF00001C61 /* PolyBezier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FD1EAFD6DF00001C61 /* PolyBezier.swift */; }; - FDB6B40F1EAFD6DF00001C61 /* PolyBezier.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FD1EAFD6DF00001C61 /* PolyBezier.swift */; }; + FDB6B40E1EAFD6DF00001C61 /* PathComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FD1EAFD6DF00001C61 /* PathComponent.swift */; }; + FDB6B40F1EAFD6DF00001C61 /* PathComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FD1EAFD6DF00001C61 /* PathComponent.swift */; }; FDB6B4101EAFD6DF00001C61 /* QuadraticBezierCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FE1EAFD6DF00001C61 /* QuadraticBezierCurve.swift */; }; FDB6B4111EAFD6DF00001C61 /* QuadraticBezierCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FE1EAFD6DF00001C61 /* QuadraticBezierCurve.swift */; }; FDB6B4121EAFD6DF00001C61 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB6B3FF1EAFD6DF00001C61 /* Types.swift */; }; @@ -114,6 +116,7 @@ FD0F54F51DC43FFB0084CDCD /* MacDemos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacDemos.app; sourceTree = BUILT_PRODUCTS_DIR; }; FD0F55081DC43FFB0084CDCD /* BezierKitTestHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BezierKitTestHelpers.swift; sourceTree = ""; }; FD0F550A1DC43FFB0084CDCD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FD149EB92135CBFF009E791D /* AugmentedGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AugmentedGraph.swift; sourceTree = ""; }; FD26253B1EAC7B9A00C64652 /* BezierKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BezierKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FD26253D1EAC7B9A00C64652 /* BezierKit_iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BezierKit_iOS.h; sourceTree = ""; }; FD26253E1EAC7B9A00C64652 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -123,13 +126,13 @@ FD4A0DC31EAAD01F0031A393 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FD4A63FD200AA50B00930E10 /* Shape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Shape.swift; path = Library/Shape.swift; sourceTree = SOURCE_ROOT; }; FD4A6400200ACB8900930E10 /* ShapeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeTests.swift; sourceTree = ""; }; - FD4A6405200B11DA00930E10 /* PolyBezierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolyBezierTests.swift; sourceTree = ""; }; + FD4A6405200B11DA00930E10 /* PathComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathComponentTests.swift; sourceTree = ""; }; FDA727581ED5035300011871 /* CubicBezierCurveTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CubicBezierCurveTests.swift; sourceTree = ""; }; FDB6B3F71EAFD6DF00001C61 /* BezierCurve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierCurve.swift; sourceTree = ""; }; FDB6B3F81EAFD6DF00001C61 /* CubicBezierCurve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CubicBezierCurve.swift; sourceTree = ""; }; FDB6B3F91EAFD6DF00001C61 /* Draw.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Draw.swift; sourceTree = ""; }; FDB6B3FC1EAFD6DF00001C61 /* CGPoint+Overloads.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGPoint+Overloads.swift"; sourceTree = ""; }; - FDB6B3FD1EAFD6DF00001C61 /* PolyBezier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PolyBezier.swift; sourceTree = ""; }; + FDB6B3FD1EAFD6DF00001C61 /* PathComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathComponent.swift; sourceTree = ""; }; FDB6B3FE1EAFD6DF00001C61 /* QuadraticBezierCurve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuadraticBezierCurve.swift; sourceTree = ""; }; FDB6B3FF1EAFD6DF00001C61 /* Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; FDB6B4001EAFD6DF00001C61 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; @@ -236,7 +239,7 @@ FD40244F2110CF5100FA723C /* QuadraticBezierCurveTests.swift */, FDEB70A31F3674CB00539003 /* ArcApproximateableTests.swift */, FD4A6400200ACB8900930E10 /* ShapeTests.swift */, - FD4A6405200B11DA00930E10 /* PolyBezierTests.swift */, + FD4A6405200B11DA00930E10 /* PathComponentTests.swift */, FD05111720153EAE000D035E /* BoundingBoxTests.swift */, FDC7D705211288BD00A9EEF0 /* PathTests.swift */, FDC859592118EC5600AF7642 /* DrawTests.swift */, @@ -272,11 +275,12 @@ FDE6CD911EC92EDF00FAB479 /* ArcApproximateable.swift */, FDB6B3F91EAFD6DF00001C61 /* Draw.swift */, FDB6B3FC1EAFD6DF00001C61 /* CGPoint+Overloads.swift */, - FDB6B3FD1EAFD6DF00001C61 /* PolyBezier.swift */, FDB6B3FF1EAFD6DF00001C61 /* Types.swift */, FDB6B4001EAFD6DF00001C61 /* Utils.swift */, FD4A63FD200AA50B00930E10 /* Shape.swift */, FDC7D7012111323A00A9EEF0 /* Path.swift */, + FDB6B3FD1EAFD6DF00001C61 /* PathComponent.swift */, + FD149EB92135CBFF009E791D /* AugmentedGraph.swift */, FDC859622119274A00AF7642 /* BoundingVolumeHierarchy.swift */, ); path = Library; @@ -547,7 +551,7 @@ FDC859642119274A00AF7642 /* BoundingVolumeHierarchy.swift in Sources */, FD4A63FF200AA50B00930E10 /* Shape.swift in Sources */, FDB6B40D1EAFD6DF00001C61 /* CGPoint+Overloads.swift in Sources */, - FDB6B40F1EAFD6DF00001C61 /* PolyBezier.swift in Sources */, + FDB6B40F1EAFD6DF00001C61 /* PathComponent.swift in Sources */, FDB6B4151EAFD6DF00001C61 /* Utils.swift in Sources */, FDB6B4071EAFD6DF00001C61 /* Draw.swift in Sources */, FDB6B4131EAFD6DF00001C61 /* Types.swift in Sources */, @@ -555,6 +559,7 @@ FDE6CD931EC92EDF00FAB479 /* ArcApproximateable.swift in Sources */, FDB6B4051EAFD6DF00001C61 /* CubicBezierCurve.swift in Sources */, FDE6CD8D1EC8F2F800FAB479 /* LineSegment.swift in Sources */, + FD149EBB2135CBFF009E791D /* AugmentedGraph.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -567,7 +572,7 @@ FDC455EE211D057E00DBF2B2 /* BoundingVolumeHierarchy.swift in Sources */, FD4A63FE200AA50B00930E10 /* Shape.swift in Sources */, FDB6B40C1EAFD6DF00001C61 /* CGPoint+Overloads.swift in Sources */, - FDB6B40E1EAFD6DF00001C61 /* PolyBezier.swift in Sources */, + FDB6B40E1EAFD6DF00001C61 /* PathComponent.swift in Sources */, FDB6B4141EAFD6DF00001C61 /* Utils.swift in Sources */, FDB6B4061EAFD6DF00001C61 /* Draw.swift in Sources */, FDB6B4121EAFD6DF00001C61 /* Types.swift in Sources */, @@ -575,6 +580,7 @@ FDE6CD921EC92EDF00FAB479 /* ArcApproximateable.swift in Sources */, FDB6B4041EAFD6DF00001C61 /* CubicBezierCurve.swift in Sources */, FDE6CD8C1EC8F2F800FAB479 /* LineSegment.swift in Sources */, + FD149EBA2135CBFF009E791D /* AugmentedGraph.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -582,7 +588,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD4A6408200B11DF00930E10 /* PolyBezierTests.swift in Sources */, + FD4A6408200B11DF00930E10 /* PathComponentTests.swift in Sources */, FD4A6403200ACBD200930E10 /* ShapeTests.swift in Sources */, FDF0664E1FFA0C9900123308 /* BezierCurveTests.swift in Sources */, FDEB70A41F3674CB00539003 /* ArcApproximateableTests.swift in Sources */, @@ -601,7 +607,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FD4A6409200B11DF00930E10 /* PolyBezierTests.swift in Sources */, + FD4A6409200B11DF00930E10 /* PathComponentTests.swift in Sources */, FD4A6404200ACBD200930E10 /* ShapeTests.swift in Sources */, FDF0664F1FFA0C9900123308 /* BezierCurveTests.swift in Sources */, FDEB70A51F3674CB00539003 /* ArcApproximateableTests.swift in Sources */, diff --git a/BezierKit/BezierKitTests/BezierCurveTests.swift b/BezierKit/BezierKitTests/BezierCurveTests.swift index bad59655..7f76c167 100644 --- a/BezierKit/BezierKitTests/BezierCurveTests.swift +++ b/BezierKit/BezierKitTests/BezierCurveTests.swift @@ -147,7 +147,7 @@ class BezierCurveTests: XCTestCase { func testOutlineDistance() { // When only one distance value is given, the outline is generated at distance d on both the normal and anti-normal let lineSegment = BezierCurveTests.lineSegmentForOutlining - let outline: PolyBezier = lineSegment.outline(distance: 1) + let outline: PathComponent = lineSegment.outline(distance: 1) XCTAssertEqual(outline.curves.count, 4) let (o0, o1, o2, o3) = lineOffsets(lineSegment, 1, 1, 1, 1) @@ -163,7 +163,7 @@ class BezierCurveTests: XCTestCase { let lineSegment = BezierCurveTests.lineSegmentForOutlining let distanceAlongNormal: CGFloat = 1 let distanceOppositeNormal: CGFloat = 2 - let outline: PolyBezier = lineSegment.outline(distanceAlongNormal: distanceAlongNormal, distanceOppositeNormal: distanceOppositeNormal) + let outline: PathComponent = lineSegment.outline(distanceAlongNormal: distanceAlongNormal, distanceOppositeNormal: distanceOppositeNormal) XCTAssertEqual(outline.curves.count, 4) let o0 = lineSegment.startingPoint + distanceAlongNormal * lineSegment.normal(0) @@ -185,7 +185,7 @@ class BezierCurveTests: XCTestCase { let distanceAlongNormal2: CGFloat = 1 let distanceOppositeNormal2: CGFloat = 2 - let outline: PolyBezier = lineSegment.outline(distanceAlongNormalStart: distanceAlongNormal1, + let outline: PathComponent = lineSegment.outline(distanceAlongNormalStart: distanceAlongNormal1, distanceOppositeNormalStart: distanceOppositeNormal1, distanceAlongNormalEnd: distanceAlongNormal2, distanceOppositeNormalEnd: distanceOppositeNormal2) @@ -206,7 +206,7 @@ class BezierCurveTests: XCTestCase { // 2. quadratics are upgrade in the outline function (why?) let q = QuadraticBezierCurve(p0: CGPoint(x: 0.0, y: 0.0), p1: CGPoint(x: 9.0, y: 11.0), p2: CGPoint(x: 20.0, y: 20.0)) - let outline: PolyBezier = q.outline(distanceAlongNormalStart: sqrt(2), distanceOppositeNormalStart: sqrt(2), distanceAlongNormalEnd: 2 * sqrt(2), distanceOppositeNormalEnd: 2 * sqrt(2)) + let outline: PathComponent = q.outline(distanceAlongNormalStart: sqrt(2), distanceOppositeNormalStart: sqrt(2), distanceAlongNormalEnd: 2 * sqrt(2), distanceOppositeNormalEnd: 2 * sqrt(2)) let expectedSegment1 = LineSegment(p0: CGPoint(x: 1, y: -1), p1: CGPoint(x: -1, y: 1)) let expectedSegment2 = QuadraticBezierCurve(p0: CGPoint(x: -1, y: 1), p1: CGPoint(x: 7.5, y: 12.5), p2: CGPoint(x: 18, y: 22)) @@ -225,7 +225,7 @@ class BezierCurveTests: XCTestCase { // this tests a special corner case of outlines where endpoint normals are parallel let q = QuadraticBezierCurve(p0: CGPoint(x: 0.0, y: 0.0), p1: CGPoint(x: 5.0, y: 0.0), p2: CGPoint(x: 10.0, y: 0.0)) - let outline: PolyBezier = q.outline(distance: 1) + let outline: PathComponent = q.outline(distance: 1) let expectedSegment1 = LineSegment(p0: CGPoint(x: 0, y: -1), p1: CGPoint(x: 0, y: 1)) let expectedSegment2 = LineSegment(p0: CGPoint(x: 0, y: 1), p1: CGPoint(x: 10, y: 1)) @@ -243,7 +243,7 @@ class BezierCurveTests: XCTestCase { // this tests a special corner case of tapered outlines where endpoint normals are parallel let q = QuadraticBezierCurve(p0: CGPoint(x: 0.0, y: 0.0), p1: CGPoint(x: 10.0, y: 0.0), p2: CGPoint(x: 20.0, y: 0.0)) - let outline: PolyBezier = q.outline(distanceAlongNormalStart: 2, distanceOppositeNormalStart: 2, distanceAlongNormalEnd: 1, distanceOppositeNormalEnd: 1) + let outline: PathComponent = q.outline(distanceAlongNormalStart: 2, distanceOppositeNormalStart: 2, distanceAlongNormalEnd: 1, distanceOppositeNormalEnd: 1) let expectedSegment1 = LineSegment(p0: CGPoint(x: 0.0, y: -2.0), p1: CGPoint(x: 0.0, y: 2.0)) let expectedSegment2 = LineSegment(p0: CGPoint(x: 0.0, y: 2.0), p1: CGPoint(x: 20.0, y: 1.0)) diff --git a/BezierKit/BezierKitTests/PolyBezierTests.swift b/BezierKit/BezierKitTests/PathComponentTests.swift similarity index 61% rename from BezierKit/BezierKitTests/PolyBezierTests.swift rename to BezierKit/BezierKitTests/PathComponentTests.swift index 6e52c972..66e0985c 100644 --- a/BezierKit/BezierKitTests/PolyBezierTests.swift +++ b/BezierKit/BezierKitTests/PathComponentTests.swift @@ -1,5 +1,5 @@ // -// PolyBezierTests.swift +// PathComponentTests.swift // BezierKit // // Created by Holmes Futrell on 1/13/18. @@ -9,26 +9,26 @@ import XCTest @testable import BezierKit -class PolyBezierTests: XCTestCase { +class PathComponentTests: XCTestCase { let line1 = LineSegment(p0: CGPoint(x: 1.0, y: 2.0), p1: CGPoint(x: 5.0, y: 5.0)) // length = 5 let line2 = LineSegment(p0: CGPoint(x: 5.0, y: 5.0), p1: CGPoint(x: 13.0, y: -1.0)) // length = 10 func testLength() { - let p = PolyBezier(curves: [line1, line2]) + let p = PathComponent(curves: [line1, line2]) XCTAssertEqual(p.length, 15.0) // sum of two lengths } func testBoundingBox() { - let p = PolyBezier(curves: [line1, line2]) + let p = PathComponent(curves: [line1, line2]) XCTAssertEqual(p.boundingBox, BoundingBox(min: CGPoint(x: 1.0, y: -1.0), max: CGPoint(x: 13.0, y: 5.0))) // just the union of the two bounding boxes } func testOffset() { - // construct a PolyBezier from a split cubic + // construct a PathComponent from a split cubic let q = QuadraticBezierCurve(p0: CGPoint(x: 0.0, y: 0.0), p1: CGPoint(x: 2.0, y: 1.0), p2: CGPoint(x: 4.0, y: 0.0)) let (ql, qr) = q.split(at: 0.5) - let p = PolyBezier(curves: [ql, qr]) + let p = PathComponent(curves: [ql, qr]) // test that offset gives us the same result as offsetting the split segments let pOffset = p.offset(distance: 1) @@ -53,17 +53,17 @@ class PolyBezierTests: XCTestCase { let l2 = LineSegment(p0: p4, p1: p5) let c1 = CubicBezierCurve(p0: p5, p1: p6, p2: p7, p3: p8) - let polyBezier1 = PolyBezier(curves: [l1, q1, l2, c1]) - let polyBezier2 = PolyBezier(curves: [l1, q1, l2]) - let polyBezier3 = PolyBezier(curves: [l1, q1, l2, c1]) + let pathComponent1 = PathComponent(curves: [l1, q1, l2, c1]) + let pathComponent2 = PathComponent(curves: [l1, q1, l2]) + let pathComponent3 = PathComponent(curves: [l1, q1, l2, c1]) var altC1 = c1 altC1.p2.x = -0.25 - let polyBezier4 = PolyBezier(curves: [l1, q1, l2, altC1]) + let pathComponent4 = PathComponent(curves: [l1, q1, l2, altC1]) - XCTAssertNotEqual(polyBezier1, polyBezier2) // polyBezier2 is missing 4th path element, so not equal - XCTAssertEqual(polyBezier1, polyBezier3) // same path elements means equal - XCTAssertNotEqual(polyBezier1, polyBezier4) // polyBezier4 has an element with a modified path + XCTAssertNotEqual(pathComponent1, pathComponent2) // pathComponent2 is missing 4th path element, so not equal + XCTAssertEqual(pathComponent1, pathComponent3) // same path elements means equal + XCTAssertNotEqual(pathComponent1, pathComponent4) // pathComponent4 has an element with a modified path } func testIsEqual() { @@ -73,19 +73,19 @@ class PolyBezierTests: XCTestCase { let l2 = LineSegment(p0: p4, p1: p5) let c1 = CubicBezierCurve(p0: p5, p1: p6, p2: p7, p3: p8) - let polyBezier1 = PolyBezier(curves: [l1, q1, l2, c1]) - let polyBezier2 = PolyBezier(curves: [l1, q1, l2, c1]) + let pathComponent1 = PathComponent(curves: [l1, q1, l2, c1]) + let pathComponent2 = PathComponent(curves: [l1, q1, l2, c1]) var altC1 = c1 altC1.p2.x = -0.25 - let polyBezier3 = PolyBezier(curves: [l1, q1, l2, altC1]) + let pathComponent3 = PathComponent(curves: [l1, q1, l2, altC1]) let string = "hello!" as NSString - XCTAssertFalse(polyBezier1.isEqual(string)) - XCTAssertFalse(polyBezier1.isEqual(nil)) - XCTAssertTrue(polyBezier1.isEqual(polyBezier1)) - XCTAssertTrue(polyBezier1.isEqual(polyBezier2)) - XCTAssertFalse(polyBezier1.isEqual(polyBezier3)) + XCTAssertFalse(pathComponent1.isEqual(string)) + XCTAssertFalse(pathComponent1.isEqual(nil)) + XCTAssertTrue(pathComponent1.isEqual(pathComponent1)) + XCTAssertTrue(pathComponent1.isEqual(pathComponent2)) + XCTAssertFalse(pathComponent1.isEqual(pathComponent3)) } func testNSCoder() { @@ -94,11 +94,11 @@ class PolyBezierTests: XCTestCase { let q1 = QuadraticBezierCurve(p0: p2, p1: p3, p2: p4) let l2 = LineSegment(p0: p4, p1: p5) let c1 = CubicBezierCurve(p0: p5, p1: p6, p2: p7, p3: p8) - let polyBezier = PolyBezier(curves: [l1, q1, l2, c1]) + let pathComponent = PathComponent(curves: [l1, q1, l2, c1]) - let data = NSKeyedArchiver.archivedData(withRootObject: polyBezier) - let decodedPolyBezier = NSKeyedUnarchiver.unarchiveObject(with: data) as! PolyBezier - XCTAssertEqual(polyBezier, decodedPolyBezier ) + let data = NSKeyedArchiver.archivedData(withRootObject: pathComponent) + let decodedPathComponent = NSKeyedUnarchiver.unarchiveObject(with: data) as! PathComponent + XCTAssertEqual(pathComponent, decodedPathComponent ) } diff --git a/BezierKit/BezierKitTests/PathTests.swift b/BezierKit/BezierKitTests/PathTests.swift index f0d33956..b78798b9 100644 --- a/BezierKit/BezierKitTests/PathTests.swift +++ b/BezierKit/BezierKitTests/PathTests.swift @@ -186,6 +186,8 @@ class PathTests: XCTestCase { XCTAssertEqual(decodedPath, path) } + // MARK: contains + func testContainsSimple1() { let rect = CGRect(origin: CGPoint(x: -1, y: -1), size: CGSize(width: 2, height: 2)) let path = Path(cgPath: CGPath(rect: rect, transform: nil)) @@ -245,4 +247,20 @@ class PathTests: XCTestCase { } } + func testContainsCircleWithHole() { + let rect1 = CGRect(origin: CGPoint(x: -3, y: -3), size: CGSize(width: 6, height: 6)) + let circlePath = Path(cgPath: CGPath(ellipseIn: rect1, transform: nil)) + let rect2 = CGRect(origin: CGPoint(x: -1, y: -1), size: CGSize(width: 2, height: 2)) + let reversedCirclePath = Path(cgPath: CGPath(ellipseIn: rect2, transform: nil)).reversed() + let circleWithHole = Path(subpaths: circlePath.subpaths + reversedCirclePath.subpaths) + XCTAssertFalse(circleWithHole.contains(CGPoint(x: 0.0, y: 0.0), using: .evenOdd)) + XCTAssertFalse(circleWithHole.contains(CGPoint(x: 0.0, y: 0.0), using: .winding)) + XCTAssertTrue(circleWithHole.contains(CGPoint(x: 2.0, y: 0.0), using: .evenOdd)) + XCTAssertTrue(circleWithHole.contains(CGPoint(x: 2.0, y: 0.0), using: .winding)) + XCTAssertFalse(circleWithHole.contains(CGPoint(x: 4.0, y: 0.0), using: .evenOdd)) + XCTAssertFalse(circleWithHole.contains(CGPoint(x: 4.0, y: 0.0), using: .winding)) + } + + // MARK: simplify(using:) + } diff --git a/BezierKit/Library/AugmentedGraph.swift b/BezierKit/Library/AugmentedGraph.swift new file mode 100644 index 00000000..0844bcb3 --- /dev/null +++ b/BezierKit/Library/AugmentedGraph.swift @@ -0,0 +1,346 @@ +// +// AugmentedGraph.swift +// BezierKit +// +// Created by Holmes Futrell on 8/28/18. +// Copyright © 2018 Holmes Futrell. All rights reserved. +// + +import CoreGraphics + +internal extension PathComponent { + func linkedListRepresentation() -> [Vertex] { + guard self.curves.count > 0 else { + return [] + } + assert(self.curves.first!.startingPoint == self.curves.last!.endingPoint, "this method assumes component is closed!") + var elements: [Vertex] = [] // elements[i] is the first vertex of curves[i] + let firstPoint: CGPoint = self.curves.first!.startingPoint + let firstVertex = Vertex(location: firstPoint, isIntersection: false) + elements.append(firstVertex) + var lastVertex = firstVertex + for i in 1.. Vertex { + let v = Vertex(location: component.point(at: l), isIntersection: true) + return v + } + + self.component1 = component1 + self.component2 = component2 + self.list1 = component1.linkedListRepresentation() + self.list2 = component2.linkedListRepresentation() + intersections.forEach { + let vertex1 = intersectionVertexForComponent(component1, at: $0.indexedComponentLocation1) + let vertex2 = intersectionVertexForComponent(component2, at: $0.indexedComponentLocation2) + connectNeighbors(vertex1, vertex2) // sets the vertex crossing neighbor pointer + self.insertIntersectionVertex(vertex1, inList: &list1, for: component1, at: $0.indexedComponentLocation1) + self.insertIntersectionVertex(vertex2, inList: &list2, for: component2, at: $0.indexedComponentLocation2) + } + // mark each intersection as either entry or exit + markEntryExit(self.v1, component2) + markEntryExit(self.v2, component1) + } + + internal func booleanOperation(_ operationType: BooleanPathOperation) -> Path { + + func moveForwards(_ v: Vertex, _ onFirstCurve: Bool) -> Bool { + switch operationType { + case .union: + return v.intersectionInfo.isExit + case .difference: + return onFirstCurve ? v.intersectionInfo.isExit : v.intersectionInfo.isEntry + case .intersection: + return v.intersectionInfo.isEntry + } + } + + var unvisitedCrossings: Set = Set() + + var current = self.v1 + repeat { + if current.isCrossing { + unvisitedCrossings.insert(current) + } + current = current.next + } while current !== self.v1 + + if unvisitedCrossings.count == 0 { + // handle components that do not cross + switch operationType { + case .union: + return Path(subpaths: [component1, component2].filter { $0.curves.count > 0 }) + case .intersection: + if component1.contains(component2.curves[0].startingPoint, using: .evenOdd) { + return Path(subpaths: [component2]) + } + else if component2.contains(component1.curves[0].startingPoint, using: .evenOdd) { + return Path(subpaths: [component1]) + } + else { + return Path() + } + case .difference: + return Path(subpaths: [component1]) + } + } + + // TODO: add all the crossings to the unvisited crossings set + + var pathComponents: [PathComponent] = [PathComponent]() + while unvisitedCrossings.count > 0 { + + var v = unvisitedCrossings.first! + let start = v + unvisitedCrossings.remove(v) + + var curves: [BezierCurve] = [BezierCurve]() + var isOnFirstCurve = true + var movingForwards = moveForwards(v, true) + + repeat { + + repeat { + if movingForwards { + curves.append(v.emitNext()) + v = v.next + } + else { + curves.append(v.emitPrevious()) + v = v.previous + } + } while v.isCrossing == false + + if isOnFirstCurve { + unvisitedCrossings.remove(v) + } + + v = v.intersectionInfo.neighbor! + + isOnFirstCurve = !isOnFirstCurve + if isOnFirstCurve { + unvisitedCrossings.remove(v) + } + + // decide on a (possibly) new direction + movingForwards = moveForwards(v, isOnFirstCurve) + + } while v !== start + + // TODO: non-deterministic behavior from usage of Set when choosing starting vertex + pathComponents.append(PathComponent(curves: curves)) + } + return Path(subpaths: pathComponents) + } +} + +internal enum VertexTransition { + case line + case quadCurve(control: CGPoint) + case curve(control1: CGPoint, control2: CGPoint) + init(curve: BezierCurve) { + switch curve { + case is LineSegment: + self = .line + case let quadCurve as QuadraticBezierCurve: + self = .quadCurve(control: quadCurve.p1) + case let cubicCurve as CubicBezierCurve: + self = .curve(control1: cubicCurve.p1, control2: cubicCurve.p2) + default: + fatalError("Vertex does not support curve type (\(type(of: curve))") + } + } +} + +internal class Vertex { + public let location: CGPoint + public let isIntersection: Bool + // pointers must be set after initialization + + public struct IntersectionInfo { + public var isEntry: Bool = false + public var isExit: Bool = false + public var neighbor: Vertex? = nil + } + public var intersectionInfo: IntersectionInfo = IntersectionInfo() + + public var isCrossing: Bool { + return self.isIntersection && (self.intersectionInfo.isEntry || self.intersectionInfo.isExit) + } + + internal struct SplitInfo { + var t: CGFloat + } + internal var splitInfo: SplitInfo? = nil // non-nil only when vertex is inserted by splitting an element + + public private(set) var next: Vertex! = nil + public private(set) weak var previous: Vertex! = nil + public private(set) var nextTransition: VertexTransition! = nil + public private(set) var previousTransition: VertexTransition! = nil + + public func setNextVertex(_ vertex: Vertex, transition: VertexTransition) { + self.next = vertex + self.nextTransition = transition + } + + public func setPreviousVertex(_ vertex: Vertex, transition: VertexTransition) { + self.previous = vertex + self.previousTransition = transition + } + + init(location: CGPoint, isIntersection: Bool) { + self.location = location + self.isIntersection = isIntersection + } + + internal func emitTo(_ end: CGPoint, using transition: VertexTransition) -> BezierCurve { + switch transition { + case .line: + return LineSegment(p0: self.location, p1: end) + case .quadCurve(let c): + return QuadraticBezierCurve(p0: self.location, p1: c, p2: end) + case .curve(let c1, let c2): + return CubicBezierCurve(p0: self.location, p1: c1, p2: c2, p3: end) + } + } + + public func emitNext() -> BezierCurve { + return self.emitTo(next.location, using: nextTransition) + } + + public func emitPrevious() -> BezierCurve { + return self.emitTo(previous.location, using: previousTransition) + } +} + +extension Vertex: Equatable { + public static func == (left: Vertex, right: Vertex) -> Bool { + return left === right + } +} + +extension Vertex: Hashable { + public var hashValue: Int { + return ObjectIdentifier(self).hashValue + } +} + diff --git a/BezierKit/Library/BezierCurve.swift b/BezierKit/Library/BezierCurve.swift index 91123c4d..ef9afc0c 100644 --- a/BezierKit/Library/BezierCurve.swift +++ b/BezierKit/Library/BezierCurve.swift @@ -490,22 +490,22 @@ extension BezierCurve { // MARK: - outlines - public func outline(distance d1: CGFloat) -> PolyBezier { + public func outline(distance d1: CGFloat) -> PathComponent { return internalOutline(d1: d1, d2: d1, d3: 0.0, d4: 0.0, graduated: false) } - public func outline(distanceAlongNormal d1: CGFloat, distanceOppositeNormal d2: CGFloat) -> PolyBezier { + public func outline(distanceAlongNormal d1: CGFloat, distanceOppositeNormal d2: CGFloat) -> PathComponent { return internalOutline(d1: d1, d2: d2, d3: 0.0, d4: 0.0, graduated: false) } public func outline(distanceAlongNormalStart d1: CGFloat, distanceOppositeNormalStart d2: CGFloat, distanceAlongNormalEnd d3: CGFloat, - distanceOppositeNormalEnd d4: CGFloat) -> PolyBezier { + distanceOppositeNormalEnd d4: CGFloat) -> PathComponent { return internalOutline(d1: d1, d2: d2, d3: d3, d4: d4, graduated: true) } - private func internalOutline(d1: CGFloat, d2: CGFloat, d3: CGFloat, d4: CGFloat, graduated: Bool) -> PolyBezier { + private func internalOutline(d1: CGFloat, d2: CGFloat, d3: CGFloat, d4: CGFloat, graduated: Bool) -> PathComponent { let reduced = self.reduce() let len = reduced.count @@ -554,7 +554,7 @@ extension BezierCurve { let segments = [ls] + fcurves + [le] + bcurves // let slen = segments.count - return PolyBezier(curves: segments) + return PathComponent(curves: segments) } diff --git a/BezierKit/Library/Draw.swift b/BezierKit/Library/Draw.swift index 9d9d41c6..a668180b 100644 --- a/BezierKit/Library/Draw.swift +++ b/BezierKit/Library/Draw.swift @@ -89,7 +89,9 @@ public class Draw { public static let pinkish = Draw.Color(red: 1.0, green: 100.0 / 255.0, blue: 100.0 / 255.0, alpha: 1.0) public static let transparentBlue = Draw.Color(red: 0.0, green: 0.0, blue: 1.0, alpha: 0.3) public static let transparentBlack = Draw.Color(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.2) - + public static let blue = Draw.Color(red: 0.0, green: 0.0, blue: 255.0, alpha: 1.0) + public static let green = Draw.Color(red: 0.0, green: 255.0, blue: 0.0, alpha: 1.0) + private static var randomIndex = 0 private static let randomColors: [CGColor] = { var temp: [CGColor] = [] @@ -295,9 +297,9 @@ public class Draw { } - public static func drawPolyBezier(_ context: CGContext, polyBezier: PolyBezier, offset: CGPoint = .zero, includeBoundingVolumeHierarchy: Bool = false) { + public static func drawPathComponent(_ context: CGContext, pathComponent: PathComponent, offset: CGPoint = .zero, includeBoundingVolumeHierarchy: Bool = false) { if includeBoundingVolumeHierarchy { - polyBezier.bvh.visit { node, depth in + pathComponent.bvh.visit { node, depth in setColor(context, color: randomColors[depth]) context.setLineWidth(1.0) context.setLineWidth(5.0 / CGFloat(depth+1)) @@ -308,7 +310,7 @@ public class Draw { } context.setLineWidth(1.0) setColor(context, color: Draw.black) - polyBezier.curves.forEach { + pathComponent.curves.forEach { drawCurve(context, curve: $0, offset: offset) } } diff --git a/BezierKit/Library/Path.swift b/BezierKit/Library/Path.swift index 935c4c57..6a735b21 100644 --- a/BezierKit/Library/Path.swift +++ b/BezierKit/Library/Path.swift @@ -9,16 +9,29 @@ import CoreGraphics import Foundation +@objc(BezierKitPathFillRule) public enum PathFillRule: NSInteger { + case winding=0, evenOdd +} + +internal func windingCountImpliesContainment(_ count: Int, using rule: PathFillRule) -> Bool { + switch rule { + case .winding: + return count != 0 + case .evenOdd: + return count % 2 != 0 + } +} + @objc(BezierKitPath) public class Path: NSObject, NSCoding { private class PathApplierFunctionContext { var currentPoint: CGPoint? = nil var subpathStartPoint: CGPoint? = nil var currentSubpath: [BezierCurve] = [] - var components: [PolyBezier] = [] + var components: [PathComponent] = [] func finishUp() { if currentSubpath.isEmpty == false { - components.append(PolyBezier(curves: currentSubpath)) + components.append(PathComponent(curves: currentSubpath)) currentSubpath = [] } } @@ -38,7 +51,7 @@ import Foundation } }() - public let subpaths: [PolyBezier] + public let subpaths: [PathComponent] public func pointIsWithinDistanceOfBoundary(point p: CGPoint, distance d: CGFloat) -> Bool { return self.subpaths.contains { @@ -53,13 +66,18 @@ import Foundation var intersections: [CGPoint] = [] for s1 in self.subpaths { for s2 in path.subpaths { - intersections += s1.intersects(s2, threshold: threshold) + let componentIntersections: [PathComponentIntersection] = s1.intersects(s2, threshold: threshold) + intersections += componentIntersections.map { s1.point(at: $0.indexedComponentLocation1) } } } return intersections } - required public init(subpaths: [PolyBezier]) { + @objc public convenience override init() { + self.init(subpaths: []) + } + + required public init(subpaths: [PathComponent]) { self.subpaths = subpaths } @@ -73,7 +91,7 @@ import Foundation switch element.pointee.type { case .moveToPoint: if context.currentSubpath.isEmpty == false { - context.components.append(PolyBezier(curves: context.currentSubpath)) + context.components.append(PathComponent(curves: context.currentSubpath)) } context.currentPoint = points[0] context.subpathStartPoint = points[0] @@ -95,7 +113,7 @@ import Foundation let line = LineSegment(p0: context.currentPoint!, p1: context.subpathStartPoint!) context.currentSubpath.append(line) } - context.components.append(PolyBezier(curves: context.currentSubpath)) + context.components.append(PathComponent(curves: context.currentSubpath)) context.currentPoint = context.subpathStartPoint! context.currentSubpath = [] } @@ -115,7 +133,7 @@ import Foundation } required public init?(coder aDecoder: NSCoder) { - guard let array = aDecoder.decodeObject() as? Array else { + guard let array = aDecoder.decodeObject() as? Array else { return nil } self.subpaths = array @@ -131,69 +149,83 @@ import Foundation return self.subpaths == otherPath.subpaths } - // MARK: - + // MARK: - vector boolean operations - public func point(at location: IndexedPathLocation) -> CGPoint { - return self.element(at: location).compute(location.t) - } +// public func point(at location: IndexedPathLocation) -> CGPoint { +// return self.element(at: location).compute(location.t) +// } private func element(at location: IndexedPathLocation) -> BezierCurve { return self.subpaths[location.componentIndex].curves[location.elementIndex] } - internal func intersects(line: LineSegment) -> [IndexedPathLocation] { - let lineBoundingBox = line.boundingBox - var results: [IndexedPathLocation] = [] - for i in 0.. Bool { + let windingCount = self.subpaths.reduce(0) { + $0 + $1.windingCount(at: point) } - return results + return windingCountImpliesContainment(windingCount, using: rule) } - @objc public func contains(_ point: CGPoint, using rule: PathFillRule = .winding) -> Bool { - // TODO: assumes element.normal() is always defined, which unfortunately it's not (eg degenerate curves as points, cusps, zero derivatives at the end of curves) - let line = LineSegment(p0: point, p1: CGPoint(x: self.boundingBox.min.x - self.boundingBox.size.x, y: point.y)) // horizontal line from point out of bounding box - let delta = line.p1 - line.p0 - let intersections = self.intersects(line: line) - var windingCount = 0 - intersections.forEach { - let element = self.element(at: $0) - let t = $0.t - let dotProduct = delta.dot(element.normal($0.t)) - if dotProduct < 0 { - if t != 0 { - windingCount -= 1 - } - } - else if dotProduct > 0 { - if t != 1 { - windingCount += 1 - } - } + @objc(subtractingPath:) public func subtracting(_ other: Path) -> Path { + assert(self.subpaths.count <= 1, "todo: support multi-component paths") + assert(other.subpaths.count <= 1, "todo: support multi-component paths") + guard self.subpaths.count != 0 else { + return Path() } - switch rule { - case .winding: - return windingCount != 0 - case .evenOdd: - return abs(windingCount) % 2 == 1 + guard other.subpaths.count != 0 else { + return self } + let component1 = self.subpaths[0] + let component2 = other.subpaths[0] + let intersections = component1.intersects(component2) + let augmentedGraph = AugmentedGraph(component1: component1, component2: component2, intersections: intersections) + return augmentedGraph.booleanOperation(.difference) + } + + @objc(unionedWithPath:) public func `union`(_ other: Path) -> Path { + assert(self.subpaths.count <= 1, "todo: support multi-component paths") + assert(other.subpaths.count <= 1, "todo: support multi-component paths") + guard self.subpaths.count != 0 else { + return other + } + guard other.subpaths.count != 0 else { + return self + } + let component1 = self.subpaths[0] + let component2 = other.subpaths[0] + let intersections = component1.intersects(component2) + let augmentedGraph = AugmentedGraph(component1: component1, component2: component2, intersections: intersections) + return augmentedGraph.booleanOperation(.union) + } + + @objc(intersectedWithPath:) public func intersecting(_ other: Path) -> Path { + assert(self.subpaths.count <= 1, "todo: support multi-component paths") + assert(other.subpaths.count <= 1, "todo: support multi-component paths") + guard self.subpaths.count != 0 else { + return Path() + } + guard other.subpaths.count != 0 else { + return Path() + } + let component1 = self.subpaths[0] + let component2 = other.subpaths[0] + let intersections = component1.intersects(component2) + let augmentedGraph = AugmentedGraph(component1: component1, component2: component2, intersections: intersections) + return augmentedGraph.booleanOperation(.intersection) + } + + @objc public func crossingsRemoved() -> Path { + assert(self.subpaths.count <= 1, "todo: support multi-component paths") + guard self.subpaths.count > 0 else { + return Path() + } + let component = self.subpaths[0] + let intersections = component.intersects() + let augmentedGraph = AugmentedGraph(component1: component, component2: component, intersections: intersections) + return augmentedGraph.booleanOperation(.union) } } -@objc public enum PathFillRule: NSInteger { - case winding=0, evenOdd -}; - @objc extension Path: Transformable { @objc(copyUsingTransform:) public func copy(using t: CGAffineTransform) -> Self { return type(of: self).init(subpaths: self.subpaths.map { $0.copy(using: t)}) @@ -206,14 +238,7 @@ import Foundation } } -//@objc(BezierKitPathIntersection) public class PathIntersection: NSObject { -// let indices: [IndexedPathLocation] -// init(indices: [IndexedPathLocation]) { -// self.indices = indices -// } -//} - -@objc(BezierKitPathIndex) public class IndexedPathLocation: NSObject { +@objc(BezierKitPathPosition) public class IndexedPathLocation: NSObject { fileprivate let componentIndex: Int fileprivate let elementIndex: Int fileprivate let t: CGFloat diff --git a/BezierKit/Library/PathComponent.swift b/BezierKit/Library/PathComponent.swift new file mode 100644 index 00000000..8186a3dd --- /dev/null +++ b/BezierKit/Library/PathComponent.swift @@ -0,0 +1,251 @@ +// +// PathComponent.swift +// BezierKit +// +// Created by Holmes Futrell on 11/23/16. +// Copyright © 2016 Holmes Futrell. All rights reserved. +// + +import CoreGraphics +import Foundation + +#if os(macOS) +private extension NSValue { // annoying but MacOS (unlike iOS) doesn't have NSValue.cgPointValue available + var cgPointValue: CGPoint { + let pointValue: NSPoint = self.pointValue + return CGPoint(x: pointValue.x, y: pointValue.y) + } + convenience init(cgPoint: CGPoint) { + self.init(point: NSPoint(x: cgPoint.x, y: cgPoint.y)) + } +} +#endif + +public final class PathComponent: NSObject, NSCoding { + + public let curves: [BezierCurve] + + internal lazy var bvh: BVHNode = BVHNode(objects: curves) + + public lazy var cgPath: CGPath = { + let mutablePath = CGMutablePath() + guard curves.count > 0 else { + return mutablePath.copy()! + } + mutablePath.move(to: curves[0].startingPoint) + for curve in self.curves { + switch curve { + case let line as LineSegment: + mutablePath.addLine(to: line.endingPoint) + case let quadCurve as QuadraticBezierCurve: + mutablePath.addQuadCurve(to: quadCurve.p2, control: quadCurve.p1) + case let cubicCurve as CubicBezierCurve: + mutablePath.addCurve(to: cubicCurve.p3, control1: cubicCurve.p1, control2: cubicCurve.p2) + default: + fatalError("CGPath does not support curve type (\(type(of: curve))") + } + } + mutablePath.closeSubpath() + return mutablePath.copy()! + }() + + internal init(curves: [BezierCurve]) { + self.curves = curves + } + + public var length: CGFloat { + return self.curves.reduce(0.0) { $0 + $1.length() } + } + + public var boundingBox: BoundingBox { + return self.bvh.boundingBox + } + + public func offset(distance d: CGFloat) -> PathComponent { + return PathComponent(curves: self.curves.reduce([]) { + $0 + $1.offset(distance: d) + }) + } + + public func pointIsWithinDistanceOfBoundary(point p: CGPoint, distance d: CGFloat) -> Bool { + var found = false + self.bvh.visit { node, _ in + let boundingBox = node.boundingBox + if boundingBox.upperBoundOfDistance(to: p) <= d { + found = true + } + else if case let .leaf(object, _) = node.nodeType { + let curve = object as! BezierCurve + if distance(p, curve.project(point: p)) < d { + found = true + } + } + return !found && node.boundingBox.lowerBoundOfDistance(to: p) <= d + } + return found + } + + public func intersects(_ other: PathComponent, threshold: CGFloat = BezierKit.defaultIntersectionThreshold) -> [PathComponentIntersection] { + precondition(other !== self, "use intersects(threshold:) for self intersection testing.") + var intersections: [PathComponentIntersection] = [] + self.bvh.intersects(node: other.bvh) { o1, o2, i1, i2 in + let c1 = o1 as! BezierCurve + let c2 = o2 as! BezierCurve + let elementIntersections = c1.intersects(curve: c2, threshold: threshold) + let pathComponentIntersections = elementIntersections.map { (i: Intersection) -> PathComponentIntersection in + let i1 = IndexedPathComponentLocation(elementIndex: i1, t: i.t1) + let i2 = IndexedPathComponentLocation(elementIndex: i2, t: i.t2) + return PathComponentIntersection(indexedComponentLocation1: i1, indexedComponentLocation2: i2) + } + intersections += pathComponentIntersections + } + return intersections + } + + public func intersects(threshold: CGFloat = BezierKit.defaultIntersectionThreshold) -> [PathComponentIntersection] { + var intersections: [PathComponentIntersection] = [] + self.bvh.intersects(node: self.bvh) { o1, o2, i1, i2 in + let c1 = o1 as! BezierCurve + let c2 = o2 as! BezierCurve + var elementIntersections: [Intersection] = [] + if i1 == i2 { + // we are intersecting a path element against itself + if let c = c1 as? CubicBezierCurve { + elementIntersections = c.intersects(threshold: threshold) + } + } + else { + // 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 + } + return true + } + } + intersections += elementIntersections.map { + return PathComponentIntersection(indexedComponentLocation1: IndexedPathComponentLocation(elementIndex: i1, t: $0.t1), + indexedComponentLocation2: IndexedPathComponentLocation(elementIndex: i2, t: $0.t2)) + } + } + return intersections + } + + // MARK: - NSCoding + // (cannot be put in extension because init?(coder:) is a designated initializer) + + public func encode(with aCoder: NSCoder) { + let values: [[NSValue]] = self.curves.map { (curve: BezierCurve) -> [NSValue] in + return curve.points.map { return NSValue(cgPoint: $0) } + } + aCoder.encode(values) + } + + required public init?(coder aDecoder: NSCoder) { + guard let curveData = aDecoder.decodeObject() as? [[NSValue]] else { + return nil + } + self.curves = curveData.map { values in + createCurve(from: values.map { $0.cgPointValue })! + } + } + + // MARK: - + + override public func isEqual(_ object: Any?) -> Bool { + // override is needed because NSObject implementation of isEqual(_:) uses pointer equality + guard let otherPathComponent = object as? PathComponent else { + return false + } + guard self.curves.count == otherPathComponent.curves.count else { + return false + } + for i in 0.. [IndexedPathComponentLocation] { + let lineBoundingBox = line.boundingBox + var results: [IndexedPathComponentLocation] = [] + self.bvh.visit { (node: BVHNode, depth: Int) in + if case let .leaf(object, elementIndex) = node.nodeType { + let curve = object as! BezierCurve + results += curve.intersects(line: line).map { + return IndexedPathComponentLocation(elementIndex: elementIndex, t: $0.t1) + } + } + // TODO: better line box intersection + return node.boundingBox.overlaps(lineBoundingBox) + } + return results + } + + public func point(at location: IndexedPathComponentLocation) -> CGPoint { + return self.curves[location.elementIndex].compute(location.t) + } + + internal func windingCount(at point: CGPoint) -> Int { + // TODO: assumes element.normal() is always defined, which unfortunately it's not (eg degenerate curves as points, cusps, zero derivatives at the end of curves) + let line = LineSegment(p0: point, p1: CGPoint(x: self.boundingBox.min.x - self.boundingBox.size.x, y: point.y)) // horizontal line from point out of bounding box + let delta = line.p1 - line.p0 + let intersections = self.intersects(line: line) + var windingCount = 0 + intersections.forEach { + let element = self.element(at: $0) + let t = $0.t + assert(element.derivative($0.t).length > 1.0e-3, "possible NaN normal vector. Possible data for unit test?") + let dotProduct = delta.dot(element.normal(t)) + if dotProduct < 0 { + if t != 0 { + windingCount -= 1 + } + } + else if dotProduct > 0 { + if t != 1 { + windingCount += 1 + } + } + } + return windingCount + } + + private func element(at location: IndexedPathComponentLocation) -> BezierCurve { + return self.curves[location.elementIndex] + } + + public func contains(_ point: CGPoint, using rule: PathFillRule = .winding) -> Bool { + let windingCount = self.windingCount(at: point) + return windingCountImpliesContainment(windingCount, using: rule) + } + +} + +extension PathComponent: Transformable { + public func copy(using t: CGAffineTransform) -> PathComponent { + return PathComponent(curves: self.curves.map { $0.copy(using: t)} ) + } +} + +extension PathComponent: Reversible { + public func reversed() -> PathComponent { + return PathComponent(curves: self.curves.reversed().map({$0.reversed()})) + } +} + +public struct IndexedPathComponentLocation { + let elementIndex: Int + let t: CGFloat +} + +public struct PathComponentIntersection { + let indexedComponentLocation1, indexedComponentLocation2: IndexedPathComponentLocation +} diff --git a/BezierKit/Library/PolyBezier.swift b/BezierKit/Library/PolyBezier.swift deleted file mode 100644 index 845583db..00000000 --- a/BezierKit/Library/PolyBezier.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// PolyBezier.swift -// BezierKit -// -// Created by Holmes Futrell on 11/23/16. -// Copyright © 2016 Holmes Futrell. All rights reserved. -// - -import CoreGraphics -import Foundation - -#if os(macOS) -private extension NSValue { // annoying but MacOS (unlike iOS) doesn't have NSValue.cgPointValue available - var cgPointValue: CGPoint { - let pointValue: NSPoint = self.pointValue - return CGPoint(x: pointValue.x, y: pointValue.y) - } - convenience init(cgPoint: CGPoint) { - self.init(point: NSPoint(x: cgPoint.x, y: cgPoint.y)) - } -} -#endif - -public final class PolyBezier: NSObject, NSCoding { - - public let curves: [BezierCurve] - - internal lazy var bvh: BVHNode = BVHNode(objects: curves) - - public lazy var cgPath: CGPath = { - let mutablePath = CGMutablePath() - guard curves.count > 0 else { - return mutablePath.copy()! - } - mutablePath.move(to: curves[0].startingPoint) - for curve in self.curves { - switch curve { - case let line as LineSegment: - mutablePath.addLine(to: line.endingPoint) - case let quadCurve as QuadraticBezierCurve: - mutablePath.addQuadCurve(to: quadCurve.p2, control: quadCurve.p1) - case let cubicCurve as CubicBezierCurve: - mutablePath.addCurve(to: cubicCurve.p3, control1: cubicCurve.p1, control2: cubicCurve.p2) - default: - fatalError("CGPath does not support curve type (\(type(of: curve))") - } - } - mutablePath.closeSubpath() - return mutablePath.copy()! - }() - - internal init(curves: [BezierCurve]) { - self.curves = curves - } - - public var length: CGFloat { - return self.curves.reduce(0.0) { $0 + $1.length() } - } - - public var boundingBox: BoundingBox { - return self.bvh.boundingBox - } - - public func offset(distance d: CGFloat) -> PolyBezier { - return PolyBezier(curves: self.curves.reduce([]) { - $0 + $1.offset(distance: d) - }) - } - - public func pointIsWithinDistanceOfBoundary(point p: CGPoint, distance d: CGFloat) -> Bool { - var found = false - self.bvh.visit { node, _ in - let boundingBox = node.boundingBox - if boundingBox.upperBoundOfDistance(to: p) <= d { - found = true - } - else if case let .leaf(object, _) = node.nodeType { - let curve = object as! BezierCurve - if distance(p, curve.project(point: p)) < d { - found = true - } - } - return !found && node.boundingBox.lowerBoundOfDistance(to: p) <= d - } - return found - } - - public func intersects(_ other: PolyBezier, threshold: CGFloat = BezierKit.defaultIntersectionThreshold) -> [CGPoint] { - var intersections: [CGPoint] = [] - self.bvh.intersects(node: other.bvh) { o1, o2, i1, i2 in - let c1 = o1 as! BezierCurve - let c2 = o2 as! BezierCurve - intersections += c1.intersects(curve: c2, threshold: threshold).map { c1.compute($0.t1) } - } - return intersections - } - - // MARK: - NSCoding - // (cannot be put in extension because init?(coder:) is a designated initializer) - - public func encode(with aCoder: NSCoder) { - let values: [[NSValue]] = self.curves.map { (curve: BezierCurve) -> [NSValue] in - return curve.points.map { return NSValue(cgPoint: $0) } - } - aCoder.encode(values) - } - - required public init?(coder aDecoder: NSCoder) { - guard let curveData = aDecoder.decodeObject() as? [[NSValue]] else { - return nil - } - self.curves = curveData.map { values in - createCurve(from: values.map { $0.cgPointValue })! - } - } - - // MARK: - - - override public func isEqual(_ object: Any?) -> Bool { - // override is needed because NSObject implementation of isEqual(_:) uses pointer equality - guard let otherPolyBezier = object as? PolyBezier else { - return false - } - guard self.curves.count == otherPolyBezier.curves.count else { - return false - } - for i in 0.. PolyBezier { - return PolyBezier(curves: self.curves.map { $0.copy(using: t)} ) - } -} - -extension PolyBezier: Reversible { - public func reversed() -> PolyBezier { - return PolyBezier(curves: self.curves.reversed().map({$0.reversed()})) - } -} diff --git a/BezierKit/Library/Utils.swift b/BezierKit/Library/Utils.swift index fcd50060..67abdff6 100644 --- a/BezierKit/Library/Utils.swift +++ b/BezierKit/Library/Utils.swift @@ -311,6 +311,11 @@ internal class Utils { return result } + static func mod(_ a: Int, _ n: Int) -> Int { + precondition(n > 0, "modulus must be positive") + let r = a % n + return r >= 0 ? r : r + n + } static func lerp(_ r: CGFloat, _ v1: CGPoint, _ v2: CGPoint) -> CGPoint { return v1 + r * (v2 - v1) diff --git a/BezierKit/MacDemos/Demos.swift b/BezierKit/MacDemos/Demos.swift index 621827bd..0ed2dab4 100644 --- a/BezierKit/MacDemos/Demos.swift +++ b/BezierKit/MacDemos/Demos.swift @@ -426,30 +426,42 @@ class Demos { assert(glyph1 != 0 && glyph2 != 0, "couldn't get glyphs") let cgPath1: CGPath = CTFontCreatePathForGlyph(font, glyph1, nil)! - let path1 = Path(cgPath: cgPath1.copy(using: &translate)!) - - - - for s in path1.subpaths[1..<2] { - Draw.drawPolyBezier(context, polyBezier: s) - } - -// context.addPath(path1.cgPath) - Draw.setColor(context, color: Draw.red) - context.strokePath() + var path1 = Path(cgPath: cgPath1.copy(using: &translate)!) + path1 = Path(subpaths: [path1.subpaths[1]]) if let mouse = demoState.lastInputLocation { + + var translation = CGAffineTransform.init(translationX: mouse.x, y: mouse.y) let cgPath2: CGPath = CTFontCreatePathForGlyph(font, glyph2, &translation)! let path2 = Path(cgPath: cgPath2) - context.addPath(path2.cgPath) - Draw.setColor(context, color: Draw.black) - context.strokePath() - for intersection in path1.intersects(path: path2) { - Draw.drawPoint(context, origin: intersection) +// for intersection in path1.intersects(path: path2) { +// Draw.drawPoint(context, origin: intersection) +// } + +// var first: Vertex = augmentedGraph.v1 +// var v = first +// repeat { +// Draw.setColor(context, color: v.isIntersection ? Draw.blue : Draw.black) +// if v.isIntersection { +// if v.intersectionInfo.isEntry { +// Draw.setColor(context, color: Draw.green) +// } +// if v.intersectionInfo.isExit { +// Draw.setColor(context, color:Draw.red) +// } +// } +// Draw.drawPoint(context, origin: v.location) +// v = v.next +// } while v !== first + + let subtracted = path1.intersecting(path2) + subtracted.subpaths.forEach { + Draw.drawPathComponent(context, pathComponent: $0) } + } }) static let demo24 = Demo(title: "BVH", @@ -476,7 +488,7 @@ class Demos { let path = Path(cgPath: mutablePath) for s in path.subpaths { - Draw.drawPolyBezier(context, polyBezier: s, offset: CGPoint(x: 100.0, y: 100.0), includeBoundingVolumeHierarchy: true) + Draw.drawPathComponent(context, pathComponent: s, offset: CGPoint(x: 100.0, y: 100.0), includeBoundingVolumeHierarchy: true) } diff --git a/README.md b/README.md index 63fe59cd..3fa1b4a2 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.7' + pod 'BezierKit', '>= 0.1.8' end ```