Class-A surfacing 3/3: blend curves, curved-edge fillets, the G2 corner and continuity analysis (#1642)
Thanks @midasdf.
This commit is contained in:
1 parent
3126962cd4
commit
bbd4f29ebb
43 files changed
+4753
-132
No files matched your search
@@ -0,0 +1,53 @@
|
||||
//! Blend curves: one Bézier that joins two curves with a requested continuity order, by Hermite
|
||||
//! interpolation of each reference curve's position and derivatives at the joints. This is the
|
||||
//! geometry `sketch.blend_curve` builds; it knows nothing about sketches or the document.
|
||||
|
||||
use crate::{MAX_DERIVATIVE_ORDER, Vec2, profile::binom};
|
||||
|
||||
/// Control points of the Bézier of degree `2k + 1`, `k = d0.len()`, that starts at `p0` with
|
||||
/// derivatives `d0` and ends at `p1` with derivatives `d1`, all with respect to its own parameter
|
||||
/// on [0, 1]. The derivative vectors are the reference curves' at the joints, measured in their
|
||||
/// own parameters and oriented the way the blend runs through the joint, so the blend agrees with
|
||||
/// each reference through its k-th derivative (G1, G2, G3, G4 for k = 1…4).
|
||||
///
|
||||
/// The two ends fix exactly half of the controls each: a Bézier's j-th forward difference is
|
||||
/// `P⁽ʲ⁾(0)·(n − j)!/n!`, so the first `k + 1` controls follow from the start data and the last
|
||||
/// `k + 1` from the end data, which meet exactly at the middle control. `None` when the ends
|
||||
/// disagree on the order, the order is above [`MAX_DERIVATIVE_ORDER`], or a value is not finite.
|
||||
pub fn hermite_bezier(p0: Vec2, p1: Vec2, d0: &[Vec2], d1: &[Vec2]) -> Option<Vec<Vec2>> {
|
||||
let k = d0.len();
|
||||
if k != d1.len() || k > MAX_DERIVATIVE_ORDER || !p0.is_finite() || !p1.is_finite() || d0.iter().chain(d1).any(|d| !d.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let n = 2 * k + 1;
|
||||
let mut c = vec![Vec2::ZERO; n + 1];
|
||||
c[0] = p0;
|
||||
for j in 1..=k {
|
||||
// `Δʲp₀ = P⁽ʲ⁾(0)·(n − j)!/n!`, and solving it for `pⱼ` puts `(−1)^(j − i + 1)` on every
|
||||
// control already known.
|
||||
let scale = 1.0 / (0..j).fold(1.0, |acc, i| acc * (n - i) as f64);
|
||||
let mut value = d0[j - 1] * scale;
|
||||
for (i, p) in c.iter().take(j).enumerate() {
|
||||
value += *p * (odd_sign(j - i) * binom(j, i));
|
||||
}
|
||||
c[j] = value;
|
||||
}
|
||||
c[n] = p1;
|
||||
for j in 1..=k {
|
||||
// The same at the far end, reading the controls backwards: `Σᵢ(−1)ⁱ·C(j,i)·pₙ₋ᵢ` is
|
||||
// `P⁽ʲ⁾(1)·(n − j)!/n!`, and solving that for `pₙ₋ⱼ` puts `(−1)^(j + i + 1)` on every control
|
||||
// already known.
|
||||
let scale = 1.0 / (0..j).fold(1.0, |acc, i| acc * (n - i) as f64);
|
||||
let mut value = d1[j - 1] * (scale * odd_sign(j + 1));
|
||||
for (i, p) in c[n - j + 1..].iter().rev().enumerate() {
|
||||
value += *p * (odd_sign(j + i) * binom(j, i));
|
||||
}
|
||||
c[n - j] = value;
|
||||
}
|
||||
c.iter().all(|p| p.is_finite()).then_some(c)
|
||||
}
|
||||
|
||||
/// `(−1)^(m + 1)`: the sign an m-th difference expansion carries in a Bézier's endpoint identity.
|
||||
fn odd_sign(m: usize) -> f64 {
|
||||
if m % 2 == 1 { 1.0 } else { -1.0 }
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod arcfit;
|
||||
pub mod blend;
|
||||
pub mod brep;
|
||||
pub mod bspline;
|
||||
pub mod compare;
|
||||
@@ -23,9 +24,10 @@ pub mod tspline;
|
||||
mod vec;
|
||||
|
||||
pub use arcfit::MAX_FIT_PIECES;
|
||||
pub use blend::hermite_bezier;
|
||||
pub use mesh::{Aabb3, BoxedTriangle, InsideIndex, MassProperties, Mesh, MeshMeasure, find_close_pair};
|
||||
pub use plane::Plane;
|
||||
pub use profile::{Loop2, MAX_BEZIER_DEGREE, Region2, Seg2, bezier, regions_apart};
|
||||
pub use profile::{Loop2, MAX_BEZIER_CTRL, MAX_BEZIER_DEGREE, MAX_DERIVATIVE_ORDER, Region2, Seg2, bezier, regions_apart};
|
||||
pub use vec::{Vec2, Vec3};
|
||||
|
||||
/// Geometric tolerance for coincidence tests (mm).
|
||||
|
||||
+131
-10
@@ -38,26 +38,35 @@ pub enum Seg2 {
|
||||
b: Vec2,
|
||||
w: f64,
|
||||
},
|
||||
/// Bézier of degree `n` (4…7) over `p[0..=n]` (the rest unused): pieces of higher-degree
|
||||
/// splines.
|
||||
/// Bézier of degree `n` (4…[`MAX_BEZIER_DEGREE`]) over `p[0..=n]` (the rest unused): pieces of
|
||||
/// higher-degree splines.
|
||||
Bezier {
|
||||
n: u8,
|
||||
p: [Vec2; 8],
|
||||
p: [Vec2; MAX_BEZIER_CTRL],
|
||||
},
|
||||
}
|
||||
|
||||
/// Highest degree of [`Seg2::Bezier`].
|
||||
pub const MAX_BEZIER_DEGREE: usize = 7;
|
||||
/// Highest degree of [`Seg2::Bezier`]: a degree-9 piece is the single Bézier a G4 blend curve
|
||||
/// (`sketch.blend_curve` with `continuity: "g4"`) is made of.
|
||||
pub const MAX_BEZIER_DEGREE: usize = 9;
|
||||
|
||||
/// A Bézier segment over `pts` (2…8 points): a line, a cubic or a higher-degree piece. A
|
||||
/// quadratic becomes a conic of weight 1.
|
||||
/// Control points a [`Seg2::Bezier`] carries: its highest degree plus one (the unused tail is
|
||||
/// zero).
|
||||
pub const MAX_BEZIER_CTRL: usize = MAX_BEZIER_DEGREE + 1;
|
||||
|
||||
/// Highest derivative order [`Seg2::derivatives`] hands out, and therefore the highest
|
||||
/// continuity order a blend curve can be built for (4 = G4).
|
||||
pub const MAX_DERIVATIVE_ORDER: usize = 4;
|
||||
|
||||
/// A Bézier segment over `pts` (2…[`MAX_BEZIER_CTRL`] points): a line, a cubic or a higher-degree
|
||||
/// piece. A quadratic becomes a conic of weight 1.
|
||||
pub fn bezier(pts: &[Vec2]) -> Option<Seg2> {
|
||||
Some(match *pts {
|
||||
[a, b] => Seg2::Line { a, b },
|
||||
[a, apex, b] => Seg2::Conic { a, apex, b, w: 1.0 },
|
||||
[p0, p1, p2, p3] => Seg2::Cubic { p0, p1, p2, p3 },
|
||||
_ if pts.len() <= MAX_BEZIER_DEGREE + 1 => {
|
||||
let mut p = [Vec2::ZERO; 8];
|
||||
_ if (2..=MAX_BEZIER_CTRL).contains(&pts.len()) => {
|
||||
let mut p = [Vec2::ZERO; MAX_BEZIER_CTRL];
|
||||
for (d, s) in p.iter_mut().zip(pts) {
|
||||
*d = *s;
|
||||
}
|
||||
@@ -86,6 +95,38 @@ fn casteljau(c: &[Vec2], t: f64) -> (Vec2, Vec<Vec2>, Vec<Vec2>) {
|
||||
(left.last().copied().unwrap_or(Vec2::ZERO), left, right)
|
||||
}
|
||||
|
||||
/// Binomial coefficient `C(n, k)` for the small orders the derivative rules need (0 when
|
||||
/// `k > n`).
|
||||
pub(crate) fn binom(n: usize, k: usize) -> f64 {
|
||||
if k > n {
|
||||
return 0.0;
|
||||
}
|
||||
(0..k.min(n - k)).fold(1.0, |v, i| v * (n - i) as f64 / (i + 1) as f64)
|
||||
}
|
||||
|
||||
/// Derivatives 1…`order` of the Bézier over `c` (degree `c.len() - 1`) at `t`: the j-th forward
|
||||
/// difference of the control polygon, scaled by `n!/(n − j)!` and evaluated by de Casteljau.
|
||||
/// Exact in exact arithmetic, zero above the degree, `None` for non-finite input.
|
||||
fn bezier_derivatives(c: &[Vec2], t: f64, order: usize) -> Option<Vec<Vec2>> {
|
||||
if !t.is_finite() || c.is_empty() || c.iter().any(|p| !p.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
let n = c.len() - 1;
|
||||
let mut diffs = c.to_vec();
|
||||
let mut scale = 1.0;
|
||||
let mut out = Vec::with_capacity(order);
|
||||
for j in 1..=order {
|
||||
if diffs.len() < 2 {
|
||||
out.push(Vec2::ZERO);
|
||||
continue;
|
||||
}
|
||||
diffs = diffs.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
scale *= (n + 1 - j) as f64;
|
||||
out.push(casteljau(&diffs, t).0 * scale);
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Gauss–Legendre nodes and weights on [0, 1] (5 points).
|
||||
const GL5: [(f64, f64); 5] = [
|
||||
(0.046_910_077_030_668, 0.118_463_442_528_095),
|
||||
@@ -96,7 +137,7 @@ const GL5: [(f64, f64); 5] = [
|
||||
];
|
||||
|
||||
impl Seg2 {
|
||||
fn bez(n: u8, p: &[Vec2; 8]) -> &[Vec2] {
|
||||
fn bez(n: u8, p: &[Vec2; MAX_BEZIER_CTRL]) -> &[Vec2] {
|
||||
&p[..(n as usize).clamp(1, MAX_BEZIER_DEGREE) + 1]
|
||||
}
|
||||
/// Derivative with respect to the parameter at `t` (Béziers and conics; lines and arcs too).
|
||||
@@ -121,6 +162,86 @@ impl Seg2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Derivatives 1…`order` with respect to the segment's own parameter at `t`, exactly: a line
|
||||
/// and an arc from their analytic form (an arc's are the circular-motion recurrence, zero
|
||||
/// curvature terms included), a cubic and a higher-degree piece from the forward differences
|
||||
/// of their controls, a conic through the quotient rule of its rational form. A blend curve
|
||||
/// reads a reference curve's derivatives here to build a Hermite interpolation, so this is
|
||||
/// exact at both ends (`t` = 0 and 1) and `None` — never a wrong number — when `order` is
|
||||
/// above [`MAX_DERIVATIVE_ORDER`], `t` is not finite or a value is not finite, as with a
|
||||
/// degenerate rational piece.
|
||||
pub fn derivatives(&self, t: f64, order: usize) -> Option<Vec<Vec2>> {
|
||||
if !t.is_finite() || order > MAX_DERIVATIVE_ORDER {
|
||||
return None;
|
||||
}
|
||||
let mut d = match *self {
|
||||
Seg2::Line { a, b } => {
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let mut d = vec![b - a];
|
||||
d.resize(MAX_DERIVATIVE_ORDER, Vec2::ZERO);
|
||||
d
|
||||
}
|
||||
Seg2::Arc { center, radius, start, sweep } => {
|
||||
if !center.is_finite() || !radius.is_finite() || !start.is_finite() || !sweep.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Differentiating the radial direction twice returns its negative, so the
|
||||
// directions of the successive derivatives are tangential, radial, tangential…
|
||||
// with alternating signs, each order picking up one more factor of the sweep.
|
||||
let mut v = Vec2::from_angle(start + sweep * t).perp();
|
||||
let mut d = Vec::with_capacity(MAX_DERIVATIVE_ORDER);
|
||||
for j in 1..=MAX_DERIVATIVE_ORDER {
|
||||
d.push(v * (radius * sweep.powi(j as i32)));
|
||||
v = v.perp();
|
||||
}
|
||||
d
|
||||
}
|
||||
Seg2::Cubic { p0, p1, p2, p3 } => bezier_derivatives(&[p0, p1, p2, p3], t, MAX_DERIVATIVE_ORDER)?,
|
||||
Seg2::Bezier { n, ref p } => bezier_derivatives(Self::bez(n, p), t, MAX_DERIVATIVE_ORDER)?,
|
||||
Seg2::Conic { a, apex, b, w } => {
|
||||
if !a.is_finite() || !apex.is_finite() || !b.is_finite() || !w.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// The conic is `N(t) / d(t)` with `N` the quadratic through `a`, `w·apex`, `b` and
|
||||
// `d` its weight polynomial; both are evaluated here in the power basis (at the
|
||||
// ends `d` = 1, so a join is never singular). The quotient rule in its
|
||||
// derivative-of-a-product form gives the higher derivatives: the numerator of
|
||||
// `x⁽ʲ⁾` is `N⁽ʲ⁾` minus the terms that already use `d` and a lower `x`.
|
||||
let (n0, n1, n2) = (a, apex * (2.0 * w) - a * 2.0, a - apex * (2.0 * w) + b);
|
||||
let (e1, e2) = (2.0 * (w - 1.0), 2.0 * (1.0 - w));
|
||||
let quotient = |j: usize| match j {
|
||||
0 => n0 + n1 * t + n2 * (t * t),
|
||||
1 => n1 + n2 * (2.0 * t),
|
||||
2 => n2 * 2.0,
|
||||
_ => Vec2::ZERO,
|
||||
};
|
||||
let weight = |j: usize| match j {
|
||||
0 => 1.0 + e1 * t + e2 * (t * t),
|
||||
1 => e1 + e2 * (2.0 * t),
|
||||
2 => e2 * 2.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
let den = weight(0);
|
||||
if !(den.abs() > 1e-300) {
|
||||
return None;
|
||||
}
|
||||
let mut x = vec![quotient(0) / den];
|
||||
for j in 1..=MAX_DERIVATIVE_ORDER {
|
||||
let mut v = quotient(j);
|
||||
for i in 1..=j {
|
||||
v -= x[j - i] * (weight(i) * binom(j, i));
|
||||
}
|
||||
x.push(v / den);
|
||||
}
|
||||
x.remove(0);
|
||||
x
|
||||
}
|
||||
};
|
||||
d.truncate(order);
|
||||
d.iter().all(|q| q.is_finite()).then_some(d)
|
||||
}
|
||||
pub fn start(&self) -> Vec2 {
|
||||
match *self {
|
||||
Seg2::Line { a, .. } | Seg2::Conic { a, .. } => a,
|
||||
|
||||
@@ -560,3 +560,161 @@ mod close_pairs {
|
||||
assert!(budget < 10_000_000, "work was counted");
|
||||
}
|
||||
}
|
||||
|
||||
mod blend {
|
||||
use super::*;
|
||||
|
||||
/// `j!` for the small orders the Hermite and quotient identities use.
|
||||
fn fact(j: usize) -> f64 {
|
||||
(1..=j).fold(1.0, |a, b| a * b as f64)
|
||||
}
|
||||
|
||||
/// The derivatives 1…`order` a Bézier's control polygon carries at its end `at_end`: the j-th
|
||||
/// difference of the controls, scaled by `n!/(n − j)!`. This is the textbook endpoint identity
|
||||
/// of a Bézier, written here independently of the library's.
|
||||
fn control_derivatives(ctrl: &[Vec2], at_end: bool, order: usize) -> Vec<Vec2> {
|
||||
let c: Vec<Vec2> = if at_end { ctrl.iter().rev().copied().collect() } else { ctrl.to_vec() };
|
||||
let n = c.len() - 1;
|
||||
let (mut diffs, mut scale) = (c, 1.0);
|
||||
let mut out = Vec::new();
|
||||
for j in 1..=order {
|
||||
diffs = diffs.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
scale *= (n + 1 - j) as f64;
|
||||
out.push(diffs.first().copied().unwrap_or_default() * scale);
|
||||
}
|
||||
// The reversed controls describe `P(1 − s)`, so reversing again to read the original
|
||||
// parameterisation negates the odd orders.
|
||||
if at_end {
|
||||
for (i, v) in out.iter_mut().enumerate() {
|
||||
if i % 2 == 0 {
|
||||
*v = -*v;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hermite_bezier_meets_the_derivative_data_at_both_ends() {
|
||||
let (p0, p1) = (Vec2::new(1.0, 2.0), Vec2::new(-3.0, 4.5));
|
||||
let d0 = [Vec2::new(2.0, 1.0), Vec2::new(-0.5, 3.0), Vec2::new(1.5, 1.0), Vec2::new(0.25, -2.0)];
|
||||
let d1 = [Vec2::new(-1.0, 0.5), Vec2::new(2.0, -1.5), Vec2::new(-0.75, 0.5), Vec2::new(3.0, 1.25)];
|
||||
for k in 0..=MAX_DERIVATIVE_ORDER {
|
||||
let cps = hermite_bezier(p0, p1, &d0[..k], &d1[..k]).unwrap();
|
||||
assert_eq!(cps.len(), 2 * k + 2, "degree 2k+1 has 2k+2 controls");
|
||||
assert_eq!(cps[0], p0);
|
||||
assert_eq!(*cps.last().unwrap(), p1);
|
||||
for (got, want) in control_derivatives(&cps, false, k).iter().zip(&d0[..k]) {
|
||||
assert!(got.dist(*want) < 1e-11, "k={k} at t=0: {got:?} vs {want:?}");
|
||||
}
|
||||
for (got, want) in control_derivatives(&cps, true, k).iter().zip(&d1[..k]) {
|
||||
assert!(got.dist(*want) < 1e-11, "k={k} at t=1: {got:?} vs {want:?}");
|
||||
}
|
||||
// The piece is exactly the single Bézier `bezier` hands back, and its ends are the two
|
||||
// joints.
|
||||
let seg = bezier(&cps).unwrap();
|
||||
assert!(seg.start().dist(p0) < 1e-12 && seg.end().dist(p1) < 1e-12);
|
||||
for (got, want) in seg.derivatives(0.0, k).unwrap().iter().zip(&d0[..k]) {
|
||||
assert!(got.dist(*want) < 1e-10, "k={k} at t=0: {got:?} vs {want:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hermite_bezier_refuses_hostile_data() {
|
||||
let (p0, p1) = (Vec2::ZERO, Vec2::X);
|
||||
let d = [Vec2::Y, Vec2::X];
|
||||
assert!(hermite_bezier(p0, p1, &d, &d).is_some());
|
||||
assert!(hermite_bezier(p0, p1, &d, &d[..1]).is_none(), "the ends must agree on the order");
|
||||
assert!(hermite_bezier(p0, p1, &[Vec2::ZERO; 5], &[Vec2::ZERO; 5]).is_none(), "order above G4");
|
||||
assert!(hermite_bezier(Vec2::new(f64::NAN, 0.0), p1, &[], &[]).is_none());
|
||||
assert!(hermite_bezier(p0, p1, &[Vec2::new(f64::INFINITY, 0.0)], &[Vec2::Y]).is_none());
|
||||
assert!(hermite_bezier(p0, p1, &[], &[Vec2::Y]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seg_derivatives_are_exact_for_lines_and_polynomial_pieces() {
|
||||
let line = Seg2::Line { a: Vec2::new(-1.0, 2.0), b: Vec2::new(3.0, 5.0) };
|
||||
let d = line.derivatives(0.0, MAX_DERIVATIVE_ORDER).unwrap();
|
||||
assert!(d[0].dist(Vec2::new(4.0, 3.0)) < 1e-12);
|
||||
assert_eq!(d[1].len() + d[2].len() + d[3].len(), 0.0, "a line has no curvature");
|
||||
let cubic = Seg2::Cubic { p0: Vec2::new(0.0, 0.0), p1: Vec2::new(3.0, 9.0), p2: Vec2::new(7.0, -4.0), p3: Vec2::new(11.0, 2.0) };
|
||||
let ctrl = [Vec2::new(0.0, 0.0), Vec2::new(3.0, 9.0), Vec2::new(7.0, -4.0), Vec2::new(11.0, 2.0)];
|
||||
for (t, at_end) in [(0.0, false), (1.0, true)] {
|
||||
for (got, want) in cubic.derivatives(t, MAX_DERIVATIVE_ORDER).unwrap().iter().zip(control_derivatives(&ctrl, at_end, 4)) {
|
||||
assert!(got.dist(want) < 1e-9, "t={t}: {got:?} vs {want:?}");
|
||||
}
|
||||
}
|
||||
for t in [0.0, 0.35, 1.0] {
|
||||
assert!(cubic.derivatives(t, 1).unwrap()[0].dist(cubic.derivative(t)) < 1e-9, "t={t}");
|
||||
}
|
||||
// A degree-9 piece (the G4 blend's own shape) at both ends.
|
||||
let pts: Vec<Vec2> = (0..10).map(|i| Vec2::new(i as f64 * 0.7, (i as f64 * 1.3).sin())).collect();
|
||||
let seg = bezier(&pts).unwrap();
|
||||
assert!(matches!(seg, Seg2::Bezier { n: 9, .. }));
|
||||
for (t, at_end) in [(0.0, false), (1.0, true)] {
|
||||
for (got, want) in seg.derivatives(t, MAX_DERIVATIVE_ORDER).unwrap().iter().zip(control_derivatives(&pts, at_end, 4)) {
|
||||
assert!(got.dist(want) < 1e-9, "t={t}: {got:?} vs {want:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seg_derivatives_follow_the_circular_recurrence() {
|
||||
let arc = Seg2::Arc { center: Vec2::new(2.0, -1.0), radius: 5.0, start: 0.7, sweep: -1.9 };
|
||||
let Seg2::Arc { center, sweep, .. } = arc else { panic!("an arc") };
|
||||
for t in [0.0, 0.4, 1.0] {
|
||||
let d = arc.derivatives(t, MAX_DERIVATIVE_ORDER).unwrap();
|
||||
let p = arc.point_at(t);
|
||||
let sw2 = sweep * sweep;
|
||||
// On a circle every derivative follows from the second: P″ = −sw²(P − c), and the rest
|
||||
// differentiate that relation, so a line and an arc have zero curvature rates.
|
||||
assert!(d[1].dist((p - center) * -sw2) < 1e-9, "t={t}");
|
||||
assert!(d[2].dist(d[0] * -sw2) < 1e-9, "t={t}");
|
||||
assert!(d[3].dist(d[1] * -sw2) < 1e-9, "t={t}");
|
||||
assert!(d[0].dist(arc.derivative(t)) < 1e-9, "t={t}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seg_derivatives_of_a_conic_satisfy_the_quotient_identity() {
|
||||
let conic = Seg2::Conic { a: Vec2::new(-2.0, 0.0), apex: Vec2::new(0.0, 3.0), b: Vec2::new(4.0, 0.5), w: 0.7 };
|
||||
let Seg2::Conic { a, apex, b, w } = conic else { panic!("a conic") };
|
||||
// The rational quadratic's numerator and weight polynomials, straight from their Bernstein
|
||||
// form, and their power coefficients at `t` by differences (exact for a quadratic).
|
||||
let n_at = |x: f64| a * ((1.0 - x) * (1.0 - x)) + apex * (2.0 * w * x * (1.0 - x)) + b * (x * x);
|
||||
let d_at = |x: f64| (1.0 - x) * (1.0 - x) + 2.0 * w * x * (1.0 - x) + x * x;
|
||||
for t in [0.0, 0.25, 1.0] {
|
||||
let h = 1e-3;
|
||||
// Taylor coefficients at `t` (differences are exact for a quadratic): f, f′/1!, f″/2!.
|
||||
let coef_v = |f: &dyn Fn(f64) -> Vec2| [f(t), (f(t + h) - f(t - h)) / (2.0 * h), (f(t + h) - f(t) * 2.0 + f(t - h)) / (2.0 * h * h)];
|
||||
let coef_s = |f: &dyn Fn(f64) -> f64| [f(t), (f(t + h) - f(t - h)) / (2.0 * h), (f(t + h) - f(t) * 2.0 + f(t - h)) / (2.0 * h * h)];
|
||||
let n = coef_v(&n_at);
|
||||
let d = coef_s(&d_at);
|
||||
let got = conic.derivatives(t, MAX_DERIVATIVE_ORDER).unwrap();
|
||||
// Taylor coefficients of P: P(t) and P⁽ʲ⁾(t)/j!. Leibniz on N = P·D then reads
|
||||
// Σ xᵢ·dⱼ₋ᵢ = nⱼ at every order, which the quotient rule must reproduce.
|
||||
let x: Vec<Vec2> = std::iter::once(conic.point_at(t)).chain(got.iter().enumerate().map(|(i, v)| *v / fact(i + 1))).collect();
|
||||
for j in 0..=4usize {
|
||||
let lhs: Vec2 = (0..=j).fold(Vec2::ZERO, |acc, i| acc + x[i] * d.get(j - i).copied().unwrap_or(0.0));
|
||||
let rhs = n.get(j).copied().unwrap_or(Vec2::ZERO);
|
||||
assert!(lhs.dist(rhs) < 1e-8 * rhs.len().max(1.0), "t={t} j={j}: {lhs:?} vs {rhs:?}");
|
||||
}
|
||||
assert!(got[0].dist(conic.derivative(t)) < 1e-8, "t={t}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seg_derivatives_refuse_hostile_input_and_degenerate_pieces() {
|
||||
let c = Seg2::Cubic { p0: Vec2::ZERO, p1: Vec2::X, p2: Vec2::Y, p3: Vec2::X + Vec2::Y };
|
||||
assert!(c.derivatives(0.0, MAX_DERIVATIVE_ORDER + 1).is_none(), "no order above G4");
|
||||
assert!(c.derivatives(f64::NAN, 2).is_none());
|
||||
let nan = Seg2::Line { a: Vec2::new(f64::NAN, 0.0), b: Vec2::X };
|
||||
assert!(nan.derivatives(0.0, 1).is_none());
|
||||
// A conic whose weight vanishes is a straight chord drawn at zero speed: its derivatives
|
||||
// are finite, but the first is zero, which is what a blend refuses.
|
||||
let flat = Seg2::Conic { a: Vec2::ZERO, apex: Vec2::new(1.0, 1.0), b: Vec2::new(2.0, 0.0), w: 0.0 };
|
||||
let d = flat.derivatives(0.0, 1).unwrap();
|
||||
assert_eq!(d[0], Vec2::ZERO);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user