From f7409dd59fd2a594464cafa53f2115da2aa75a80 Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:25:54 -0400 Subject: [PATCH] geom, kernel: a loop with fewer than three corners is filled by a ring of patches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tube's end has no corner at all; the kernel gives its circle in four quarters, and one patch on them has a corner on each quarter joint where the loop runs straight on: flat, its two derivatives parallel, which SMLib's check refuses (Bad Surf FlatCorner within 0.5°). Any layout with a patch corner on such a loop has one. Such a loop is now the ring of structured meshing (an O-grid): four patches each on one side of the loop, two meeting at every point of it, about one inner patch (Quilt::ring). The spokes are now any curve two patches share (Seam), each glued G1 with linear gluing functions: at an inner vertex of valence m the characteristic value ±2cos(2π/m) (the centre's spokes as before, the ring's inner corners with m = 3), at the loop what its two sides' derivatives there ask (foot_gluing; 0 and their speeds' ratio where the loop runs straight on). IsoFill says where each spoke starts and ends (SpokeEnd) and holds the inner vertices; the kernel builds them so. The tube end caps G1 (normal within 0.0157° of the wall, sealed 10458.232 against the tube and dome's 10458.232) and G2 (curvature across 4.6e-5 /mm against G1's 0.30), five faces with no flat corner. A crown on the ring lifts the inner patch whole (the ring rises as v(2 − v)). Saved designs: a general patch on a smooth closed loop (a tube's end) rebuilds as five faces where it was one (which SMLib refused). Co-Authored-By: Claude Opus 5.5 --- crates/geom/src/fill_iso.rs | 442 ++++++++++++++++++++++++++---- crates/geom/src/fill_iso_tests.rs | 19 +- crates/kernel/src/fill.rs | 38 +-- 3 files changed, 422 insertions(+), 77 deletions(-) diff --git a/crates/geom/src/fill_iso.rs b/crates/geom/src/fill_iso.rs index 816be878f9..0ebcde9552 100644 --- a/crates/geom/src/fill_iso.rs +++ b/crates/geom/src/fill_iso.rs @@ -10,7 +10,8 @@ //! parameter, raised to the side's degree, the pieces joined at a knot of full multiplicity), //! so a vertex where the loop runs straight on (a seam) is never a patch's corner: a patch //! corner there would be flat (its two derivatives parallel), degenerate and refused by a -//! B-rep's checks. A loop with fewer than three corners keeps its edges as its sides. +//! B-rep's checks. A loop with fewer than three corners keeps its edges as its sides, and must +//! have four of them (a tube's end, which the kernel gives in four pieces). //! - **The patches.** A loop of four sides `e₀ … e₃` bounds one patch on the unit square: `e₀` //! runs along `v = 0` from `u = 0` to 1, `e₁` up `u = 1`, `e₂` back along `v = 1` and `e₃` down //! `u = 0`. A loop of three has no `e₃`: the side `u = 0` collapses to the loop's first corner @@ -20,8 +21,11 @@ //! `V_k, M_k, C, M_{k−1}` (`V_k` the corner where side `k` starts): the first half of side `k` //! along `v = 0`, spoke `k` up `u = 1`, spoke `k − 1` back along `v = 1` and the second half of //! side `k − 1` down `u = 0`. Each spoke is one row of the two patches it divides, the same -//! control points in both. Every edge's pcurve is a straight line on its stretch of a patch's -//! side, affine in its parameter. +//! control points in both. A loop with fewer than three corners has no N-gon whose corners +//! are not flat; it is the ring of structured meshing (an "O-grid"): four patches each on one +//! side, two meeting at every point of the loop, about one inner patch ([`Quilt::ring`]). +//! Every edge's pcurve is a straight line on its stretch of a patch's side, affine in its +//! parameter. //! - **Rational edges** (a circle's arc, as every conic is held) are boundary rows of a rational //! patch: the fill works on homogeneous control points `(w·P, w)`. An edge's homogeneous points //! may be scaled by a constant without changing its curve or its parameter, so each edge is @@ -69,15 +73,18 @@ //! - across a patch's own C0 knot lines (a side's break where it is only C0 in its //! parameter), the derivatives either side in the ratio of the crossing sides' speeds //! ([`knot_line_rows`]): the fill is tangent continuous there too; -//! - across each spoke, the two patches' derivatives away from it opposed up to the linear -//! gluing function of G1 joins at an N-valent point (C. Loop, J. Peters, J. Hahn): +//! - across each spoke (a curve inside the fill two patches share: [`Seam`]), the two patches' +//! derivatives away from it opposed up to the linear gluing function of G1 joins at an +//! N-valent point (C. Loop, J. Peters, J. Hahn): //! `∂S_k/∂u + ∂S_{k+1}/∂v = α(t)·∂S/∂t` on the shared parameter t (0 at the edge's //! midpoint, 1 at the centre), `α(t) = 2·cos(2π/N)·t`: the patches join tangent there. At //! the midpoint `α = 0` (the edge's two halves leave it at the same speed, opposed); at the //! centre it is the value the spokes' tangents `d_k` meet when they are any affine image of //! the regular N-star, `d_{k−1} + d_{k+1} = 2·cos(2π/N)·d_k`. Parametric C1 (`α = 0`) asks //! `d_{k−1} + d_{k+1} = 0` there, which for N other than a multiple of four only `d = 0` -//! meets: every patch would have a flat corner at the centre; +//! meets: every patch would have a flat corner at the centre. The ring's spokes are glued +//! alike: at a valence-3 inner corner `α = ±2·cos(2π/3)`, at the loop by what its two sides' +//! derivatives there ask ([`foot_gluing`]); //! - points the fill passes through, located on the last solution (nearest point of any patch) //! and fitted again (Hoschek's parameter correction). //! @@ -233,14 +240,27 @@ pub struct IsoPatch { #[derive(Clone, Debug, PartialEq)] pub struct IsoFill { pub patches: Vec, - /// Each spoke as a curve (degree, knots on `[0, 1]`, control points and, on a rational fill, - /// weights): from its edge's midpoint to the centre. Empty for a fill of one patch. + /// Each spoke (a curve inside the fill two patches share) as a curve (degree, knots on + /// `[0, 1]`, control points and, on a rational fill, weights), from its first end to its + /// second. Empty for a fill of one patch. pub spokes: Vec, - /// Where each spoke starts: on loop edge `.0` at its parameter `.1` (the middle of a side). - pub feet: Vec<(usize, f64)>, + /// Where each spoke starts and ends. + pub ends: Vec<[SpokeEnd; 2]>, + /// The fill's vertices inside the loop (the centre, or the inner patch's corners), where + /// spokes end ([`SpokeEnd::Inner`]). + pub inner: Vec, pub achieved: Achieved, } +/// Where a spoke ends. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SpokeEnd { + /// On loop edge `edge` at its parameter `t`. + Loop { edge: usize, t: f64 }, + /// At the fill's inner vertex `.0` ([`IsoFill::inner`]). + Inner(usize), +} + /// What a fill achieves at its stations and points (its boundary is exact). #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Achieved { @@ -400,8 +420,9 @@ struct SidePiece { } /// The region's sides (module docs): the chains of edges between the loop's corners, the first -/// starting at a corner; each edge its own side when the loop has fewer than three corners. -fn loop_sides(edges: &[IsoEdge], corner_turn: f64) -> Vec> { +/// starting at a corner; each edge its own side when the loop has fewer than three corners. And +/// whether it has three or more. +fn loop_sides(edges: &[IsoEdge], corner_turn: f64) -> (Vec>, bool) { let n = edges.len(); let whole = |k: usize| SidePiece { edge: k, range: edges.get(k).map_or([0.0, 1.0], IsoEdge::domain), at: [0.0, 1.0] }; // A corner where edge k starts: the tangents either side differ by more than the turn. @@ -416,7 +437,7 @@ fn loop_sides(edges: &[IsoEdge], corner_turn: f64) -> Vec> { .collect(); let corners: Vec = (0..n).filter(|k| corner.get(*k).copied().unwrap_or(true)).collect(); if corners.len() < 3 { - return (0..n).map(|k| vec![whole(k)]).collect(); + return ((0..n).map(|k| vec![whole(k)]).collect(), false); } // Each edge's share of its side: its control polygon's length (any positive measure keeps the // side exact; this one keeps its speed near the edges' own). @@ -436,7 +457,7 @@ fn loop_sides(edges: &[IsoEdge], corner_turn: f64) -> Vec> { } sides.push(side); } - sides + (sides, true) } /// A side as one curve on `[0, 1]` (module docs): its edges (homogeneous points scaled by @@ -704,10 +725,89 @@ struct Quilt { unknowns: usize, /// The loop edges' pieces on the patches' sides. pieces: Vec, - /// Spokes: (patch on whose `u = 1` it runs, patch on whose `v = 1` it runs). - spokes: Vec<(usize, usize)>, - /// Where each spoke starts on the loop: (edge, parameter). - feet: Vec<(usize, f64)>, + /// The curves inside the fill two patches share, each joined G1 across. + spokes: Vec, + /// The fill's vertices inside the loop: a patch and its control point there. + inner: Vec<(usize, usize, usize)>, + /// How the fill's middle is crowned. + layout: Layout, +} + +/// How the region is laid out in patches (module docs). +#[derive(Clone, Copy, Debug, PartialEq)] +enum Layout { + /// One patch on three or four sides. + One, + /// N patches about a centre. + Centre, + /// Four patches round the loop and one inside them. + Ring, +} + +/// A curve inside the fill shared by two patches: it runs along side `a.1` of patch `a.0` in that +/// side's direction (`t` from 0 to 1) and back along side `b.1` of patch `b.0`. Across it, with +/// `D_a`, `D_b` each patch's derivative across it away from itself and `L` the derivative along it, +/// `D_a + β(t)·D_b = α(t)·L`: the linear gluing of a G1 join, `α` and `β` linear between their +/// values at the ends (module docs). +#[derive(Clone, Copy, Debug, PartialEq)] +struct Seam { + a: (usize, usize), + b: (usize, usize), + alpha: [f64; 2], + beta: [f64; 2], + ends: [SpokeEnd; 2], +} + +impl Seam { + /// Where on each patch's side the seam is at its parameter `t`. + fn at(&self, t: f64) -> ([f64; 2], [f64; 2]) { + (station_on(self.a.1, t).0, station_on(self.b.1, 1.0 - t).0) + } + + /// The gluing functions at `t`. + fn gluing(&self, t: f64) -> (f64, f64) { + let lerp = |x: [f64; 2]| x[0] + (x[1] - x[0]) * t; + (lerp(self.alpha), lerp(self.beta)) + } + + /// Whether `t` is within `share` of an end inside the loop (an inner vertex). + fn near_inner(&self, t: f64, share: f64) -> bool { + (matches!(self.ends[0], SpokeEnd::Inner(_)) && t < share) || (matches!(self.ends[1], SpokeEnd::Inner(_)) && t > 1.0 - share) + } +} + +/// The gluing at a spoke's foot on the loop: `(β, α)` with `D_a + β·D_b = α·L` for the two +/// patches' derivatives across it away from themselves (there, the loop's two sides' tangents, run +/// apart) and the spoke's own `L`, least squares in their plane. Where the loop runs straight on, +/// `α = 0` and `β` is the ratio of the sides' speeds. +fn foot_gluing(da: Vec3, db: Vec3, l: Vec3) -> (f64, f64) { + // [db, −l]·(β, α) = −da. + let (a11, a12, a22) = (db.dot(db), -db.dot(l), l.dot(l)); + let (r1, r2) = (-db.dot(da), l.dot(da)); + let det = a11 * a22 - a12 * a12; + if !(det.abs() > 1e-300) { + return (1.0, 0.0); + } + ((r1 * a22 - a12 * r2) / det, (a11 * r2 - a12 * r1) / det) +} + +/// The derivative of order one along side `s` of the unit square, in its running direction: +/// `(du, dv, sign)`. +fn along_side(s: usize) -> (usize, usize, f64) { + match s { + 0 => (1, 0, 1.0), + 1 => (0, 1, 1.0), + 2 => (1, 0, -1.0), + _ => (0, 1, -1.0), + } +} + +/// The gluing function `α` at an end of a seam at an inner vertex of `valence` patches: the value +/// the regular characteristic map meets there, `2·cos(2π/m)` with the seam's parameter running +/// towards the vertex (`towards`), its opposite running away (module docs). +fn characteristic(valence: usize, towards: bool) -> f64 { + let a = 2.0 * (std::f64::consts::TAU / valence.max(1) as f64).cos(); + if towards { a } else { -a } } /// The constants each edge's homogeneous points are scaled by so that the weights at every @@ -741,7 +841,7 @@ impl Quilt { /// sides that run that way; about a centre, the space along each spoke is the two patches' /// either side of it (one's along v, the other's along u), holding the spoke and the two edge /// halves it meets those patches beside. - fn new(edges: &[IsoEdge], loop_sides: &[Vec], extra: &[Vec; 2], degree: usize) -> SplineResult { + fn new(edges: &[IsoEdge], loop_sides: &[Vec], ring: bool, extra: &[Vec; 2], degree: usize) -> SplineResult { let n = loop_sides.len(); let scales = corner_scales(edges)?; let whole: Vec = loop_sides.iter().map(|side| side_curve(edges, &scales, side)).collect::>()?; @@ -749,6 +849,9 @@ impl Quilt { let c = whole.get(k).ok_or_else(|| SplineError::Invalid("sides".into()))?; piece_on_unit(c.space.degree(), c.space.knots(), &c.points, range, reverse) }; + if ring { + return Quilt::ring(loop_sides, &whole, extra, degree); + } if n <= 4 { // One patch: the loop's sides are its sides (side 2 and 3 run back along their rows). let mut sides: Vec = (0..n).map(|k| part(k, [0.0, 1.0], k >= 2)).collect::>()?; @@ -778,7 +881,15 @@ impl Quilt { } let unknowns = inner_u * inner_v; let pieces = loop_sides.iter().enumerate().flat_map(|(k, side)| pieces_between(side, k, [0.0, 1.0], 0, k)).collect(); - return Ok(Quilt { patches: vec![patch], slots: vec![slots], unknowns, pieces, spokes: Vec::new(), feet: Vec::new() }); + return Ok(Quilt { + patches: vec![patch], + slots: vec![slots], + unknowns, + pieces, + spokes: Vec::new(), + inner: Vec::new(), + layout: Layout::One, + }); } // N patches about the centre: halves of each side, and straight spokes to start. let middle: Vec = whole.iter().map(|c| c.space.eval(&c.points, 0.5, 0)).collect::>()?; @@ -867,14 +978,184 @@ impl Quilt { slots.push(s); } let mut pieces = Vec::with_capacity(2 * n); - let mut feet = Vec::with_capacity(n); + let mut spokes = Vec::with_capacity(n); for (k, side) in loop_sides.iter().enumerate() { pieces.extend(pieces_between(side, k, [0.0, 0.5], k, 0)); pieces.extend(pieces_between(side, k, [0.5, 1.0], (k + 1) % n, 3)); - feet.push(on_side(side, 0.5).ok_or_else(|| SplineError::Invalid("a side's middle".into()))?); + let (edge, t) = on_side(side, 0.5).ok_or_else(|| SplineError::Invalid("a side's middle".into()))?; + // Spoke k up patch k's u = 1 (from the midpoint to the centre), back along patch + // k + 1's v = 1; at the midpoint the edge's halves leave it at the same speed. + spokes.push(Seam { + a: (k, 1), + b: ((k + 1) % n, 2), + alpha: [0.0, characteristic(n, true)], + beta: [1.0, 1.0], + ends: [SpokeEnd::Loop { edge, t }, SpokeEnd::Inner(0)], + }); } - let spokes = (0..n).map(|k| (k, (k + 1) % n)).collect(); - Ok(Quilt { patches, slots, unknowns, pieces, spokes, feet }) + let [cu, cv] = patches.first().map(Patch::counts).ok_or_else(|| SplineError::Invalid("patch".into()))?; + Ok(Quilt { patches, slots, unknowns, pieces, spokes, inner: vec![(0, cu - 1, cv - 1)], layout: Layout::Centre }) + } + + /// The ring (module docs): on four sides `P_k → P_{k+1}` of a loop with fewer than three + /// corners, ring patch `k` has the side along `v = 0`, spoke `r_{k+1}` (from `P_{k+1}` to the + /// inner corner `Q_{k+1}`) up `u = 1`, the inner patch's side `i_k` (from `Q_k` to `Q_{k+1}`) + /// along `v = 1` and spoke `r_k` down `u = 0`; the inner patch (patch 4) has the corners + /// `Q_0 … Q_3` and `i_k` as its side `k`. Every point of the loop is on two patches' sides, so + /// no patch has a corner where the loop runs straight on. The spaces: one along the loop and + /// the inner sides (holding each side and its reversal, so the inner patch holds `i_2` and + /// `i_3` run back), one along the spokes; `Q_k` starts half way from `P_k` to the centre. + fn ring(loop_sides: &[Vec], whole: &[Curve], extra: &[Vec; 2], degree: usize) -> SplineResult { + let bad = || SplineError::Invalid("a ring of four sides".into()); + if whole.len() != 4 || loop_sides.len() != 4 { + return Err(bad()); + } + let start = |k: usize| whole.get(k).and_then(|c| c.points.first().copied()).unwrap_or(HPoint::ZERO); + let middle: Vec = whole + .iter() + .map(|c| c.space.eval(&c.points, 0.5, 0)) + .collect::>>()? + .into_iter() + .filter_map(HPoint::project) + .collect(); + let centre = middle.iter().fold(Vec3::ZERO, |s, p| s + *p) * (1.0 / middle.len().max(1) as f64); + let q: Vec = (0..4).map(|k| start(k).project().map_or(HPoint::ZERO, |p| HPoint::weighted(p + (centre - p) * 0.5, 1.0))).collect(); + let at = |v: &[HPoint], k: usize| v.get(k % 4).copied().unwrap_or(HPoint::ZERO); + let radial: Vec = (0..4).map(|k| line(start(k), at(&q, k))).collect::>()?; + let inner: Vec = (0..4).map(|k| line(at(&q, k), at(&q, k + 1))).collect::>()?; + let reversed = |c: &Curve| piece_on_unit(c.space.degree(), c.space.knots(), &c.points, [0.0, 1.0], true); + let backs: Vec = whole.iter().map(reversed).collect::>()?; + let along: Vec<&Curve> = whole.iter().chain(&backs).chain(&inner).collect(); + let (ring_space, rows) = compatible(&along, &extra[0], degree)?; + let (spoke_space, spoke_rows) = compatible(&radial.iter().collect::>(), &extra[0], degree)?; + let row = |v: &[Vec], k: usize| v.get(k).cloned().ok_or_else(bad); + let back = |mut r: Vec| { + r.reverse(); + r + }; + let mut patches = Vec::with_capacity(5); + for k in 0..4 { + patches.push(coons( + [ring_space.clone(), spoke_space.clone()], + &row(&rows, k)?, + &row(&rows, 8 + k)?, + &row(&spoke_rows, k)?, + &row(&spoke_rows, (k + 1) % 4)?, + false, + )?); + } + patches.push(coons( + [ring_space.clone(), ring_space.clone()], + &row(&rows, 8)?, + &back(row(&rows, 10)?), + &back(row(&rows, 11)?), + &row(&rows, 9)?, + false, + )?); + let (cu, cv) = (ring_space.count(), spoke_space.count()); + let mut unknowns = 0usize; + let mut next = || { + unknowns += 1; + unknowns - 1 + }; + let q_id: Vec = (0..4).map(|_| next()).collect(); + let spoke_ids: Vec> = (0..4).map(|_| (1..cv.saturating_sub(1)).map(|_| next()).collect()).collect(); + let side_ids: Vec> = (0..4).map(|_| (1..cu.saturating_sub(1)).map(|_| next()).collect()).collect(); + // Point m (from Q_k) of the inner side i_k, and point j (from P_k) of the spoke r_k. + let on_side = |k: usize, m: usize| -> Option { + if m == 0 { + q_id.get(k % 4).copied() + } else if m + 1 == cu { + q_id.get((k + 1) % 4).copied() + } else { + side_ids.get(k % 4).and_then(|s| s.get(m - 1)).copied() + } + }; + let on_spoke = |k: usize, j: usize| -> Option { + if j + 1 == cv { q_id.get(k % 4).copied() } else { spoke_ids.get(k % 4).and_then(|s| s.get(j - 1)).copied() } + }; + let mut slots = Vec::with_capacity(5); + for k in 0..4 { + let mut s = vec![None; cu * cv]; + for j in 1..cv { + for i in 0..cu { + let id = if i == 0 { + on_spoke(k, j) + } else if i + 1 == cu { + on_spoke(k + 1, j) + } else if j + 1 == cv { + on_side(k, i) + } else { + Some(next()) + }; + if let Some(slot) = s.get_mut(i + cu * j) { + *slot = Some(id.ok_or_else(bad)?); + } + } + } + slots.push(s); + } + let mut s = vec![None; cu * cu]; + for j in 0..cu { + for i in 0..cu { + let id = if j == 0 { + on_side(0, i) + } else if i + 1 == cu { + on_side(1, j) + } else if j + 1 == cu { + on_side(2, cu - 1 - i) + } else if i == 0 { + on_side(3, cu - 1 - j) + } else { + Some(next()) + }; + if let Some(slot) = s.get_mut(i + cu * j) { + *slot = Some(id.ok_or_else(bad)?); + } + } + } + slots.push(s); + let pieces = loop_sides.iter().enumerate().flat_map(|(k, side)| pieces_between(side, k, [0.0, 1.0], k, 0)).collect(); + // The seams: spoke r_k between ring patches k − 1 (its u = 1) and k (its u = 0), glued at + // P_k by what the loop's two sides there ask (their derivatives against each other, and + // the spoke's), at Q_k by the valence-3 characteristic; side i_k between ring patch k + // (its v = 1, from Q_{k+1} to Q_k) and the inner patch (its side k). + let mut spokes = Vec::with_capacity(8); + for k in 0..4 { + let prev = (k + 3) % 4; + let (Some(a), Some(b)) = (patches.get(prev), patches.get(k)) else { return Err(bad()) }; + let (da, db) = (a.eval(1.0, 0.0, 1, 0)?, b.eval(0.0, 0.0, 1, 0)? * -1.0); + let l = b.eval(0.0, 0.0, 0, 1)?; + let (beta, alpha) = foot_gluing(da, db, l); + let foot = loop_sides.get(k).and_then(|s| s.first()).map(|sp| SpokeEnd::Loop { edge: sp.edge, t: sp.range[0] }).ok_or_else(bad)?; + spokes.push(Seam { + a: (prev, 1), + b: (k, 3), + alpha: [alpha, characteristic(3, true)], + beta: [beta, 1.0], + ends: [foot, SpokeEnd::Inner(k)], + }); + } + for k in 0..4 { + let towards = characteristic(3, true); + spokes.push(Seam { + a: (k, 2), + b: (4, k), + alpha: [-towards, towards], + beta: [1.0, 1.0], + ends: [SpokeEnd::Inner((k + 1) % 4), SpokeEnd::Inner(k)], + }); + } + let last = cu - 1; + Ok(Quilt { + patches, + slots, + unknowns, + pieces, + spokes, + inner: vec![(4, 0, 0), (4, last, 0), (4, last, last), (4, 0, last)], + layout: Layout::Ring, + }) } /// Most free control points one solve takes, one patch or several about a centre. @@ -1198,12 +1479,18 @@ pub fn iso_fill( if !(spec.corner_turn.is_finite() && spec.corner_turn >= 0.0) { return invalid("the corner turn must be a finite angle"); } - let sides = loop_sides(edges, spec.corner_turn); + let (sides, cornered) = loop_sides(edges, spec.corner_turn); + if !cornered && sides.len() != 4 { + return Err(SplineError::Unsupported(format!( + "a patch on a loop of {} edges with fewer than three corners (such a loop is filled in four pieces)", + sides.len() + ))); + } let corners = corner_orders(edges, &sides, &mut *across, size, spec.angle_tol)?; let mut extra: [Vec; 2] = [Vec::new(), Vec::new()]; let mut spans = [1usize; 2]; loop { - let quilt = Quilt::new(edges, &sides, &extra, degree)?; + let quilt = Quilt::new(edges, &sides, !cornered, &extra, degree)?; let stations = stations(edges, &quilt, &mut *across, &corners)?; let spaces = quilt.patches.first().map(|p| p.spaces.clone()).ok_or_else(|| SplineError::Invalid("no patch".into()))?; let shared = !quilt.spokes.is_empty(); @@ -1303,27 +1590,40 @@ fn finished(quilt: Quilt, achieved: Achieved) -> SplineResult { let [from, to] = pc.piece.range; sides.push(((pc.side, pc.along[0]), IsoSide::Edge { edge: pc.piece.edge, from, to, uv: [lerp(pc.along[0]), lerp(pc.along[1])] })); } - if !quilt.spokes.is_empty() { - // Spoke k up u = 1 (from its midpoint to the centre), spoke k − 1 back along v = 1. - sides.push(((1, 0.0), IsoSide::Spoke { spoke: k, forward: true, uv: side_uv(1) })); - sides.push(((2, 0.0), IsoSide::Spoke { spoke: (k + n - 1) % n, forward: false, uv: side_uv(2) })); + for (i, seam) in quilt.spokes.iter().enumerate() { + if seam.a.0 == k { + sides.push(((seam.a.1, 0.0), IsoSide::Spoke { spoke: i, forward: true, uv: side_uv(seam.a.1) })); + } + if seam.b.0 == k { + sides.push(((seam.b.1, 0.0), IsoSide::Spoke { spoke: i, forward: false, uv: side_uv(seam.b.1) })); + } } // The loop's order round the patch. sides.sort_by(|a, b| a.0.0.cmp(&b.0.0).then(a.0.1.total_cmp(&b.0.1))); patches.push(IsoPatch { surface: patch.surface()?, sides: sides.into_iter().map(|(_, s)| s).collect() }); } - let spokes = quilt - .spokes - .iter() - .filter_map(|&(left, _)| { - let p = quilt.patches.get(left)?; - let [cu, cv] = p.counts(); - let weights = p.rational.then(|| (0..cv).map(|j| p.weight(cu - 1, j)).collect()); - let along = &p.spaces[1]; - Some(IsoEdge { degree: along.degree(), knots: along.knots().to_vec(), points: (0..cv).map(|j| p.at(cu - 1, j)).collect(), weights }) - }) - .collect(); - Ok(IsoFill { patches, spokes, feet: quilt.feet.clone(), achieved }) + let spokes = quilt.spokes.iter().map(|seam| boundary_row(&quilt, seam.a)).collect::>()?; + let inner = quilt.inner.iter().map(|&(p, i, j)| quilt.patches.get(p).map(|x| x.at(i, j)).unwrap_or(Vec3::ZERO)).collect(); + Ok(IsoFill { patches, spokes, ends: quilt.spokes.iter().map(|s| s.ends).collect(), inner, achieved }) +} + +/// The boundary row of side `s` of patch `p` as a curve in the side's running direction. +fn boundary_row(quilt: &Quilt, (p, s): (usize, usize)) -> SplineResult { + let patch = quilt.patches.get(p).ok_or_else(|| SplineError::Invalid("patch".into()))?; + let [cu, cv] = patch.counts(); + let space = patch.spaces.get(s % 2).ok_or_else(|| SplineError::Invalid("space".into()))?; + let cells: Vec<(usize, usize)> = match s { + 0 => (0..cu).map(|i| (i, 0)).collect(), + 1 => (0..cv).map(|j| (cu - 1, j)).collect(), + 2 => (0..cu).rev().map(|i| (i, cv - 1)).collect(), + _ => (0..cv).rev().map(|j| (0, j)).collect(), + }; + let mut knots = space.knots().to_vec(); + if s >= 2 { + knots = knots.iter().rev().map(|k| 1.0 - k).collect(); + } + let weights = patch.rational.then(|| cells.iter().map(|&(i, j)| patch.weight(i, j)).collect()); + Ok(IsoEdge { degree: space.degree(), knots, points: cells.iter().map(|&(i, j)| patch.at(i, j)).collect(), weights }) } /// Stations along a side: [`STATIONS_PER_SPAN`] a span and [`STATIONS_PER_POINT`] per control @@ -1419,14 +1719,28 @@ fn solve(mut quilt: Quilt, stations: &[Station], spec: &IsoSpec) -> SplineResult linkage_rows(&mut rows, patch, s, span(patch).get(1 - s.side % 2).copied().unwrap_or(1.0))?; } // Across each spoke the two patches' derivatives away from it are opposed, up to the - // gluing function's share of the derivative along it (module docs). - let gluing = 2.0 * (std::f64::consts::TAU / quilt.patches.len() as f64).cos(); - for &(left, right) in &quilt.spokes { - let (Some(a), Some(b)) = (quilt.patches.get(left), quilt.patches.get(right)) else { continue }; + // gluing functions (module docs). + for seam in &quilt.spokes { + let ((pa, sa), (pb, sb)) = (seam.a, seam.b); + let (Some(a), Some(b)) = (quilt.patches.get(pa), quilt.patches.get(pb)) else { continue }; for i in 0..spoke_count { let t = (i as f64 + 0.5) / spoke_count as f64; - let along: Vec<(usize, usize, f64)> = a.row(1.0, t, 0, 1)?.into_iter().map(|(i, j, c)| (i, j, -gluing * t * c)).collect(); - on_each_axis(&mut rows, &[(left, a.row(1.0, t, 1, 0)?), (right, b.row(t, 1.0, 0, 1)?), (left, along)], Vec3::ZERO, 1.0); + let (ua, ub) = seam.at(t); + let (alpha, beta) = seam.gluing(t); + // Away from each patch: against its derivative into it. + let ((_, (da, ea, ga)), (_, (db, eb, gb))) = (station_on(sa, t), station_on(sb, 1.0 - t)); + let (la, ma, ha) = along_side(sa); + let scaled = |row: Vec<(usize, usize, f64)>, c: f64| row.into_iter().map(|(i, j, x)| (i, j, c * x)).collect::>(); + on_each_axis( + &mut rows, + &[ + (pa, scaled(a.row(ua[0], ua[1], da, ea)?, -ga)), + (pb, scaled(b.row(ub[0], ub[1], db, eb)?, -gb * beta)), + (pa, scaled(a.row(ua[0], ua[1], la, ma)?, -alpha * ha)), + ], + Vec3::ZERO, + 1.0, + ); } } for (p, (patch, uv)) in spec.points.iter().zip(&located) { @@ -2004,15 +2318,16 @@ fn measure( } } } - // Across the spokes, clear of the centre. - for &(left, right) in &quilt.spokes { - let (Some(a), Some(b)) = (quilt.patches.get(left), quilt.patches.get(right)) else { continue }; + // Across the spokes, clear of the vertices inside the loop. + for seam in &quilt.spokes { + let (Some(a), Some(b)) = (quilt.patches.get(seam.a.0), quilt.patches.get(seam.b.0)) else { continue }; for i in 0..spoke_count { let t = (i as f64 + 0.5) / spoke_count as f64; - if let (Some(na), Some(nb)) = (a.normal(1.0, t), b.normal(t, 1.0)) { + let (ua, ub) = seam.at(t); + if let (Some(na), Some(nb)) = (a.normal(ua[0], ua[1]), b.normal(ub[0], ub[1])) { let a = na.dot(nb).clamp(-1.0, 1.0).acos(); r.normal_deviation_all = r.normal_deviation_all.max(a); - if t < 1.0 - CLASH_SHARE { + if !seam.near_inner(t, CLASH_SHARE) { r.normal_deviation = r.normal_deviation.max(a); r.normal_excess = r.normal_excess.max(a); along.angle = along.angle.map(|x| x.max(a)); @@ -2136,19 +2451,30 @@ fn crowned(mut quilt: Quilt, stations: &[Station], crown: f64, size: f64) -> Spl } } let out = if facing < 0.0 { -1.0 } else { 1.0 }; - let about_centre = !quilt.spokes.is_empty(); - let bump = |x: f64, y: f64| if about_centre { x * (2.0 - x) * y * (2.0 - y) } else { 16.0 * x * (1.0 - x) * y * (1.0 - y) }; + let layout = quilt.layout; + let about_centre = layout != Layout::One; + let bump = |k: usize, x: f64, y: f64| match layout { + Layout::One => 16.0 * x * (1.0 - x) * y * (1.0 - y), + Layout::Centre => x * (2.0 - x) * y * (2.0 - y), + // The ring's patches rise from the loop (v = 0) to one at the inner patch, which is one. + Layout::Ring if k < 4 => y * (2.0 - y), + Layout::Ring => 1.0, + }; let mut moved = std::collections::HashMap::new(); for (k, patch) in quilt.patches.iter().enumerate() { let [cu, cv] = patch.counts(); let (gu, gv) = (greville(&patch.spaces[0]), greville(&patch.spaces[1])); + // The rows clear of those the conditions hold: from the loop's sides of each patch (a + // ring patch's sides across the loop are spokes, the inner patch has none). + let (ring, inside) = (layout == Layout::Ring && k < 4, layout == Layout::Ring && k >= 4); + let first = |across_loop: bool| if inside || (ring && across_loop) { 0 } else { held }; let far = |c: usize| if about_centre { c } else { c.saturating_sub(held) }; - for j in held..far(cv) { - for i in held..far(cu) { + for j in first(false)..far(cv) { + for i in first(true)..far(cu) { let Some(Some(id)) = quilt.slots.get(k).and_then(|s| s.get(patch.flat(i, j))) else { continue }; let (x, y) = (gu.get(i).copied().unwrap_or(0.5), gv.get(j).copied().unwrap_or(0.5)); let Some(n) = patch.normal(x, y) else { continue }; - moved.entry(*id).or_insert_with(|| patch.at(i, j) + n * (out * crown * size * bump(x, y))); + moved.entry(*id).or_insert_with(|| patch.at(i, j) + n * (out * crown * size * bump(k, x, y))); } } } diff --git a/crates/geom/src/fill_iso_tests.rs b/crates/geom/src/fill_iso_tests.rs index 1135d701b2..f2e26a4426 100644 --- a/crates/geom/src/fill_iso_tests.rs +++ b/crates/geom/src/fill_iso_tests.rs @@ -210,7 +210,7 @@ fn hostile_edges_are_refused() { #[test] fn a_rational_patch_s_derivatives_are_its_differences() { let edges = window(); - let q = Quilt::new(&edges, &loop_sides(&edges, spec().corner_turn), &[Vec::new(), Vec::new()], 3).unwrap(); + let q = Quilt::new(&edges, &loop_sides(&edges, spec().corner_turn).0, false, &[Vec::new(), Vec::new()], 3).unwrap(); let p = &q.patches[0]; assert!(p.rational); let (u, v, h) = (0.3, 0.6, 1e-5); @@ -272,8 +272,8 @@ fn any_fold(fill: &IsoFill) -> bool { } /// A tube's end: the circle in four quarter arcs, G1 to the tube's wall (its normal radial, the -/// wall going on upwards). Every corner of the loop is a smooth vertex, where the cross derivative -/// of one side is the next side's tangent, along the edge itself. +/// wall going on upwards). Every vertex of the loop is smooth: the cross derivative of a side +/// there is the next side's tangent, along the edge itself. #[test] fn a_tube_end_capped_g1_holds_the_wall_and_does_not_fold() { let edges = circle_quarters(30.0, 10.0); @@ -283,6 +283,19 @@ fn a_tube_end_capped_g1_holds_the_wall_and_does_not_fold() { eprintln!("tube end g1: {} patches, {:?}", fill.patches.len(), fill.achieved); assert!(fill.achieved.normal_excess <= s.angle_tol); assert!(!any_fold(&fill), "the cap folds"); + // No corner of the loop: four patches round it and one inside, none with a flat corner + // (two derivatives parallel), which a patch on the circle's own quarters would have. + assert_eq!(fill.patches.len(), 5, "the ring and the inner patch"); + for (k, p) in fill.patches.iter().enumerate() { + for c in [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] { + let (angle, ..) = corner_angle(p, c); + assert!(angle > 0.5 && angle < 179.5, "patch {k}: a flat corner at {c:?}: {angle}°"); + } + } + // A crown fills the middle out of the tube, the inner patch whole. + let crowned = iso_fill(&edges, &mut beside_by(&edges, radial), &IsoSpec { crown: 0.1, ..s }).unwrap(); + let top = |f: &IsoFill| f.patches[4].surface.eval(0.5, 0.5).unwrap().z; + assert!(top(&crowned) > top(&fill) + 0.5, "{} {}", top(&crowned), top(&fill)); } /// The angle (degrees) between a patch's two derivatives at a corner of its square, and their diff --git a/crates/kernel/src/fill.rs b/crates/kernel/src/fill.rs index 6b19e76442..f7c2e095f3 100644 --- a/crates/kernel/src/fill.rs +++ b/crates/kernel/src/fill.rs @@ -820,7 +820,7 @@ fn patch_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], surface: b /// straight line in (u, v) the patch gives its side. Its history is over `[body]`: every face is /// new, each loop edge (or half) a piece of the body's own, each spoke new. fn quilt_body(body: &Body, record: &b::Brep, chain: &[Piece], fill: &IsoFill, tol: f64) -> Result { - use solidcraft_geom::fill_iso::IsoSide; + use solidcraft_geom::fill_iso::{IsoSide, SpokeEnd}; let fail = |m: &str| KernelError::Failed(format!("patch: {m}")); let index = |n: usize| u32::try_from(n).map_err(|_| fail("budget")); let mut vertices: Vec<[f64; 3]> = Vec::new(); @@ -831,13 +831,10 @@ fn quilt_body(body: &Body, record: &b::Brep, chain: &[Piece], fill: &IsoFill, to let mut origin: Vec> = Vec::new(); let mut piece_of: std::collections::HashMap<(usize, u64, u64), u32> = std::collections::HashMap::new(); let mut spoke_of: std::collections::HashMap = std::collections::HashMap::new(); - let centre = match fill.spokes.first().and_then(|s| s.points.last()) { - Some(c) => { - vertices.push([c.x, c.y, c.z]); - Some(0u32) - } - None => None, - }; + // The fill's vertices inside the loop first. + for c in &fill.inner { + vertices.push([c.x, c.y, c.z]); + } let mut faces = Vec::with_capacity(fill.patches.len()); for patch in &fill.patches { let mut uses = Vec::with_capacity(patch.sides.len()); @@ -881,14 +878,23 @@ fn quilt_body(body: &Body, record: &b::Brep, chain: &[Piece], fill: &IsoFill, to Some(id) => *id, None => { let curve = fill.spokes.get(spoke).ok_or_else(|| fail("spoke"))?; - // Its foot on the loop, in the record edge's parameter as the sides' - // pieces are (the same bits where a side's piece ends there). - let &(k, t) = fill.feet.get(spoke).ok_or_else(|| fail("spoke"))?; - let &Piece { edge: e, forward, interval: [plo, phi] } = chain.get(k).ok_or_else(|| fail("spoke"))?; - let edge = record.edges.get(e).ok_or_else(|| fail("edge reference"))?; - let [a, z] = edge.interval; - let start = vertex_on(record, &mut vertices, &mut vertex_of, &mut middle_of, e, on_record(a, z, [plo, phi], forward, t))?; - let end = centre.ok_or_else(|| fail("centre"))?; + // Its ends: on the loop, in the record edge's parameter as the sides' + // pieces are (the same bits where a side's piece ends there), or inside. + let ends = fill.ends.get(spoke).ok_or_else(|| fail("spoke"))?; + let mut at = [0u32; 2]; + for (slot, end) in at.iter_mut().zip(ends) { + *slot = match *end { + SpokeEnd::Inner(i) if i < fill.inner.len() => index(i)?, + SpokeEnd::Inner(_) => return Err(fail("spoke end")), + SpokeEnd::Loop { edge: k, t } => { + let &Piece { edge: e, forward, interval: [plo, phi] } = chain.get(k).ok_or_else(|| fail("spoke"))?; + let edge = record.edges.get(e).ok_or_else(|| fail("edge reference"))?; + let [a, z] = edge.interval; + vertex_on(record, &mut vertices, &mut vertex_of, &mut middle_of, e, on_record(a, z, [plo, phi], forward, t))? + } + }; + } + let [start, end] = at; let id = index(edges.len())?; let spline = match &curve.weights { Some(w) => b::Curve::RationalSpline {