|
| 1 | +import Foundation |
| 2 | + |
| 3 | +public enum Bezier { |
| 4 | + |
| 5 | + /// Calculate a point on the bezier curve passed in, specifically the point at parameter. |
| 6 | + /// We're using De Casteljau's algorithm, which not only calculates the point at parameter |
| 7 | + /// in a numerically stable way, it also computes the two resulting bezier curves that |
| 8 | + /// would be formed if the original were split at the parameter specified. |
| 9 | + /// |
| 10 | + /// See: http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/de-casteljau.html |
| 11 | + /// for an explaination of De Casteljau's algorithm. |
| 12 | + /// |
| 13 | + /// bezierPoints, leftCurve, rightCurve will have a length of degree + 1. |
| 14 | + /// degree is the order of the bezier path, which will be cubic (3) most of the time. |
| 15 | + static func splitBezier(_ bezierPoints: [Point], ofDegree degree: Int, at parameter: Real) -> (point: Point, leftCurve: [Point], rightCurve: [Point]) { |
| 16 | + // With this algorithm we start out with the points in the bezier path. |
| 17 | + var points = Array(bezierPoints[0...degree]) |
| 18 | + var leftArray = Array(repeating: Point.zero, count: degree + 1) |
| 19 | + var rightArray = Array(repeating: Point.zero, count: degree + 1) |
| 20 | + |
| 21 | + // If the caller is asking for the resulting bezier curves, start filling those in |
| 22 | + leftArray[0] = points[0] |
| 23 | + rightArray[degree] = points[degree] |
| 24 | + |
| 25 | + for k in 1...degree { |
| 26 | + for i in 0...(degree - k) { |
| 27 | + points[i].x = (1.0 - parameter) * points[i].x + parameter * points[i + 1].x |
| 28 | + points[i].y = (1.0 - parameter) * points[i].y + parameter * points[i + 1].y |
| 29 | + } |
| 30 | + leftArray[k] = points[0] |
| 31 | + rightArray[degree - k] = points[degree - k] |
| 32 | + } |
| 33 | + |
| 34 | + // The point in the curve at parameter ends up in points[0] |
| 35 | + return (point: points[0], leftCurve: leftArray, rightCurve: rightArray) |
| 36 | + } |
| 37 | + |
| 38 | + static func findRoots(for bezierPoints: [Point], ofDegree degree: Int) -> [Real] { |
| 39 | + var results = [Real]() |
| 40 | + findRoots(for: bezierPoints, ofDegree: degree, atDepth: 0, into: &results) |
| 41 | + return results |
| 42 | + } |
| 43 | + |
| 44 | + static func closestLocation(on bezierPoints: [Point], to point: Point) -> BezierCurveLocation { |
| 45 | + let relatedBezier = convertBezier(bezierPoints, relativeTo: point) |
| 46 | + |
| 47 | + let locations = [ |
| 48 | + BezierCurveLocation(parameter: 0, distance: bezierPoints[0].distance(to: point)), |
| 49 | + BezierCurveLocation(parameter: 1, distance: bezierPoints[3].distance(to: point)), |
| 50 | + ] + findRoots(for: relatedBezier, ofDegree: 5) |
| 51 | + .map { root in |
| 52 | + let split = splitBezier(bezierPoints, ofDegree: 3, at: root) |
| 53 | + return BezierCurveLocation(parameter: root, distance: split.point.distance(to: point)) |
| 54 | + } |
| 55 | + .sorted { $0.distance < $1.distance } |
| 56 | + |
| 57 | + return locations[0] |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +private extension Bezier { |
| 62 | + static func convertBezier(_ bezierPoints: [Point], relativeTo point: Point) -> [Point] { |
| 63 | + // c[i] in the paper |
| 64 | + let distanceFromPoint = [ |
| 65 | + bezierPoints[0] - point, |
| 66 | + bezierPoints[1] - point, |
| 67 | + bezierPoints[2] - point, |
| 68 | + bezierPoints[3] - point |
| 69 | + ] |
| 70 | + |
| 71 | + // d[i] in the paper |
| 72 | + let weightedDelta = [ |
| 73 | + (bezierPoints[1] - bezierPoints[0]) * 3, |
| 74 | + (bezierPoints[2] - bezierPoints[1]) * 3, |
| 75 | + (bezierPoints[3] - bezierPoints[2]) * 3 |
| 76 | + ] |
| 77 | + |
| 78 | + // Precompute the dot product of distanceFromPoint and weightedDelta in order to speed things up |
| 79 | + var precomputedTable: [[Real]] = [ |
| 80 | + [0, 0, 0, 0], |
| 81 | + [0, 0, 0, 0], |
| 82 | + [0, 0, 0, 0] |
| 83 | + ] |
| 84 | + for row in 0 ..< 3 { |
| 85 | + for column in 0 ..< 4 { |
| 86 | + precomputedTable[row][column] = weightedDelta[row].dotMultiply(distanceFromPoint[column]) |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + // Precompute some of the values to speed things up |
| 91 | + let z: [[Real]] = [ |
| 92 | + [1.0, 0.6, 0.3, 0.1], |
| 93 | + [0.4, 0.6, 0.6, 0.4], |
| 94 | + [0.1, 0.3, 0.6, 1.0] |
| 95 | + ] |
| 96 | + |
| 97 | + // create our output array |
| 98 | + var results = Array(repeating: Point.zero, count: 6) |
| 99 | + |
| 100 | + // Set the x values of the bezier points |
| 101 | + for i in 0 ..< 6 { |
| 102 | + results[i] = Point(x: Real(i) / 5.0, y: 0) |
| 103 | + } |
| 104 | + |
| 105 | + // Finally set the y values of the bezier points |
| 106 | + let n = 3 |
| 107 | + let m = n - 1 |
| 108 | + for k in 0...(n + m) { |
| 109 | + let lowerBound = max(0, k - m) |
| 110 | + let upperBound = min(k, n) |
| 111 | + for i in lowerBound...upperBound { |
| 112 | + let j = k - i |
| 113 | + results[i + j].y += Real(precomputedTable[j][i] * z[j][i]) |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + return results |
| 118 | + } |
| 119 | + |
| 120 | + static let findBezierRootsMaximumDepth = 64 |
| 121 | + |
| 122 | + static func crossings(_ bezierPoints: [Point], degree: Int) -> Int { |
| 123 | + var count = 0 |
| 124 | + var sign = bezierPoints[0].y.sign |
| 125 | + |
| 126 | + var previousSign = sign |
| 127 | + for i in 1...degree { |
| 128 | + sign = bezierPoints[i].y.sign |
| 129 | + if sign != previousSign { |
| 130 | + count += 1 |
| 131 | + } |
| 132 | + previousSign = sign |
| 133 | + } |
| 134 | + return count |
| 135 | + } |
| 136 | + |
| 137 | + static func isControlPolygonFlatEnough(_ bezierPoints: [Point], degree: Int, intersectionPoint: inout Point) -> Bool { |
| 138 | + // 2^-63 |
| 139 | + let findBezierRootsErrorThreshold = pow(Real(2), Real(-1 * (Bezier.findBezierRootsMaximumDepth - 1))) |
| 140 | + |
| 141 | + let line = NormalizedLine(point1: bezierPoints[0], point2: bezierPoints[degree]) |
| 142 | + |
| 143 | + // Find the bounds around the line |
| 144 | + var belowDistance = 0.0 |
| 145 | + var aboveDistance = 0.0 |
| 146 | + for i in 1..<degree { |
| 147 | + let distance = line.distance(to: bezierPoints[i]) |
| 148 | + if distance > aboveDistance { |
| 149 | + aboveDistance = distance |
| 150 | + } |
| 151 | + |
| 152 | + if distance < belowDistance { |
| 153 | + belowDistance = distance |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + let zeroLine = NormalizedLine(a: 0.0, b: 1.0, c: 0.0) |
| 158 | + let aboveLine = line.offset(-aboveDistance) |
| 159 | + let intersect1 = zeroLine.intersectionWith(aboveLine) |
| 160 | + |
| 161 | + let belowLine = line.offset(-belowDistance) |
| 162 | + let intersect2 = zeroLine.intersectionWith(belowLine) |
| 163 | + |
| 164 | + let error = max(intersect1.x, intersect2.x) - min(intersect1.x, intersect2.x) |
| 165 | + if error < findBezierRootsErrorThreshold { |
| 166 | + intersectionPoint = zeroLine.intersectionWith(line) |
| 167 | + return true |
| 168 | + } |
| 169 | + |
| 170 | + return false |
| 171 | + } |
| 172 | + |
| 173 | + static func findRoots(for bezierPoints: [Point], ofDegree degree: Int, atDepth depth: Int, into results: inout [Real]) { |
| 174 | + let crossingCount = crossings(bezierPoints, degree: degree) |
| 175 | + guard crossingCount != 0 else { |
| 176 | + return |
| 177 | + } |
| 178 | + |
| 179 | + if crossingCount == 1 { |
| 180 | + if depth >= findBezierRootsMaximumDepth { |
| 181 | + let root = bezierPoints[0].x + bezierPoints[degree].x / 2.0 |
| 182 | + results.append(root) |
| 183 | + return |
| 184 | + } |
| 185 | + var intersectionPoint = Point.zero |
| 186 | + if isControlPolygonFlatEnough(bezierPoints, degree: degree, intersectionPoint: &intersectionPoint) { |
| 187 | + results.append(intersectionPoint.x) |
| 188 | + return |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + // Subdivide and try again |
| 193 | + let splitResult = splitBezier(bezierPoints, ofDegree: degree, at: 0.5) |
| 194 | + findRoots(for: splitResult.leftCurve, ofDegree: degree, atDepth: depth + 1, into: &results) |
| 195 | + findRoots(for: splitResult.rightCurve, ofDegree: degree, atDepth: depth + 1, into: &results) |
| 196 | + } |
| 197 | +} |
0 commit comments