From c7bb64dd74052ae32f9904b23be8545c042fc274 Mon Sep 17 00:00:00 2001 From: NOWN-DEV <257771410+NOWN-DEV@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:26:15 +0200 Subject: [PATCH] feat: a slanted or curved edge across a sheet metal bend folds to a smooth curve of few pieces (#1799) Thanks @NOWN-DEV. --- crates/doc/src/sheet.rs | 223 ++++++++++++++++------ crates/kernel/src/prism.rs | 2 +- crates/kernel/src/sew.rs | 108 ++++++++--- crates/kernel/src/thread_display_tests.rs | 6 +- 4 files changed, 251 insertions(+), 88 deletions(-) diff --git a/crates/doc/src/sheet.rs b/crates/doc/src/sheet.rs index fa1af19b..8a446bcd 100644 --- a/crates/doc/src/sheet.rs +++ b/crates/doc/src/sheet.rs @@ -578,14 +578,22 @@ fn rev_edge(e: EdgeSpec) -> EdgeSpec { e.reversed() } -/// Largest distance between a folded bend edge and the cubic chain standing in for it. +/// Largest distance between a folded bend edge and the chain standing in for it. const BEND_CHAIN_TOL: f64 = 1e-7; -/// Most halvings of a bend edge's parameter while fitting its chain. -const BEND_CHAIN_DEPTH: usize = 10; +/// Degree of the pieces of a folded bend edge's chain. Hermite pieces of degree 7 match the +/// edge's point and first three derivatives at both ends, so one or two of them follow a helix +/// round a whole bend, where cubic pieces (point and tangent) took 32 and more. +const BEND_CHAIN_DEGREE: usize = 7; +/// Most pieces of a folded bend edge's chain. +const BEND_CHAIN_PIECES: usize = 256; -fn cubic_at(p0: Vec3, c1: Vec3, c2: Vec3, p1: Vec3, t: f64) -> Vec3 { - let u = 1.0 - t; - p0 * (u * u * u) + c1 * (3.0 * u * u * t) + c2 * (3.0 * u * t * t) + p1 * (t * t * t) +/// The point at `t` of the Bézier over `c` (de Casteljau). +fn bezier_at(c: &[Vec3], t: f64) -> Vec3 { + let mut w = c.to_vec(); + while w.len() > 1 { + w = w.windows(2).map(|p| p[0].lerp(p[1], t)).collect(); + } + w.first().copied().unwrap_or(Vec3::ZERO) } fn dedup(p: &[Vec2]) -> Vec { @@ -973,8 +981,10 @@ impl SheetBody { } /// A flat point of flange `f`'s bend zone folded onto the bend at height `z` (not clamped - /// to the zone), with its rates of change along the zone's flat `n` and `d` directions. - fn zone_point(&self, f: &SheetFlange, q: Vec2, z: f64) -> (Vec3, Vec3, Vec3) { + /// to the zone), with its first three derivatives along the zone's flat `n` direction and + /// its first along `d` (along `d` the bend is straight: the higher ones are zero, and so are + /// the mixed ones). + fn zone_point(&self, f: &SheetFlange, q: Vec2, z: f64) -> (Vec3, [Vec3; 3], Vec3) { let s = (q - f.p).dot(f.n); let u = (q - f.p).dot(f.d); let neutral = (f.radius + self.k * self.t).max(1e-12); @@ -987,58 +997,91 @@ impl SheetBody { let (sn, cs) = phi.sin_cos(); let w = self.world(f.parent); let local = axis_o + v3(f.d) * u + ez * cs + ek * sn; + // Each derivative along n turns the last a right angle about the axis and divides it by + // the neutral radius. let ds = (ez * -sn + ek * cs) * (1.0 / neutral); - (apply_point(&w, local), apply_vector(&w, ds), apply_vector(&w, v3(f.d))) + let dss = (ez * cs + ek * sn) * (-1.0 / (neutral * neutral)); + let dsss = ds * (-1.0 / (neutral * neutral)); + let along_n = [ds, dss, dsss].map(|v| apply_vector(&w, v)); + (apply_point(&w, local), along_n, apply_vector(&w, v3(f.d))) } /// A flat edge of flange `f`'s bend zone, folded at height `z`, at parameter `t`: the point - /// and its derivative. - fn zone_edge_at(&self, f: &SheetFlange, e: &Seg2, z: f64, t: f64) -> (Vec3, Vec3) { - let (p, ds, du) = self.zone_point(f, e.point_at(t), z); - let dq = e.derivative(t); - (p, ds * dq.dot(f.n) + du * dq.dot(f.d)) + /// and its first three derivatives. + fn zone_edge_at(&self, f: &SheetFlange, e: &Seg2, z: f64, t: f64) -> [Vec3; 4] { + let (p, [g1, g2, g3], du) = self.zone_point(f, e.point_at(t), z); + let [q1, q2, q3] = match e.derivatives(t, 3).as_deref() { + Some(&[q1, q2, q3]) => [q1, q2, q3], + _ => [e.derivative(t), Vec2::ZERO, Vec2::ZERO], + }; + let ([s1, s2, s3], [u1, u2, u3]) = ([q1, q2, q3].map(|q| q.dot(f.n)), [q1, q2, q3].map(|q| q.dot(f.d))); + // The chain rule (Faà di Bruno) through s(t) along n and u(t) along d. + [p, g1 * s1 + du * u1, g2 * (s1 * s1) + g1 * s2 + du * u2, g3 * (s1 * s1 * s1) + g2 * (3.0 * s1 * s2) + g1 * s3 + du * u3] } - /// Where to break a bend zone's flat edge so cubic Hermite pieces follow the folded edge - /// to within [`BEND_CHAIN_TOL`] on both faces. The breaks depend on the flat edge only, so - /// the top face, bottom face and wall of a cut-out share one chain per height. - fn zone_breaks(&self, f: &SheetFlange, e: &Seg2) -> Vec { - let mut out = vec![0.0]; - let mut todo = vec![(0.0, 1.0, 0usize)]; - while let Some((t0, t1, depth)) = todo.pop() { - let fits = depth >= BEND_CHAIN_DEPTH - || [0.0, self.t].iter().all(|&z| { - let h = t1 - t0; - let ((p0, d0), (p1, d1)) = (self.zone_edge_at(f, e, z, t0), self.zone_edge_at(f, e, z, t1)); - let (c1, c2) = (p0 + d0 * (h / 3.0), p1 - d1 * (h / 3.0)); - [0.25, 0.5, 0.75].iter().all(|&x| cubic_at(p0, c1, c2, p1, x).dist(self.zone_edge_at(f, e, z, t0 + h * x).0) <= BEND_CHAIN_TOL) - }); - if fits { - out.push(t1); - } else { - let m = (t0 + t1) / 2.0; - // Depth first, right half last on the stack: breaks come out in order. - todo.push((m, t1, depth + 1)); - todo.push((t0, m, depth + 1)); + /// The control points of the Hermite piece of degree 7 of a folded bend edge from `t0` to + /// `t1`: it has the edge's point and first three derivatives at both ends. (The k-th + /// derivative of a Bézier of degree 7 at its start is 7!/(7 − k)! times the k-th forward + /// difference of its first control points, and the edge's k-th derivative scales by + /// (t1 − t0)^k to the piece's parameter; at its end the same, backwards.) + fn zone_piece(&self, f: &SheetFlange, e: &Seg2, z: f64, t0: f64, t1: f64) -> [Vec3; BEND_CHAIN_DEGREE + 1] { + let h = t1 - t0; + let ([p0, a1, a2, a3], [p1, b1, b2, b3]) = (self.zone_edge_at(f, e, z, t0), self.zone_edge_at(f, e, z, t1)); + let (k1, k2, k3) = (h / 7.0, h * h / 42.0, h * h * h / 210.0); + [ + p0, + p0 + a1 * k1, + p0 + a1 * (2.0 * k1) + a2 * k2, + p0 + a1 * (3.0 * k1) + a2 * (3.0 * k2) + a3 * k3, + p1 - b1 * (3.0 * k1) + b2 * (3.0 * k2) - b3 * k3, + p1 - b1 * (2.0 * k1) + b2 * k2, + p1 - b1 * k1, + p1, + ] + } + + /// Into how many equal pieces (of its parameter) a bend zone's flat edge is folded, so that + /// the Hermite pieces are within [`BEND_CHAIN_TOL`] of the folded edge on both faces. The + /// count depends on the flat edge only, so the top face, bottom face and wall of a cut-out + /// share one chain per height; and as the pieces are equal, the chain's parameter runs evenly + /// with the edge's, and the pieces join with equal derivatives up to the third. + fn zone_pieces(&self, f: &SheetFlange, e: &Seg2) -> usize { + // The largest distance between the pieces and the folded edge, at points inside each. + let worst = |n: usize| { + let mut worst = 0.0f64; + for k in 0..n { + let (t0, t1) = (k as f64 / n as f64, (k + 1) as f64 / n as f64); + for z in [0.0, self.t] { + let c = self.zone_piece(f, e, z, t0, t1); + for j in 1..8 { + let t = t0 + (t1 - t0) * j as f64 / 8.0; + worst = worst.max(bezier_at(&c, j as f64 / 8.0).dist(self.zone_edge_at(f, e, z, t)[0])); + } + } } + worst + }; + let mut n = 1; + loop { + let off = worst(n); + if off <= BEND_CHAIN_TOL || !off.is_finite() || n >= BEND_CHAIN_PIECES { + return n; + } + // The distance falls as the eighth power of the pieces' length. + let guess = (n as f64 * (off / BEND_CHAIN_TOL).powf(1.0 / 8.0)).ceil(); + n = (guess as usize).clamp(n + 1, BEND_CHAIN_PIECES); } - out } - /// A bend zone's flat edge folded at height `z`: a chain of cubic Hermite pieces. + /// A bend zone's flat edge folded at height `z`: a chain of Hermite pieces of degree 7. fn zone_chain(&self, f: &SheetFlange, e: &Seg2, z: f64) -> EdgeSpec { - let breaks = self.zone_breaks(f, e); - let mut ctrl = Vec::with_capacity(breaks.len() * 3); - for w in breaks.windows(2) { - let (t0, t1) = (w[0], w[1]); - let h = t1 - t0; - let ((p0, d0), (p1, d1)) = (self.zone_edge_at(f, e, z, t0), self.zone_edge_at(f, e, z, t1)); - if ctrl.is_empty() { - ctrl.push(p0); - } - ctrl.extend([p0 + d0 * (h / 3.0), p1 - d1 * (h / 3.0), p1]); + let n = self.zone_pieces(f, e); + let mut ctrl = Vec::with_capacity(BEND_CHAIN_DEGREE * n + 1); + for k in 0..n { + let c = self.zone_piece(f, e, z, k as f64 / n as f64, (k + 1) as f64 / n as f64); + ctrl.extend_from_slice(if k == 0 { &c } else { &c[1..] }); } - EdgeSpec::Chain { ctrl: ctrl.into() } + EdgeSpec::Chain { degree: BEND_CHAIN_DEGREE, ctrl: ctrl.into() } } /// A flat edge of a piece as a 3D edge at height `z`. @@ -4918,21 +4961,34 @@ mod tests { assert!(s.zone_point(&f, q, z).0.dist(s.map3(Piece::Zone(0), q, z)) < 1e-12); } // A flat line across the whole zone, 15 mm along the bend: the bend angle and the - // distance along the axis both grow evenly along it. + // distance along the axis both grow evenly along it. Two pieces of degree 7 follow it + // (cubic ones took 32). let e = Seg2::Line { a: f.p + f.d * 20.0, b: f.p + f.n * s.allowance(&f) + f.d * 35.0 }; - let breaks = s.zone_breaks(&f, &e); + let n = s.zone_pieces(&f, &e); + assert_eq!(n, 2); for z in [0.0, 2.0] { let helix = |t: f64| { let (phi, rho) = (angle * t, 3.0 - z); Vec3::new(e.point_at(t).x, setback - rho * phi.sin(), 3.0 - rho * phi.cos()) }; - let EdgeSpec::Chain { ctrl } = s.zone_chain(&f, &e, z) else { panic!("a slanted edge folds to a chain") }; - assert_eq!(ctrl.len(), 3 * (breaks.len() - 1) + 1); - for (k, w) in breaks.windows(2).enumerate() { - for x in [0.0, 0.1, 0.37, 0.5, 0.81, 1.0] { - let p = cubic_at(ctrl[3 * k], ctrl[3 * k + 1], ctrl[3 * k + 2], ctrl[3 * k + 3], x); - let exact = helix(w[0] + (w[1] - w[0]) * x); - assert!(p.dist(exact) < BEND_CHAIN_TOL, "height {z}, piece {k}: {}", p.dist(exact)); + let EdgeSpec::Chain { degree: 7, ctrl } = s.zone_chain(&f, &e, z) else { panic!("a slanted edge folds to a chain of degree 7") }; + assert_eq!(ctrl.len(), 7 * n + 1); + for k in 0..n { + for i in 0..=40 { + let x = i as f64 / 40.0; + let off = bezier_at(&ctrl[7 * k..=7 * k + 7], x).dist(helix((k as f64 + x) / n as f64)); + assert!(off < BEND_CHAIN_TOL, "height {z}, piece {k}, {x}: {off}"); + } + } + // The pieces are equal, so the chain is one smooth curve: on either side of a joint + // the first three differences of the control points are the same. + let diff = |p: &[Vec3]| p.windows(2).map(|w| w[1] - w[0]).collect::>(); + for k in 1..n { + let (mut before, mut after) = (ctrl[7 * k - 3..=7 * k].to_vec(), ctrl[7 * k..=7 * k + 3].to_vec()); + for order in 1..=3 { + (before, after) = (diff(&before), diff(&after)); + let (b, a) = (before[before.len() - 1], after[0]); + assert!(b.dist(a) < 1e-12, "height {z}, joint {k}, order {order}: {b:?} vs {a:?}"); } } } @@ -7436,6 +7492,59 @@ mod tests { assert!(s.solid().is_ok() && s.flat_regions().unwrap().len() == 1); } + #[test] + fn a_torn_bend_end_folds_to_two_smooth_pieces_on_its_helix() { + // The tray of `a_tear_cuts_each_bend_from_the_corner_to_its_flanges_end` (T = 2, R = 1): + // each of its eight torn bend ends is the wall ruled between a helix on the bend's inner + // face and one on its outer face, `bend_end` at the turn φ from 0 to θ. Cubic pieces took + // 32 at either angle, which a viewer draws as 32 strips; two pieces of degree 7 hold the + // helix to within the chains' tolerance. + for deg in [90.0f64, 72.0] { + let what = format!("{deg}°"); + let (s, _) = tray(&torn(), deg, 38.6); + let mut work = FlatWork::new(); + let cells = s.cells_with_work(&mut work).unwrap(); + let faces = s.faces(&cells, 0.0, s.t, &mut work).unwrap(); + let walls: Vec<(&EdgeSpec, &EdgeSpec)> = faces + .iter() + .filter_map(|f| match &f.surface { + SurfSpec::Ruled { a, b } => Some((a, b)), + _ => None, + }) + .collect(); + assert_eq!(walls.len(), 8, "{what}"); + let th = deg.to_radians(); + for (bottom, top) in walls { + for (chain, z) in [(bottom, 0.0), (top, s.t)] { + let EdgeSpec::Chain { ctrl, .. } = chain else { panic!("{what}: a torn end folds to a chain") }; + assert_eq!(ctrl.len(), 15, "{what}: two pieces of degree 7"); + // The torn end this chain runs along, and which way. + let (first, last) = (ctrl[0], ctrl[ctrl.len() - 1]); + let end = s.flanges.iter().flat_map(|f| [(f, false), (f, true)]).find_map(|(f, at_end)| { + let at = |phi: f64| s.bend_end(f, at_end, phi, z); + if at(0.0).dist(first) < 1e-9 && at(th).dist(last) < 1e-9 { + Some((f, at_end, false)) + } else if at(th).dist(first) < 1e-9 && at(0.0).dist(last) < 1e-9 { + Some((f, at_end, true)) + } else { + None + } + }); + let Some((f, at_end, back)) = end else { panic!("{what}: {first:?} to {last:?} is no torn end") }; + for k in 0..2 { + for i in 0..=40 { + let t = (k as f64 + i as f64 / 40.0) / 2.0; + let exact = s.bend_end(f, at_end, th * if back { 1.0 - t } else { t }, z); + let off = bezier_at(&ctrl[7 * k..=7 * k + 7], i as f64 / 40.0).dist(exact); + assert!(off < BEND_CHAIN_TOL, "{what}: height {z}, {t}: {off}"); + } + } + } + } + holds_the_flat_material(&s, &what); + } + } + #[test] fn a_torn_bend_end_is_no_flat_face() { use std::f64::consts::{FRAC_PI_2, SQRT_2}; diff --git a/crates/kernel/src/prism.rs b/crates/kernel/src/prism.rs index 0101115e..71d0475a 100644 --- a/crates/kernel/src/prism.rs +++ b/crates/kernel/src/prism.rs @@ -1995,7 +1995,7 @@ fn sew_connected_sections( let [p0, p1, p2, p3] = cubic_of(sg)?; let c = same_chain([welded(p0, z), at(p1, z), at(p2, z), welded(p3, z)]); let mid = (c[0] + (c[1] + c[2]) * 3.0 + c[3]) * 0.125; - same_edge(EdgeSpec::Chain { ctrl: c.to_vec().into() }, c[0], c[3], mid) + same_edge(EdgeSpec::Chain { degree: 3, ctrl: c.to_vec().into() }, c[0], c[3], mid) } }) }; diff --git a/crates/kernel/src/sew.rs b/crates/kernel/src/sew.rs index 0c5c8b4a..c8c26b8b 100644 --- a/crates/kernel/src/sew.rs +++ b/crates/kernel/src/sew.rs @@ -1,5 +1,5 @@ //! Sewing a solid from face descriptions: each face is its boundary loops (lines, circular -//! arcs and cubic Bézier chains, counter-clockwise about the outward normal; holes clockwise) +//! arcs and Bézier chains, counter-clockwise about the outward normal; holes clockwise) //! and its surface (planar, revolved from a line, extruded from an arc, or ruled between two //! Bézier chains). Vertices and edges at the same places are shared, so the faces close into //! one shell without booleans — for solids whose faces are known exactly, such as folded @@ -17,6 +17,8 @@ use crate::{KernelError, Result, guard}; /// Most control points of one Bézier chain edge. pub const MAX_CHAIN_POINTS: usize = 3 * 1024 + 1; +/// Highest degree of a Bézier chain's pieces. +pub const MAX_CHAIN_DEGREE: usize = 9; /// A boundary edge. #[derive(Clone, Debug, PartialEq)] @@ -31,10 +33,12 @@ pub enum EdgeSpec { b: Vec3, mid: Vec3, }, - /// A chain of cubic Bézier pieces: 3n + 1 control points (n ≥ 1); the curve passes - /// through `ctrl[0]`, `ctrl[3]`, … `ctrl[3n]`. For curves with no exact line, arc or conic - /// form, such as a cut-out edge traced across a sheet metal bend. + /// A chain of Bézier pieces of degree `degree` (1…[`MAX_CHAIN_DEGREE`]): `degree`·n + 1 + /// control points (n ≥ 1); the curve passes through `ctrl[0]`, `ctrl[degree]`, … + /// `ctrl[degree·n]`. For curves with no exact line, arc or conic form, such as a font's + /// outline (cubic) or a cut-out edge traced across a sheet metal bend. Chain { + degree: usize, ctrl: Arc<[Vec3]>, }, } @@ -43,7 +47,7 @@ impl EdgeSpec { fn ends(&self) -> (Vec3, Vec3) { match self { EdgeSpec::Line { a, b } | EdgeSpec::Arc { a, b, .. } => (*a, *b), - EdgeSpec::Chain { ctrl } => (ctrl.first().copied().unwrap_or(Vec3::ZERO), ctrl.last().copied().unwrap_or(Vec3::ZERO)), + EdgeSpec::Chain { ctrl, .. } => (ctrl.first().copied().unwrap_or(Vec3::ZERO), ctrl.last().copied().unwrap_or(Vec3::ZERO)), } } @@ -57,7 +61,7 @@ impl EdgeSpec { let (m, e) = (*mid - *a, *b - *a); m * (1.0 / m.len2()) - e * (1.0 / e.len2()) } - EdgeSpec::Chain { ctrl } => { + EdgeSpec::Chain { ctrl, .. } => { let first = *ctrl.first()?; ctrl.iter().skip(1).map(|p| *p - first).find(|d| d.len() > 1e-12)? } @@ -70,7 +74,7 @@ impl EdgeSpec { match self { EdgeSpec::Line { a, b } => EdgeSpec::Line { a: *b, b: *a }, EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: *b, b: *a, mid: *mid }, - EdgeSpec::Chain { ctrl } => EdgeSpec::Chain { ctrl: ctrl.iter().rev().copied().collect() }, + EdgeSpec::Chain { degree, ctrl } => EdgeSpec::Chain { degree: *degree, ctrl: ctrl.iter().rev().copied().collect() }, } } @@ -79,7 +83,7 @@ impl EdgeSpec { match self { EdgeSpec::Line { .. } => None, EdgeSpec::Arc { mid, .. } => Some(key(*mid)), - EdgeSpec::Chain { ctrl } => { + EdgeSpec::Chain { ctrl, .. } => { // Summed in pairs from both ends, so that the chain run either way gives the very // same point: a font's control points are simple fractions, whose mean can fall // exactly halfway between two steps of the key, where summing in another order @@ -99,8 +103,12 @@ impl EdgeSpec { match self { EdgeSpec::Line { a, b } => a.is_finite() && b.is_finite(), EdgeSpec::Arc { a, b, mid } => a.is_finite() && b.is_finite() && mid.is_finite(), - EdgeSpec::Chain { ctrl } => { - ctrl.len() >= 4 && ctrl.len() <= MAX_CHAIN_POINTS && (ctrl.len() - 1) % 3 == 0 && ctrl.iter().all(|p| p.is_finite()) + EdgeSpec::Chain { degree, ctrl } => { + (1..=MAX_CHAIN_DEGREE).contains(degree) + && ctrl.len() > *degree + && ctrl.len() <= MAX_CHAIN_POINTS + && (ctrl.len() - 1) % degree == 0 + && ctrl.iter().all(|p| p.is_finite()) } } } @@ -115,8 +123,9 @@ pub enum SurfSpec { Revolved { a: Vec3, b: Vec3, axis_origin: Vec3, axis: Vec3, angle: f64 }, /// The surface an arc sweeps moving along `dir`. Extruded { a: Vec3, b: Vec3, mid: Vec3, dir: Vec3 }, - /// The straight lines joining equal parameters of two Bézier chains with the same number - /// of pieces, `a` and `b` running the same way; both chains lie on it as boundary edges. + /// The straight lines joining equal parameters of two Bézier chains of one degree with the + /// same number of pieces, `a` and `b` running the same way; both chains lie on it as + /// boundary edges. Ruled { a: EdgeSpec, b: EdgeSpec }, } @@ -184,7 +193,7 @@ fn welded(faces: &[FaceSpec]) -> Option> { let edge = |e: &EdgeSpec| match e { EdgeSpec::Line { a, b } => EdgeSpec::Line { a: weld(*a), b: weld(*b) }, EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: weld(*a), b: weld(*b), mid: *mid }, - EdgeSpec::Chain { ctrl } => { + EdgeSpec::Chain { degree, ctrl } => { let mut ctrl = ctrl.to_vec(); let n = ctrl.len(); for i in [0, n.saturating_sub(1)] { @@ -192,7 +201,7 @@ fn welded(faces: &[FaceSpec]) -> Option> { *p = weld(*p); } } - EdgeSpec::Chain { ctrl: ctrl.into() } + EdgeSpec::Chain { degree: *degree, ctrl: ctrl.into() } } }; Some( @@ -245,18 +254,20 @@ impl Cache { Ok(made) } - /// A Bézier chain as a cubic B-spline (triple inner knots) ending on the shared vertices. + /// A Bézier chain as a B-spline of its degree (inner knots as many times as the degree) + /// ending on the shared vertices. fn chain(&mut self, e: &EdgeSpec) -> Result> { - let EdgeSpec::Chain { ctrl } = e else { return Err(KernelError::Invalid("sew: not a Bézier chain".into())) }; + let EdgeSpec::Chain { degree, ctrl } = e else { return Err(KernelError::Invalid("sew: not a Bézier chain".into())) }; if !e.valid() { - return Err(KernelError::Invalid("sew: a Bézier chain needs 3n + 1 finite control points".into())); + return Err(KernelError::Invalid("sew: a Bézier chain needs degree·n + 1 finite control points".into())); } - let n = (ctrl.len() - 1) / 3; - let mut knots = vec![0.0; 4]; + let d = *degree; + let n = (ctrl.len() - 1) / d; + let mut knots = vec![0.0; d + 1]; for i in 1..n { - knots.extend([i as f64; 3]); + knots.extend(std::iter::repeat_n(i as f64, d)); } - knots.extend([n as f64; 4]); + knots.extend(std::iter::repeat_n(n as f64, d + 1)); let mut pts: Vec = ctrl.iter().map(|p| p3(*p)).collect(); let (a, b) = e.ends(); if let Some(first) = pts.first_mut() { @@ -491,7 +502,7 @@ fn plane_normal(face: &FaceSpec) -> Option { let n = face.loops.first()?.iter().fold(Vec3::ZERO, |n, e| match e { EdgeSpec::Line { a, b } => n + a.cross(*b), EdgeSpec::Arc { a, b, mid } => n + a.cross(*mid) + mid.cross(*b), - EdgeSpec::Chain { ctrl } => ctrl.iter().zip(ctrl.iter().skip(1)).fold(n, |n, (p, q)| n + p.cross(*q)), + EdgeSpec::Chain { ctrl, .. } => ctrl.iter().zip(ctrl.iter().skip(1)).fold(n, |n, (p, q)| n + p.cross(*q)), }); n.normalized() } @@ -830,7 +841,7 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option { let (ca, cb) = (cache.chain(a)?, cache.chain(b)?); - if ca.control_points().len() != cb.control_points().len() { + if ca.degree() != cb.degree() || ca.control_points().len() != cb.control_points().len() { return Err(KernelError::Invalid("sew: ruled chains differ in pieces".into())); } let surface = mt::Surface::BSplineSurface(mt::BSplineSurface::homotopy(ca, cb)); @@ -974,7 +985,7 @@ mod tests { p(35.32833796296296, 11.885361111111111), p(35.53081944444444, 11.885361111111111), ]; - let forward = EdgeSpec::Chain { ctrl: ctrl.to_vec().into() }; + let forward = EdgeSpec::Chain { degree: 3, ctrl: ctrl.to_vec().into() }; assert_eq!(forward.mid_key(), forward.reversed().mid_key()); } @@ -1130,7 +1141,22 @@ mod tests { /// The bulge's cubic split at its middle (de Casteljau), at height `z`. fn bulge(z: f64) -> EdgeSpec { let ctrl = [(4.0, 0.0), (4.5, 0.0), (4.75, 0.75), (4.75, 1.5), (4.75, 2.25), (4.5, 3.0), (4.0, 3.0)]; - EdgeSpec::Chain { ctrl: ctrl.iter().map(|(x, y)| Vec3::new(*x, *y, z)).collect() } + EdgeSpec::Chain { degree: 3, ctrl: ctrl.iter().map(|(x, y)| Vec3::new(*x, *y, z)).collect() } + } + + /// The bulge's cubic raised to degree 5, (4,0) (4.6,0) (4.9,0.9) (4.9,2.1) (4.6,3) (4,3), and + /// split at its middle (de Casteljau), at height `z`: the same curve as a quintic chain. + fn quintic_bulge(z: f64) -> EdgeSpec { + let raised = [(4.0, 0.0), (4.6, 0.0), (4.9, 0.9), (4.9, 2.1), (4.6, 3.0), (4.0, 3.0)].map(|(x, y)| Vec3::new(x, y, z)); + let (mut left, mut right, mut w) = (Vec::new(), Vec::new(), raised.to_vec()); + while let (Some(first), Some(last)) = (w.first().copied(), w.last().copied()) { + left.push(first); + right.push(last); + w = w.windows(2).map(|p| (p[0] + p[1]) * 0.5).collect(); + } + right.reverse(); + left.extend(right.into_iter().skip(1)); + EdgeSpec::Chain { degree: 5, ctrl: left.into() } } #[test] @@ -1145,10 +1171,22 @@ mod tests { assert!((m.bbox.max.x - 4.75).abs() < 1e-3, "{:?}", m.bbox); } + #[test] + fn a_ruled_wall_between_quintic_chains_closes_the_same_solid() { + let EdgeSpec::Chain { ctrl, .. } = quintic_bulge(0.0) else { panic!() }; + assert_eq!(ctrl.len(), 11); + let body = sew(&bulged_box(quintic_bulge(0.0), quintic_bulge(2.0))).unwrap(); + assert_eq!(body.face_count(), 6); + assert!(body.validity().is_empty(), "{:?}", body.validity()); + let m = crate::measure(&body).unwrap(); + assert!((m.volume - 27.6).abs() < 2e-3, "{}", m.volume); + assert!((m.bbox.max.x - 4.75).abs() < 1e-3, "{:?}", m.bbox); + } + #[test] fn hostile_bezier_chains_are_refused() { - let chain = |ctrl: Vec| EdgeSpec::Chain { ctrl: ctrl.into() }; - let EdgeSpec::Chain { ctrl } = bulge(2.0) else { panic!() }; + let chain = |ctrl: Vec| EdgeSpec::Chain { degree: 3, ctrl: ctrl.into() }; + let EdgeSpec::Chain { ctrl, .. } = bulge(2.0) else { panic!() }; // Not 3n + 1 control points; a control point that is not a number; too many pieces. let mut nan = ctrl.to_vec(); nan[2].x = f64::NAN; @@ -1160,6 +1198,22 @@ mod tests { let whole = chain(vec![Vec3::new(4.0, 0.0, 2.0), Vec3::new(5.0, 0.0, 2.0), Vec3::new(5.0, 3.0, 2.0), Vec3::new(4.0, 3.0, 2.0)]); let e = sew(&bulged_box(bulge(0.0), whole)).unwrap_err().to_string(); assert!(e.contains("ruled chains differ"), "{e}"); + // Degree 0, or above the highest, or a count of control points that doesn't fit the degree + // (11 control points are no chain of quartics). + let EdgeSpec::Chain { ctrl: fifth, .. } = quintic_bulge(2.0) else { panic!() }; + let degree = |degree: usize, ctrl: &[Vec3]| EdgeSpec::Chain { degree, ctrl: ctrl.to_vec().into() }; + // `n` control points bulging out from (4, 0) to (4, 3) at height `z`. + let arch = |n: usize, z: f64| -> Vec { + let at = |i: usize| i as f64 / (n - 1) as f64; + (0..n).map(|i| Vec3::new(4.0 + (std::f64::consts::PI * at(i)).sin(), 3.0 * at(i), z)).collect() + }; + for top in [degree(0, &ctrl), degree(MAX_CHAIN_DEGREE + 1, &arch(MAX_CHAIN_DEGREE + 2, 2.0)), degree(4, &fifth)] { + assert!(sew(&bulged_box(bulge(0.0), top)).is_err()); + } + // A ruled wall needs chains of one degree: 16 control points are five cubic pieces or + // three quintic ones. + let e = sew(&bulged_box(degree(3, &arch(16, 0.0)), degree(5, &arch(16, 2.0)))).unwrap_err().to_string(); + assert!(e.contains("ruled chains differ"), "{e}"); } #[test] diff --git a/crates/kernel/src/thread_display_tests.rs b/crates/kernel/src/thread_display_tests.rs index 2b696b2f..2e74a35e 100644 --- a/crates/kernel/src/thread_display_tests.rs +++ b/crates/kernel/src/thread_display_tests.rs @@ -22,12 +22,12 @@ fn helix(r: f64, z0: f64, pitch: f64, a0: f64, a1: f64) -> EdgeSpec { let (s, e) = (a0 + h * i as f64, a0 + h * (i + 1) as f64); ctrl.extend([at(s) + turn(s) + rise, at(e) - turn(e) - rise, at(e)]); } - EdgeSpec::Chain { ctrl: ctrl.into() } + EdgeSpec::Chain { degree: 3, ctrl: ctrl.into() } } fn rev(e: &EdgeSpec) -> EdgeSpec { match e { - EdgeSpec::Chain { ctrl } => EdgeSpec::Chain { ctrl: ctrl.iter().rev().copied().collect::>() }, + EdgeSpec::Chain { degree, ctrl } => EdgeSpec::Chain { degree: *degree, ctrl: ctrl.iter().rev().copied().collect::>() }, EdgeSpec::Line { a, b } => EdgeSpec::Line { a: *b, b: *a }, EdgeSpec::Arc { a, b, mid } => EdgeSpec::Arc { a: *b, b: *a, mid: *mid }, } @@ -39,7 +39,7 @@ fn line(a: Vec3, b: Vec3) -> EdgeSpec { fn ends(e: &EdgeSpec) -> (Vec3, Vec3) { match e { - EdgeSpec::Chain { ctrl } => (ctrl[0], ctrl[ctrl.len() - 1]), + EdgeSpec::Chain { ctrl, .. } => (ctrl[0], ctrl[ctrl.len() - 1]), EdgeSpec::Line { a, b } | EdgeSpec::Arc { a, b, .. } => (*a, *b), } }