Skip to content

Commit

Permalink
Merge pull request #22 from hfutrell/0.1.14-release
Browse files Browse the repository at this point in the history
0.1.14 release
  • Loading branch information
hfutrell authored Dec 8, 2018
2 parents 1b8169b + c05402a commit 56a9a86
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 82 deletions.
2 changes: 1 addition & 1 deletion BezierKit.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions BezierKit/BezierKitTests/CubicBezierCurveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
65 changes: 54 additions & 11 deletions BezierKit/BezierKitTests/PathTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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])
Expand All @@ -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])
Expand All @@ -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
}
Expand All @@ -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() {
Expand All @@ -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)))
}
Expand All @@ -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)
Expand Down Expand Up @@ -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]))

Expand All @@ -483,15 +483,15 @@ class PathTests: XCTestCase {
cgPathAlt.addLines(between: Array(points[3..<points.count]) + Array(points[1...3]))
let pathAlt = Path(cgPath: cgPathAlt)

let resultAlt = pathAlt.crossingsRemoved()
let resultAlt = pathAlt.crossingsRemoved()!
XCTAssertEqual(resultAlt.subpaths.count, 1)
XCTAssertTrue(componentsEqualAsideFromElementOrdering(resultAlt.subpaths[0], expectedResult.subpaths[0]))
}

func testCrossingsRemovedNoCrossings() {
// a test which ensures that if a path has no crossings then crossingsRemoved does not modify it
let square = Path(cgPath: CGPath(ellipseIn: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0), transform: nil))
let result = square.crossingsRemoved()
let result = square.crossingsRemoved()!
XCTAssertEqual(result.subpaths.count, 1)
XCTAssertTrue(componentsEqualAsideFromElementOrdering(result.subpaths[0], square.subpaths[0]))
}
Expand All @@ -518,10 +518,53 @@ class PathTests: XCTestCase {
XCTAssertEqual(contour.windingCount(CGPoint(x: 0.5, y: 0.5)), -1) // winding count at center of one square region
XCTAssertEqual( contour.windingCount(CGPoint(x: 1.5, y: 1.5)), 1) // winding count at center of other square region

let crossingsRemoved = contour.crossingsRemoved()
let crossingsRemoved = contour.crossingsRemoved()!

XCTAssertEqual(crossingsRemoved.subpaths.count, 1)
XCTAssertTrue(componentsEqualAsideFromElementOrdering(crossingsRemoved.subpaths[0], contour.subpaths[0]))
}

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

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
// 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 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)
}

}
81 changes: 39 additions & 42 deletions BezierKit/Library/AugmentedGraph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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..<lists.count {
var hasCrossing = false
self.forEachVertexInComponent(atIndex: i) { v in
if v.isCrossing {
hasCrossing = true
}
}
if hasCrossing == false {
result.append(self.path.subpaths[i])
}
}
return result
}

fileprivate func markEntryExit(_ path: Path, useRelativeWinding: Bool = false) {
let fillRule: PathFillRule = useRelativeWinding ? .winding : .evenOdd
// let fillRule = PathFillRule.winding

for i in 0..<lists.count {

// determine winding counts relative to the first vertex
var relativeWindingCount = 0
self.forEachVertexInComponent(atIndex: i) { v in
Expand All @@ -145,11 +161,15 @@ 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)
// we used to use the derivative here but in important cases derivatives can be exactly tangent
// at intersections!
let smallNumber: CGFloat = 0.01

let n1 = v.intersectionInfo.neighbor!.emitPrevious().compute(smallNumber) - v.location
let n2 = v.intersectionInfo.neighbor!.emitNext().compute(smallNumber) - v.location

let v1 = previous.derivative(0)
let v2 = next.derivative(0)
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)
Expand Down Expand Up @@ -214,7 +234,6 @@ internal class PathLinkedListRepresentation {
}

// for each intersection, determine isEntry / isExit based on winding count
var hasCrossing: Bool = false
var windingCount: Int = initialWinding
self.forEachVertexInComponent(atIndex: i) { v in
guard v.isIntersection else {
Expand All @@ -229,15 +248,8 @@ internal class PathLinkedListRepresentation {
if useRelativeWinding {
isInside = windingCountImpliesContainment(windingCount, using: fillRule) && windingCountImpliesContainment(windingCount+1, using: fillRule)
}

v.intersectionInfo.isEntry = wasInside == false && isInside == true
v.intersectionInfo.isExit = wasInside == true && isInside == false
if v.intersectionInfo.isEntry || v.intersectionInfo.isExit {
hasCrossing = true
}
}
if !hasCrossing {
nonCrossingComponents.append(self.path.subpaths[i])
}
}
}
Expand Down Expand Up @@ -288,9 +300,6 @@ internal class AugmentedGraph {
private let path1: Path
private let path2: Path

private var nonCrossingComponents1: [PathComponent] = []
private var nonCrossingComponents2: [PathComponent] = []

internal init(path1: Path, path2: Path, intersections: [PathIntersection]) {

func intersectionVertexForPath(_ path: Path, at l: IndexedPathLocation) -> Vertex {
Expand All @@ -311,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)
}
}

Expand All @@ -330,15 +339,19 @@ internal class AugmentedGraph {
}
}

internal func booleanOperation(_ operation: BooleanPathOperation) -> Path {
// handle components that have no crossings
internal func booleanOperation(_ operation: BooleanPathOperation) -> Path? {

// 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 }
Expand All @@ -349,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) {
Expand All @@ -366,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 {
Expand All @@ -382,24 +392,11 @@ 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
}

unvisitedCrossings = unvisitedCrossings.filter { $0 !== v }

if !v.isCrossing {
print("consistency error detected -- bailing out. Needs debugging.")
v = v.intersectionInfo.neighbor! // jump back to avoid infinite loop
isOnFirstCurve = !isOnFirstCurve
return nil
}
} while v !== start
pathComponents.append(PathComponent(curves: curves))
}
// TODO: non-deterministic behavior from usage of Set when choosing starting vertex
return Path(subpaths: pathComponents)
}
}
Expand Down
2 changes: 1 addition & 1 deletion BezierKit/Library/BezierCurve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ extension BezierCurve {
*/
public func reduce() -> [Subcurve<Self>] {

// 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
Expand Down
Loading

0 comments on commit 56a9a86

Please sign in to comment.