Skip to content

Commit

Permalink
Merge pull request #45 from hfutrell/winding-count-refactor
Browse files Browse the repository at this point in the history
Winding count refactor
  • Loading branch information
hfutrell authored Jun 4, 2019
2 parents 7bbe910 + 2674b6f commit 9cb2e51
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 45 deletions.
13 changes: 12 additions & 1 deletion BezierKit/BezierKitTests/LineSegmentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ class LineSegmentTests: XCTestCase {
XCTAssertEqual(l.compute(1.0), CGPoint(x: 1.0, y: 3.0))
}

func testComputeRealWordIssue() {
let s = CGPoint(x: 0.30901699437494745, y: 0.9510565162951535)
let e = CGPoint(x: 0.30901699437494723, y: -0.9510565162951536)
let l = LineSegment(p0: s, p1: e)
XCTAssertEqual(l.compute(0), s)
XCTAssertEqual(l.compute(1), e) // this failed in practice
}

func testLength() {
let l = LineSegment(p0: CGPoint(x: 1.0, y: 2.0), p1: CGPoint(x: 4.0, y: 6.0))
XCTAssertEqual(l.length(), 5.0)
Expand All @@ -95,7 +103,10 @@ class LineSegmentTests: XCTestCase {
func testExtrema() {
let l = LineSegment(p0: CGPoint(x: 1.0, y: 2.0), p1: CGPoint(x: 4.0, y: 6.0))
let (xyz, values) = l.extrema()
XCTAssertTrue(xyz.isEmpty)
XCTAssertEqual(xyz.count, 3)
XCTAssertTrue(xyz[0].isEmpty)
XCTAssertTrue(xyz[1].isEmpty)
XCTAssertTrue(xyz[2].isEmpty)
XCTAssertTrue(values.isEmpty)
}

Expand Down
3 changes: 0 additions & 3 deletions BezierKit/BezierKitTests/Path+DataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,6 @@ class PathDataTests: XCTestCase {
cgPath.move(to: CGPoint(x: 1, y: 2))
cgPath.move(to: CGPoint(x: 2, y: 3))
cgPath.move(to: CGPoint(x: 3, y: 4))

let what = Path(cgPath: cgPath)

XCTAssertTrue(pathHasEqualElementsToCGPath(Path(cgPath: cgPath), cgPath))
cgPath.addLine(to: CGPoint(x: 4, y: 5))
XCTAssertTrue(pathHasEqualElementsToCGPath(Path(cgPath: cgPath), cgPath))
Expand Down
4 changes: 2 additions & 2 deletions BezierKit/BezierKitTests/PathTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ class PathTests: XCTestCase {
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.280, y: 0.295)), -1)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.281, y: 0.295)), 0)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.279, y: 0.296)), 0)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.280, y: 0.2960002)), 1)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.280, y: 0.2960000)), -1)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.280, y: 0.2961)), 1)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.280, y: 0.2959)), -1)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.281, y: 0.296)), 0)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.279, y: 0.297)), 0)
XCTAssertEqual(path1.windingCount(CGPoint(x: 0.280, y: 0.297)), 1)
Expand Down
1 change: 1 addition & 0 deletions BezierKit/Library/CubicBezierCurve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public struct CubicBezierCurve: NonlinearBezierCurve, ArcApproximateable, Equata
}

public func split(from t1: CGFloat, to t2: CGFloat) -> CubicBezierCurve {
guard t1 != 0.0 || t2 != 1.0 else { return self }
let h0 = self.p0
let h1 = self.p1
let h2 = self.p2
Expand Down
15 changes: 9 additions & 6 deletions BezierKit/Library/LineSegment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,7 @@ public struct LineSegment: BezierCurve, Equatable {
}

public func split(from t1: CGFloat, to t2: CGFloat) -> LineSegment {
let p0 = self.p0
let p1 = self.p1
return LineSegment(p0: Utils.lerp(t1, p0, p1),
p1: Utils.lerp(t2, p0, p1))
return LineSegment(p0: self.compute(t1), p1: self.compute(t2))
}

public func split(at t: CGFloat) -> (left: LineSegment, right: LineSegment) {
Expand All @@ -84,7 +81,13 @@ public struct LineSegment: BezierCurve, Equatable {
}

public func compute(_ t: CGFloat) -> CGPoint {
return Utils.lerp(t, self.p0, self.p1)
if t == 0 {
return self.p0
} else if t == 1 {
return self.p1
} else {
return Utils.lerp(t, self.p0, self.p1)
}
}

// -- MARK: - overrides
Expand All @@ -94,7 +97,7 @@ public struct LineSegment: BezierCurve, Equatable {
}

public func extrema() -> (xyz: [[CGFloat]], values: [CGFloat] ) {
return (xyz: [], [])
return (xyz: [[],[],[]], [])
}

public func project(_ point: CGPoint) -> (point: CGPoint, t: CGFloat) {
Expand Down
152 changes: 119 additions & 33 deletions BezierKit/Library/PathComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,52 +371,138 @@ import Foundation

// MARK: -

internal func intersects(line: LineSegment) -> [IndexedPathComponentLocation] {
let lineBoundingBox = line.boundingBox
var results: [IndexedPathComponentLocation] = []
self.bvh.visit { node, _ in
if case let .leaf(elementIndex) = node.type {
results += PathComponent.intersectionsBetweenElementAndLine(elementIndex, line, self).map {
IndexedPathComponentLocation(elementIndex: elementIndex, t: $0.t1)
}
public func point(at location: IndexedPathComponentLocation) -> CGPoint {
return self.element(at: location.elementIndex).compute(location.t)
}

private static func xIntercept<A: BezierCurve>(curve: A, y: CGFloat) -> CGFloat {
let startingPoint = curve.startingPoint
let endingPoint = curve.endingPoint
guard y != curve.startingPoint.y else { return curve.startingPoint.x }
guard y != curve.endingPoint.y else { return curve.endingPoint.x }
let linearSolutionT = ( y - startingPoint.y ) / ( endingPoint.y - startingPoint.y )
let linearSolution = LineSegment(p0: startingPoint, p1: endingPoint).compute(linearSolutionT).x
if curve.order > 1 {
let line = LineSegment(p0: CGPoint(x: 0, y: y), p1: CGPoint(x: 1, y: y))
guard let t = Utils.roots(points: curve.points, line: line).first(where: { $0 >= 0.0 && $0 <= 1.0 }) else {
assertionFailure("roots failed. Good test data?")
return linearSolution
}
// TODO: better line box intersection
return node.boundingBox.overlaps(lineBoundingBox)
return curve.compute(CGFloat(t)).x
} else {
return linearSolution
}
return results
}

public func point(at location: IndexedPathComponentLocation) -> CGPoint {
return self.element(at: location.elementIndex).compute(location.t)

private func enumerateYMonotonicComponentsForQuadratic(at index: Int, callback: (_ curve: QuadraticBezierCurve) -> Void) {
let curve = self.quadratic(at: index)
let p0 = curve.p0
let p1 = curve.p1
let p2 = curve.p2
let d0 = p1.y - p0.y
let d1 = p2.y - p1.y
var last: CGFloat = 0.0
Utils.droots(d0, d1, callback: { t in
guard t > 0, t < 1 else { return }
callback(curve.split(from: last, to: t))
last = t
})
if last < 1.0 {
callback(curve.split(from: last, to: 1.0))
}
}

private func enumerateYMonotonicComponentsForCubic(at index: Int, callback: (_ curve: CubicBezierCurve) -> Void) {
let curve = self.cubic(at: index)
let p0 = curve.p0
let p1 = curve.p1
let p2 = curve.p2
let p3 = curve.p3
let d0 = p1.y - p0.y
let d1 = p2.y - p1.y
let d2 = p3.y - p2.y
var last: CGFloat = 0.0
Utils.droots(d0, d1, d2, callback: { t in
guard t > 0, t < 1 else { return }
callback(curve.split(from: last, to: t))
last = t
})
if last < 1.0 {
callback(curve.split(from: last, to: 1.0))
}
}

internal func windingCount(at point: CGPoint) -> Int {
guard self.isClosed, self.boundingBox.contains(point) else {
return 0
}
// TODO: it's frustrating that this winding count uses a different logic than the one in augmented graph
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.p0 - line.p1).normalize()
let intersections = self.intersects(line: line)
var windingCount = 0
intersections.forEach {
let element = self.element(at: $0.elementIndex)
let t = $0.t
let dotProduct = Double(delta.dot(element.normal(t)))

if (element.compute(t) - point).dot(delta) > 0 {
return
var windingCount: Int = 0
self.bvh.visit { node, _ in
let boundingBox = node.boundingBox
guard boundingBox.min.y <= point.y, boundingBox.max.y >= point.y else {
return false
}

if dotProduct < 0 {
if t != 0 || !intersections.contains(IndexedPathComponentLocation(elementIndex: Utils.mod($0.elementIndex-1, self.elementCount), t: 1.0)) {
windingCount -= 1
guard boundingBox.min.x <= point.x else {
return false
}
guard case let .leaf(elementIndex) = node.type else {
return true // recurse
}

func adjustment(_ y: CGFloat, _ startY: CGFloat, _ endY: CGFloat) -> Int {
if endY < y, y <= startY {
return 1
} else if startY < y, y <= endY {
return -1
} else {
return 0
}
} else if dotProduct > 0 {
if t != 1 || !intersections.contains(IndexedPathComponentLocation(elementIndex: Utils.mod($0.elementIndex+1, self.elementCount), t: 0.0)) {
windingCount += 1
}

let order = self.orders[elementIndex]

guard point.x <= boundingBox.max.x else {
// super-fast code-path
// x-coordinate of point outside bounding box
// we need only see if we cross up or down
let offset = self.offsets[elementIndex]
let startingPoint = self.points[offset]
let endingPoint = self.points[offset + order]
windingCount += adjustment(point.y, startingPoint.y, endingPoint.y)
return true
}

func windingCountIncrementer<A: BezierCurve>(_ curve: A) -> Int {
if curve.boundingBox.min.x > point.x { return 0 }
// we include the highest point and exclude the lowest point
// that ensures if the juncture between curves changes direction it's counted twice or not at all
// and if the juncture between curves does not change direction it's counted exactly once
let increment = adjustment(point.y, curve.startingPoint.y, curve.endingPoint.y)
guard increment != 0 else { return 0 }
if curve.boundingBox.max.x >= point.x {
// slowest path: must determine x intercept and test against it
let x = PathComponent.xIntercept(curve: curve, y: point.y)
guard point.x > x else { return 0 }
}
return increment
}
switch order {
case 0:
windingCount += 0
case 1:
windingCount += windingCountIncrementer(line(at: elementIndex))
case 2:
self.enumerateYMonotonicComponentsForQuadratic(at: elementIndex) {
windingCount += windingCountIncrementer($0)
}
case 3:
self.enumerateYMonotonicComponentsForCubic(at: elementIndex) {
windingCount += windingCountIncrementer($0)
}
default:
fatalError("unsupported")
}
return true
}
return windingCount
}
Expand Down
1 change: 1 addition & 0 deletions BezierKit/Library/QuadraticBezierCurve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public struct QuadraticBezierCurve: NonlinearBezierCurve, ArcApproximateable, Eq
}

public func split(from t1: CGFloat, to t2: CGFloat) -> QuadraticBezierCurve {
guard t1 != 0.0 || t2 != 1.0 else { return self }
let h0 = self.p0
let h1 = self.p1
let h2 = self.p2
Expand Down

0 comments on commit 9cb2e51

Please sign in to comment.