Skip to content

Commit 6c3b7b2

Browse files
jneemKeavon
andauthored
Simplify Bezier-rs using poly-cool as the root finder (#3031)
* Convert tangents_to_point * Convert normals_to_point * Port over roots and project * Remove symmetrical_basis * Code style nits --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent bdc029c commit 6c3b7b2

File tree

7 files changed

+55
-641
lines changed

7 files changed

+55
-641
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
165165
tracing = "0.1.41"
166166
rfd = "0.15.4"
167167
open = "5.3.2"
168+
poly-cool = "0.2.0"
168169

169170
[profile.dev]
170171
opt-level = 1

libraries/bezier-rs/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@ homepage = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/bez
1313
repository = "https://github.com/GraphiteEditor/Graphite/tree/master/libraries/bezier-rs"
1414
documentation = "https://graphite.rs/libraries/bezier-rs/"
1515

16+
[features]
17+
std = ["glam/std"]
18+
1619
[dependencies]
1720
# Required dependencies
1821
glam = { workspace = true }
22+
poly-cool = { workspace = true }
1923

2024
# Optional local dependencies
2125
dyn-any = { version = "0.3.0", path = "../dyn-any", optional = true }

libraries/bezier-rs/src/bezier/lookup.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,15 +250,14 @@ impl Bezier {
250250
/// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point.
251251
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/project/solo" title="Project Demo"></iframe>
252252
pub fn project(&self, point: DVec2) -> f64 {
253-
let sbasis = crate::symmetrical_basis::to_symmetrical_basis_pair(*self);
254-
let derivative = sbasis.derivative();
255-
let dd = (sbasis - point).dot(&derivative);
256-
let roots = dd.roots();
253+
// The points at which the line from us to `point` is perpendicular
254+
// to our curve are the critical points of the distance function.
255+
let critical = self.normals_to_point(point);
257256

258257
let mut closest = 0.;
259258
let mut min_dist_squared = self.evaluate(TValue::Parametric(0.)).distance_squared(point);
260259

261-
for time in roots {
260+
for time in critical {
262261
let distance = self.evaluate(TValue::Parametric(time)).distance_squared(point);
263262
if distance < min_dist_squared {
264263
closest = time;
@@ -354,7 +353,8 @@ mod tests {
354353
assert_eq!(bezier1.project(DVec2::new(100., 100.)), 1.);
355354

356355
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
357-
assert_eq!(bezier2.project(DVec2::new(100., 0.)), 0.);
356+
assert_eq!(bezier2.project(DVec2::new(99.99, 0.)), 0.);
357+
assert!((bezier2.project(DVec2::new(-50., 150.)) - 0.5).abs() <= 1e-8);
358358

359359
let bezier3 = Bezier::from_cubic_coordinates(-50., -50., -50., -50., 50., -50., 50., -50.);
360360
assert_eq!(DVec2::new(0., -50.), bezier3.evaluate(TValue::Parametric(bezier3.project(DVec2::new(0., -50.)))));

libraries/bezier-rs/src/bezier/solvers.rs

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use super::*;
22
use crate::polynomial::Polynomial;
33
use crate::utils::{TValue, solve_cubic, solve_quadratic};
4-
use crate::{SymmetricalBasis, to_symmetrical_basis_pair};
54
use glam::DMat2;
65
use std::ops::Range;
76

@@ -10,8 +9,10 @@ impl Bezier {
109
/// Get roots as [[x], [y]]
1110
#[must_use]
1211
pub fn roots(self) -> [Vec<f64>; 2] {
13-
let s_basis = to_symmetrical_basis_pair(self);
14-
[s_basis.x.roots(), s_basis.y.roots()]
12+
let (x, y) = self.parametric_polynomial();
13+
let x = poly_cool::Poly::new(x.coefficients().iter().copied());
14+
let y = poly_cool::Poly::new(y.coefficients().iter().copied());
15+
[x.roots_between(0., 1., 1e-8), y.roots_between(0., 1., 1e-8)]
1516
}
1617

1718
/// Returns a list of lists of points representing the De Casteljau points for all iterations at the point `t` along the curve using De Casteljau's algorithm.
@@ -105,10 +106,21 @@ impl Bezier {
105106
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/tangents-to-point/solo" title="Tangents to Point Demo"></iframe>
106107
#[must_use]
107108
pub fn tangents_to_point(self, point: DVec2) -> Vec<f64> {
108-
let sbasis: crate::SymmetricalBasisPair = to_symmetrical_basis_pair(self);
109-
let derivative = sbasis.derivative();
110-
let cross = (sbasis - point).cross(&derivative);
111-
SymmetricalBasis::roots(&cross)
109+
// We solve deriv(t) * (self(t) - point) = 0. In principle, this is a quintic.
110+
// In fact, the highest-order term cancels out so it's at most a quartic.
111+
let (mut x, mut y) = self.parametric_polynomial();
112+
let x = x.coefficients_mut();
113+
let y = y.coefficients_mut();
114+
x[0] -= point.x;
115+
y[0] -= point.y;
116+
let poly = poly_cool::Poly::new([
117+
x[0] * y[1] - y[0] * x[1],
118+
2.0 * (x[0] * y[2] - y[0] * x[2]),
119+
x[2] * y[1] - y[2] * x[1] + 2.0 * (x[1] * y[2] - y[1] * x[2]) + 3.0 * (x[0] * y[3] - y[0] * x[3]),
120+
x[3] * y[1] - y[3] * x[1] + 3.0 * (x[1] * y[3] - y[1] * x[3]),
121+
2.0 * (x[3] * y[2] - y[3] * x[2]) + 3.0 * (x[2] * y[3] - y[2] * x[3]),
122+
]);
123+
poly.roots_between(0.0, 1.0, 1e-8)
112124
}
113125

114126
/// Returns a normalized unit vector representing the direction of the normal at the point `t` along the curve.
@@ -121,10 +133,21 @@ impl Bezier {
121133
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/normals-to-point/solo" title="Normals to Point Demo"></iframe>
122134
#[must_use]
123135
pub fn normals_to_point(self, point: DVec2) -> Vec<f64> {
124-
let sbasis = to_symmetrical_basis_pair(self);
125-
let derivative = sbasis.derivative();
126-
let cross = (sbasis - point).dot(&derivative);
127-
SymmetricalBasis::roots(&cross)
136+
// We solve deriv(t) dot (self(t) - point) = 0.
137+
let (mut x, mut y) = self.parametric_polynomial();
138+
let x = x.coefficients_mut();
139+
let y = y.coefficients_mut();
140+
x[0] -= point.x;
141+
y[0] -= point.y;
142+
let poly = poly_cool::Poly::new([
143+
x[0] * x[1] + y[0] * y[1],
144+
x[1] * x[1] + y[1] * y[1] + 2. * (x[0] * x[2] + y[0] * y[2]),
145+
3. * (x[2] * x[1] + y[2] * y[1]) + 3. * (x[0] * x[3] + y[0] * y[3]),
146+
4. * (x[3] * x[1] + y[3] * y[1]) + 2. * (x[2] * x[2] + y[2] * y[2]),
147+
5. * (x[3] * x[2] + y[3] * y[2]),
148+
3. * (x[3] * x[3] + y[3] * y[3]),
149+
]);
150+
poly.roots_between(0., 1., 1e-8)
128151
}
129152

130153
/// Returns the curvature, a scalar value for the derivative at the point `t` along the curve.

libraries/bezier-rs/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ mod consts;
88
mod poisson_disk;
99
mod polynomial;
1010
mod subpath;
11-
mod symmetrical_basis;
1211
mod utils;
1312

1413
pub use bezier::*;
1514
pub use subpath::*;
16-
pub use symmetrical_basis::*;
1715
pub use utils::{Cap, Join, SubpathTValue, TValue, TValueType};

0 commit comments

Comments
 (0)