|
| 1 | +use glam::DVec2; |
| 2 | + |
| 3 | +/// Solve for the first handle of an open spline. (The opposite handle can be found by mirroring the result about the anchor.) |
| 4 | +pub fn solve_spline_first_handle_open(points: &[DVec2]) -> Vec<DVec2> { |
| 5 | + let len_points = points.len(); |
| 6 | + if len_points == 0 { |
| 7 | + return Vec::new(); |
| 8 | + } |
| 9 | + if len_points == 1 { |
| 10 | + return vec![points[0]]; |
| 11 | + } |
| 12 | + |
| 13 | + // Matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html). |
| 14 | + // Because the `a` coefficients are all 1, they need not be stored. |
| 15 | + // This algorithm does a variation of the above algorithm. |
| 16 | + // Instead of using the traditional cubic (a + bt + ct^2 + dt^3), we use the bezier cubic. |
| 17 | + |
| 18 | + let mut b = vec![DVec2::new(4., 4.); len_points]; |
| 19 | + b[0] = DVec2::new(2., 2.); |
| 20 | + b[len_points - 1] = DVec2::new(2., 2.); |
| 21 | + |
| 22 | + let mut c = vec![DVec2::new(1., 1.); len_points]; |
| 23 | + |
| 24 | + // 'd' is the the second point in a cubic bezier, which is what we solve for |
| 25 | + let mut d = vec![DVec2::ZERO; len_points]; |
| 26 | + |
| 27 | + d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y); |
| 28 | + d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y); |
| 29 | + for idx in 1..(len_points - 1) { |
| 30 | + d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y); |
| 31 | + } |
| 32 | + |
| 33 | + // Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm) |
| 34 | + // Now we do row operations to eliminate `a` coefficients. |
| 35 | + c[0] /= -b[0]; |
| 36 | + d[0] /= -b[0]; |
| 37 | + #[allow(clippy::assign_op_pattern)] |
| 38 | + for i in 1..len_points { |
| 39 | + b[i] += c[i - 1]; |
| 40 | + // For some reason this `+=` version makes the borrow checker mad: |
| 41 | + // d[i] += d[i-1] |
| 42 | + d[i] = d[i] + d[i - 1]; |
| 43 | + c[i] /= -b[i]; |
| 44 | + d[i] /= -b[i]; |
| 45 | + } |
| 46 | + |
| 47 | + // At this point b[i] == -a[i + 1] and a[i] == 0. |
| 48 | + // Now we do row operations to eliminate 'c' coefficients and solve. |
| 49 | + d[len_points - 1] *= -1.; |
| 50 | + #[allow(clippy::assign_op_pattern)] |
| 51 | + for i in (0..len_points - 1).rev() { |
| 52 | + d[i] = d[i] - (c[i] * d[i + 1]); |
| 53 | + d[i] *= -1.; // d[i] /= b[i] |
| 54 | + } |
| 55 | + |
| 56 | + d |
| 57 | +} |
| 58 | + |
| 59 | +/// Solve for the first handle of a closed spline. (The opposite handle can be found by mirroring the result about the anchor.) |
| 60 | +/// If called with fewer than 3 points, this function will return an empty result. |
| 61 | +pub fn solve_spline_first_handle_closed(points: &[DVec2]) -> Vec<DVec2> { |
| 62 | + let len_points = points.len(); |
| 63 | + if len_points < 3 { |
| 64 | + return Vec::new(); |
| 65 | + } |
| 66 | + |
| 67 | + // Matrix coefficients `a`, `b` and `c` (see https://mathworld.wolfram.com/CubicSpline.html). |
| 68 | + // We don't really need to allocate them but it keeps the maths understandable. |
| 69 | + let a = vec![DVec2::ONE; len_points]; |
| 70 | + let b = vec![DVec2::splat(4.); len_points]; |
| 71 | + let c = vec![DVec2::ONE; len_points]; |
| 72 | + |
| 73 | + let mut cmod = vec![DVec2::ZERO; len_points]; |
| 74 | + let mut u = vec![DVec2::ZERO; len_points]; |
| 75 | + |
| 76 | + // `x` is initially the output of the matrix multiplication, but is converted to the second value. |
| 77 | + let mut x = vec![DVec2::ZERO; len_points]; |
| 78 | + |
| 79 | + for (i, point) in x.iter_mut().enumerate() { |
| 80 | + let previous_i = i.checked_sub(1).unwrap_or(len_points - 1); |
| 81 | + let next_i = (i + 1) % len_points; |
| 82 | + *point = 3. * (points[next_i] - points[previous_i]); |
| 83 | + } |
| 84 | + |
| 85 | + // Solve using https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm#Variants (the variant using periodic boundary conditions). |
| 86 | + // This code below is based on the reference C language implementation provided in that section of the article. |
| 87 | + let alpha = a[0]; |
| 88 | + let beta = c[len_points - 1]; |
| 89 | + |
| 90 | + // Arbitrary, but chosen such that division by zero is avoided. |
| 91 | + let gamma = -b[0]; |
| 92 | + |
| 93 | + cmod[0] = alpha / (b[0] - gamma); |
| 94 | + u[0] = gamma / (b[0] - gamma); |
| 95 | + x[0] /= b[0] - gamma; |
| 96 | + |
| 97 | + // Handle from from `1` to `len_points - 2` (inclusive). |
| 98 | + for ix in 1..=(len_points - 2) { |
| 99 | + let m = 1.0 / (b[ix] - a[ix] * cmod[ix - 1]); |
| 100 | + cmod[ix] = c[ix] * m; |
| 101 | + u[ix] = (0.0 - a[ix] * u[ix - 1]) * m; |
| 102 | + x[ix] = (x[ix] - a[ix] * x[ix - 1]) * m; |
| 103 | + } |
| 104 | + |
| 105 | + // Handle `len_points - 1`. |
| 106 | + let m = 1.0 / (b[len_points - 1] - alpha * beta / gamma - beta * cmod[len_points - 2]); |
| 107 | + u[len_points - 1] = (alpha - a[len_points - 1] * u[len_points - 2]) * m; |
| 108 | + x[len_points - 1] = (x[len_points - 1] - a[len_points - 1] * x[len_points - 2]) * m; |
| 109 | + |
| 110 | + // Loop from `len_points - 2` to `0` (inclusive). |
| 111 | + for ix in (0..=(len_points - 2)).rev() { |
| 112 | + u[ix] = u[ix] - cmod[ix] * u[ix + 1]; |
| 113 | + x[ix] = x[ix] - cmod[ix] * x[ix + 1]; |
| 114 | + } |
| 115 | + |
| 116 | + let fact = (x[0] + x[len_points - 1] * beta / gamma) / (1.0 + u[0] + u[len_points - 1] * beta / gamma); |
| 117 | + |
| 118 | + for ix in 0..(len_points) { |
| 119 | + x[ix] -= fact * u[ix]; |
| 120 | + } |
| 121 | + |
| 122 | + let mut real = vec![DVec2::ZERO; len_points]; |
| 123 | + for i in 0..len_points { |
| 124 | + let previous = i.checked_sub(1).unwrap_or(len_points - 1); |
| 125 | + let next = (i + 1) % len_points; |
| 126 | + real[i] = x[previous] * a[next] + x[i] * b[i] + x[next] * c[i]; |
| 127 | + } |
| 128 | + |
| 129 | + // The matrix is now solved. |
| 130 | + |
| 131 | + // Since we have computed the derivative, work back to find the start handle. |
| 132 | + for i in 0..len_points { |
| 133 | + x[i] = (x[i] / 3.) + points[i]; |
| 134 | + } |
| 135 | + |
| 136 | + x |
| 137 | +} |
| 138 | + |
| 139 | +#[test] |
| 140 | +fn closed_spline() { |
| 141 | + use crate::vector::misc::{dvec2_to_point, point_to_dvec2}; |
| 142 | + use kurbo::{BezPath, ParamCurve, ParamCurveDeriv}; |
| 143 | + |
| 144 | + // These points are just chosen arbitrary |
| 145 | + let points = [DVec2::new(0., 0.), DVec2::new(0., 0.), DVec2::new(6., 5.), DVec2::new(7., 9.), DVec2::new(2., 3.)]; |
| 146 | + |
| 147 | + // List of first handle or second point in a cubic bezier curve. |
| 148 | + let first_handles = solve_spline_first_handle_closed(&points); |
| 149 | + |
| 150 | + // Construct the Subpath |
| 151 | + let mut bezpath = BezPath::new(); |
| 152 | + bezpath.move_to(dvec2_to_point(points[0])); |
| 153 | + |
| 154 | + for i in 0..first_handles.len() { |
| 155 | + let next_i = i + 1; |
| 156 | + let next_i = if next_i == first_handles.len() { 0 } else { next_i }; |
| 157 | + |
| 158 | + // First handle or second point of a cubic Bezier curve. |
| 159 | + let p1 = dvec2_to_point(first_handles[i]); |
| 160 | + // Second handle or third point of a cubic Bezier curve. |
| 161 | + let p2 = dvec2_to_point(2. * points[next_i] - first_handles[next_i]); |
| 162 | + // Endpoint or fourth point of a cubic Bezier curve. |
| 163 | + let p3 = dvec2_to_point(points[next_i]); |
| 164 | + |
| 165 | + bezpath.curve_to(p1, p2, p3); |
| 166 | + } |
| 167 | + |
| 168 | + // For each pair of bézier curves, ensure that the second derivative is continuous |
| 169 | + for (bézier_a, bézier_b) in bezpath.segments().zip(bezpath.segments().skip(1).chain(bezpath.segments().take(1))) { |
| 170 | + let derivative2_end_a = point_to_dvec2(bézier_a.to_cubic().deriv().eval(1.)); |
| 171 | + let derivative2_start_b = point_to_dvec2(bézier_b.to_cubic().deriv().eval(0.)); |
| 172 | + |
| 173 | + assert!( |
| 174 | + derivative2_end_a.abs_diff_eq(derivative2_start_b, 1e-10), |
| 175 | + "second derivative at the end of a {derivative2_end_a} is equal to the second derivative at the start of b {derivative2_start_b}" |
| 176 | + ); |
| 177 | + } |
| 178 | +} |
0 commit comments