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
+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,
|
||||
|
||||
Reference in new issue
Block a user