Skip to main content

brep_kernel/construction/
solid_sweep_caps.rs

1//! The WHOLE sweep of a solid along a one-parameter motion, ends included:
2//! caps and wall.
3//!
4//! Along a screw (a turn is a screw of zero advance, a slide one of zero turn)
5//! the velocity field, read in the tool's own frame, does not change with time:
6//! at a boundary point x with outward normal n it is
7//! `g(x, n) = (ω e × (x − a) + c e) · n`. So the tool's boundary splits once
8//! and for all into FRONT (g > 0) and BACK (g < 0) parts, and the GRAZING LOOP
9//! K between them (g = 0 inside a face, or a sharp edge whose two faces have
10//! opposite signs) is fixed on the tool. Where every point of space is inside
11//! the moving tool for ONE interval of time, the sweep over [t0, t1] is bounded
12//! by exactly
13//!
14//! - the back of the tool at t0 (where points start to be covered),
15//! - the front of the tool at t1 (where they stop),
16//! - the WALL: K carried by the motion from t0 to t1.
17//!
18//! The body is assembled directly from those faces with shared edges, never as
19//! a boolean of the tool with a tube (the two meet tangentially along K).
20//!
21//! The one-interval condition is CHECKED, not assumed: points just outside
22//! every built face are walked through the whole motion, and a sweep that
23//! covers any of them (a fold, a buried cap, a turn running into itself) is
24//! refused by name.
25//!
26//! Tools: a convex polyhedron (K a polygon of edges and face chords); a solid
27//! of revolution whose faces are cylinders or cones (split along standing
28//! generators, or along two grazing curves moving round them, fitted), flat
29//! ends wholly front or back, a hemisphere from a pole to an equator or a whole
30//! ball (split along a great circle), or a ring (torus, split along its two
31//! grazing curves round it). Caps keep the tool's exact surfaces; walls are
32//! the K curves carried by quintic splines through the exact motion and its
33//! first two derivatives (error h⁶, 48 spans a turn), a row per quarter turn.
34//!
35//! Moving grazing loops: [`sweep_solid_along`] (a solid of revolution carried
36//! by a frame along a planar curve) and [`sweep_solid_tumbled`] (two turns
37//! about skew axes) split the tool at sample times, sample each piece of K
38//! exactly and loft the walls through time; a tumble that passes over a point
39//! twice is built from one-pass spans of its time, joined.
40//! [`sweep_prism_along`] sweeps a prism along a frame by the plane region the
41//! moving outline covers (an arrangement of its corners', grazing points' and
42//! end positions' paths), extruded. [`sweep_solid_parts`] returns a folding
43//! polyhedron's sweep as the sweeps of its halves through the motion's axis.
44//!
45//! [`sweep_solid_clipped`] adds the case the caller only needs inside a
46//! region: a polyhedron TURNED about the region's axis with both end positions
47//! outside the region's wedge fills the wedge with its (ρ, z) shadow turned,
48//! bounded by exact conic arcs (edges) and segments (face chords).
49use crate::solid_sweep::outline_signed_distance;
50use crate::topology::{CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord};
51use crate::{make_line, make_revolution, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, NurbsSurface, OrRefuse, OutlinePiece, SolidMotion, Vec3, Vec4};
52use std::collections::HashMap;
53use std::f64::consts::{PI, TAU};
54
55/// A tool the whole sweep takes.
56#[derive(Clone, Debug)]
57pub enum SolidTool {
58    /// A convex polyhedron: its corners, and its faces as loops of corner
59    /// indices (either winding; outward is found from the centroid).
60    Polyhedron { vertices: Vec<Vec3>, faces: Vec<Vec<usize>> },
61    /// A solid of revolution: the region `meridian` bounds in the
62    /// half-plane spanned by `radial` (first coordinate, the distance from the
63    /// axis, ≥ 0) and `axis_direction` (second coordinate), turned about the
64    /// axis through `axis_origin` (as [`crate::SweptTool::Revolved`]).
65    Revolved { axis_origin: Vec3, axis_direction: Vec3, radial: Vec3, meridian: Vec<OutlinePiece> },
66}
67
68/// Samples of a fitted pcurve: the image of its edge through the surface's
69/// exact inverse, interpolated (quintic). A rational chart's parameter has a
70/// second-derivative jump at its span joints, so the interpolant errs there
71/// as h³; at 576 that is below 1e-10 of a face's area.
72const PCURVE_SAMPLES: usize = 576;
73
74/// The prefix of every refusal of this operation.
75pub const SOLID_SWEEP_WHOLE_REFUSAL: &str = "sweepSolid";
76
77fn refuse(what: &'static str, message: String) -> KernelRefusal {
78    KernelRefusal::unsupported(KernelStage::Classify, what, format!("{SOLID_SWEEP_WHOLE_REFUSAL}: {message}"))
79}
80
81/// The stable slug of an unsupported-geometry refusal of this module.
82fn refused_as(error: &KernelRefusal) -> Option<&str> {
83    match &error.class {
84        crate::RefusalClass::UnsupportedGeometry { what } => Some(what.as_str()),
85        _ => None,
86    }
87}
88
89fn input(what: &'static str, message: String) -> KernelRefusal {
90    KernelRefusal::input(KernelStage::Collect, what, format!("{SOLID_SWEEP_WHOLE_REFUSAL}: {message}"))
91}
92
93fn frag<T>(result: Result<T, String>, what: &'static str) -> Result<T, KernelRefusal> {
94    result.or_refuse(KernelStage::Fragment, what)
95}
96
97/// The motion `x ↦ a + R(ω t)(x − a) + (c t + c₂ t²) e`: a screw (c₂ = 0;
98/// a turn has c = 0, a slide ω = 0), or a turn whose rise grows as it turns.
99#[derive(Clone, Copy, Debug)]
100struct Screw {
101    a: Vec3,
102    e: Vec3,
103    omega: f64,
104    c: f64,
105    c2: f64,
106    t0: f64,
107    t1: f64,
108    /// A frame riding a curve instead (then `a` is the curve's start, `e` the
109    /// frame's fixed up direction, `omega` a bound on its turning rate and `c`
110    /// on its speed).
111    frame: Option<Frame>,
112    /// Two turns about fixed axes instead (then `a` is the first axis' point).
113    turns: Option<Turns>,
114    /// The time whose velocity field splits the tool (default: the middle).
115    frozen: Option<f64>,
116    /// How many spans a ring's grazing loop is sampled over, per half of the
117    /// ring (`LOFT_SAMPLES` unless a loft found its rows too coarse).
118    loop_samples: usize,
119    /// How far the frame's origin travels from `a` (for speed bounds).
120    extent: f64,
121}
122
123/// Two turns driven by one parameter: first `rates[0]·t` about the first
124/// axis, then `rates[1]·t` about the second; both axes fixed in the world.
125#[derive(Clone, Copy, Debug)]
126struct Turns {
127    points: [Vec3; 2],
128    directions: [Vec3; 2],
129    rates: [f64; 2],
130}
131
132impl Turns {
133    fn apply(&self, t: f64, x: Vec3) -> Vec3 {
134        let y = self.points[0].add(x.sub(self.points[0]).rotate_about(self.directions[0], self.rates[0] * t));
135        self.points[1].add(y.sub(self.points[1]).rotate_about(self.directions[1], self.rates[1] * t))
136    }
137    fn invert(&self, t: f64, y: Vec3) -> Vec3 {
138        let x = self.points[1].add(y.sub(self.points[1]).rotate_about(self.directions[1], -self.rates[1] * t));
139        self.points[0].add(x.sub(self.points[0]).rotate_about(self.directions[0], -self.rates[0] * t))
140    }
141    /// Velocity of the tool point x at t, read back in the tool's frame.
142    fn velocity(&self, t: f64, x: Vec3) -> Vec3 {
143        let first = x.sub(self.points[0]).rotate_about(self.directions[0], self.rates[0] * t);
144        let y = self.points[0].add(first);
145        let moved = y.sub(self.points[1]).rotate_about(self.directions[1], self.rates[1] * t);
146        let inner = self.directions[0].cross(first).scale(self.rates[0]);
147        let world = self.directions[1].cross(moved).scale(self.rates[1]).add(inner.rotate_about(self.directions[1], self.rates[1] * t));
148        world.rotate_about(self.directions[1], -self.rates[1] * t).rotate_about(self.directions[0], -self.rates[0] * t)
149    }
150}
151
152/// A frame riding a planar cubic Bézier curve: origin on the curve, first
153/// axis along its tangent, a fixed up direction square to its plane.
154#[derive(Clone, Copy, Debug)]
155struct Frame {
156    control: [Vec3; 4],
157    up: Vec3,
158    /// The tangent's direction at the start.
159    start_tangent: Vec3,
160}
161
162impl Frame {
163    fn point(&self, s: f64) -> Vec3 {
164        let [p0, p1, p2, p3] = self.control;
165        let r = 1.0 - s;
166        p0.scale(r * r * r).add(p1.scale(3.0 * r * r * s)).add(p2.scale(3.0 * r * s * s)).add(p3.scale(s * s * s))
167    }
168    fn first(&self, s: f64) -> Vec3 {
169        let [p0, p1, p2, p3] = self.control;
170        let r = 1.0 - s;
171        p1.sub(p0).scale(3.0 * r * r).add(p2.sub(p1).scale(6.0 * r * s)).add(p3.sub(p2).scale(3.0 * s * s))
172    }
173    fn second(&self, s: f64) -> Vec3 {
174        let [p0, p1, p2, p3] = self.control;
175        p2.sub(p1.scale(2.0)).add(p0).scale(6.0 * (1.0 - s)).add(p3.sub(p2.scale(2.0)).add(p1).scale(6.0 * s))
176    }
177    /// The frame's turn from its start, about `up`.
178    fn angle(&self, s: f64) -> f64 {
179        let t = self.first(s);
180        self.up.dot(self.start_tangent.cross(t)).atan2(self.start_tangent.dot(t))
181    }
182    fn angle_rate(&self, s: f64) -> f64 {
183        let (d1, d2) = (self.first(s), self.second(s));
184        self.up.dot(d1.cross(d2)) / d1.dot(d1)
185    }
186}
187
188/// A turn about an axis whose rise along it grows as it turns: turned by t
189/// (radians, right-handed about `axis_direction`) the tool has risen
190/// `rise[0] t + rise[1] t²`, for t from `from` to `to` (a variable-lead
191/// screw).
192#[derive(Clone, Copy, Debug)]
193pub struct RisingTurn {
194    pub axis_origin: Vec3,
195    pub axis_direction: Vec3,
196    pub rise: [f64; 2],
197    pub from: f64,
198    pub to: f64,
199}
200
201impl Screw {
202    fn from_motion(motion: &SolidMotion) -> Result<Self, KernelRefusal> {
203        let [step] = motion.steps.as_slice() else {
204            return Err(refuse("motion_steps", "only a single screw, turn or slide is swept whole here".into()));
205        };
206        let e = step
207            .axis_direction
208            .normalized()
209            .map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
210        if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
211            return Err(input("range", "the motion's range must run forward".into()));
212        }
213        if step.turn == 0.0 && step.advance == 0.0 {
214            return Err(input("still", "the motion does not move".into()));
215        }
216        Ok(Self { a: step.axis_origin, e, omega: step.turn, c: step.advance, c2: 0.0, t0: motion.from, t1: motion.to, frame: None, turns: None, frozen: None,
217        loop_samples: LOFT_SAMPLES, extent: 0.0 })
218    }
219
220    fn from_rising(motion: &RisingTurn) -> Result<Self, KernelRefusal> {
221        let e = motion.axis_direction.normalized().map_err(|_| input("axis", "the motion's axis has no direction".into()))?;
222        if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
223            return Err(input("range", "the motion's range must run forward".into()));
224        }
225        // The rise's rate must keep one sign, or the tool's front and back swap.
226        let rate = |t: f64| motion.rise[0] + 2.0 * motion.rise[1] * t;
227        if rate(motion.from) * rate(motion.to) <= 0.0 {
228            return Err(refuse("rise_turns", "the rise stops or turns back within the motion".into()));
229        }
230        Ok(Self { a: motion.axis_origin, e, omega: 1.0, c: motion.rise[0], c2: motion.rise[1], t0: motion.from, t1: motion.to, frame: None, turns: None, frozen: None,
231        loop_samples: LOFT_SAMPLES, extent: 0.0 })
232    }
233
234    fn advance(&self, t: f64) -> f64 {
235        self.c * t + self.c2 * t * t
236    }
237
238    /// The fastest the motion advances along its axis.
239    fn rate_bound(&self) -> f64 {
240        (self.c + 2.0 * self.c2 * self.t0).abs().max((self.c + 2.0 * self.c2 * self.t1).abs())
241    }
242
243    fn apply(&self, t: f64, x: Vec3) -> Vec3 {
244        if let Some(turns) = &self.turns {
245            return turns.apply(t, x);
246        }
247        if let Some(frame) = &self.frame {
248            return frame.point(t).add(x.sub(self.a).rotate_about(frame.up, frame.angle(t)));
249        }
250        self.a.add(x.sub(self.a).rotate_about(self.e, self.omega * t)).add(self.e.scale(self.advance(t)))
251    }
252
253    fn invert(&self, t: f64, y: Vec3) -> Vec3 {
254        if let Some(turns) = &self.turns {
255            return turns.invert(t, y);
256        }
257        if let Some(frame) = &self.frame {
258            return self.a.add(y.sub(frame.point(t)).rotate_about(frame.up, -frame.angle(t)));
259        }
260        self.a.add(y.sub(self.e.scale(self.advance(t))).sub(self.a).rotate_about(self.e, -self.omega * t))
261    }
262
263    /// Velocity, in the tool's frame, of the tool point x (the same at every t
264    /// for a screw; for a growing rise, at the middle of the motion; the split
265    /// it gives is checked against both ends).
266    fn velocity(&self, x: Vec3) -> Vec3 {
267        self.velocity_at(self.frozen.unwrap_or(0.5 * (self.t0 + self.t1)), x)
268    }
269
270    fn velocity_at(&self, t: f64, x: Vec3) -> Vec3 {
271        if let Some(turns) = &self.turns {
272            return turns.velocity(t, x);
273        }
274        if let Some(frame) = &self.frame {
275            // In the riding frame: the speed along the start tangent and the
276            // turn about up through the frame's origin.
277            return frame.start_tangent.scale(frame.first(t).length()).add(frame.up.cross(x.sub(self.a)).scale(frame.angle_rate(t)));
278        }
279        self.e.cross(x.sub(self.a)).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t))
280    }
281
282    /// Value, first and second t-derivative of the path of the tool point x.
283    fn jet(&self, t: f64, x: Vec3) -> (Vec3, Vec3, Vec3) {
284        let r = x.sub(self.a).rotate_about(self.e, self.omega * t);
285        let axial = self.e.scale(r.dot(self.e));
286        let radial = r.sub(axial);
287        let value = self.a.add(r).add(self.e.scale(self.advance(t)));
288        let first = self.e.cross(radial).scale(self.omega).add(self.e.scale(self.c + 2.0 * self.c2 * t));
289        let second = radial.scale(-self.omega * self.omega).add(self.e.scale(2.0 * self.c2));
290        (value, first, second)
291    }
292
293    fn move_point(&self, t: f64, p: &Vec4) -> Result<Vec4, KernelRefusal> {
294        let x = frag(p.point(), "control_point")?;
295        Ok(Vec4::from_point(self.apply(t, x), p.w))
296    }
297
298    fn move_curve(&self, t: f64, curve: &NurbsCurve) -> Result<NurbsCurve, KernelRefusal> {
299        let points = curve.control_points.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?;
300        frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "moved_curve")
301    }
302
303    fn move_surface(&self, t: f64, surface: &NurbsSurface) -> Result<NurbsSurface, KernelRefusal> {
304        let mut rows = Vec::with_capacity(surface.control_points.len());
305        for row in &surface.control_points {
306            rows.push(row.iter().map(|p| self.move_point(t, p)).collect::<Result<Vec<_>, _>>()?);
307        }
308        frag(
309            NurbsSurface::new(surface.degree_u, surface.degree_v, surface.knots_u.clone(), surface.knots_v.clone(), rows),
310            "moved_surface",
311        )
312    }
313}
314
315/// The motion's times at the ends of its rows: each row of wall turns at most
316/// a quarter turn (long thin faces cost every later reading, and the cut).
317fn row_times(screw: &Screw) -> Vec<f64> {
318    let turned = (screw.omega * (screw.t1 - screw.t0)).abs();
319    let rows = ((turned / (0.5 * PI)).ceil() as usize).max(1);
320    // Breaks off the round fractions of the range, where a caller's workpiece
321    // faces tend to lie (a precaution: an edge lying in a face makes a
322    // degenerate cut).
323    let at = |k: usize| match k {
324        0 => screw.t0,
325        k if k == rows => screw.t1,
326        k => screw.t0 + (screw.t1 - screw.t0) * (k as f64 - 0.381966011250105) / rows as f64,
327    };
328    (0..=rows).map(at).collect()
329}
330
331/// Spans of the quintic carry over [ta, tb]: 48 per turn (error ≈ r·h⁶/46080
332/// ≈ 1e-10·r), at least 4.
333fn carry_segments(screw: &Screw, ta: f64, tb: f64) -> usize {
334    let turned = (screw.omega * (tb - ta)).abs();
335    ((turned / TAU * 48.0).ceil() as usize).max(4)
336}
337
338/// Knots of the C² quintic carry on [0, 1].
339fn carry_knots(segments: usize) -> Vec<f64> {
340    let mut knots = vec![0.0; 6];
341    for k in 1..segments {
342        knots.extend([k as f64 / segments as f64; 3]);
343    }
344    knots.extend([1.0; 6]);
345    knots
346}
347
348/// Control points of the C² quintic spline through the exact path of the tool
349/// point x, its first and second derivatives at `segments + 1` times. Affine
350/// in x, so carrying a rational curve's control points carries the curve.
351fn carry_points(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Vec<Vec3> {
352    let h = (tb - ta) / segments as f64;
353    let at = |k: usize| screw.jet(ta + h * k as f64, x);
354    let piece = |k: usize| {
355        let ((f0, d0, a0), (f1, d1, a1)) = (at(k), at(k + 1));
356        [
357            f0,
358            f0.add(d0.scale(h / 5.0)),
359            f0.add(d0.scale(2.0 * h / 5.0)).add(a0.scale(h * h / 20.0)),
360            f1.sub(d1.scale(2.0 * h / 5.0)).add(a1.scale(h * h / 20.0)),
361            f1.sub(d1.scale(h / 5.0)),
362            f1,
363        ]
364    };
365    let mut points = Vec::with_capacity(3 * segments + 3);
366    let first = piece(0);
367    points.extend_from_slice(&first[..3]);
368    for k in 0..segments - 1 {
369        let (here, next) = (piece(k), piece(k + 1));
370        points.push(here[3]);
371        points.push(here[4].scale(2.0).sub(here[3]));
372        points.push(next[2]);
373    }
374    let last = piece(segments - 1);
375    points.extend_from_slice(&last[3..]);
376    points
377}
378
379fn carry_curve(screw: &Screw, x: Vec3, ta: f64, tb: f64, segments: usize) -> Result<NurbsCurve, KernelRefusal> {
380    frag(
381        NurbsCurve::new(5, carry_knots(segments), carry_points(screw, x, ta, tb, segments).into_iter().map(|p| Vec4::from_point(p, 1.0)).collect()),
382        "carry_curve",
383    )
384}
385
386/// A boundary curve of the tool (in its own frame), from point `start` to
387/// point `end` along its parameter.
388struct ToolCurve {
389    curve: NurbsCurve,
390    start: usize,
391    end: usize,
392    degenerate: bool,
393    /// Exact points at `LOFT_SAMPLES + 1` even steps of a natural parameter
394    /// from start to end (meridian place, angle), where a grazing curve
395    /// moves on the tool and its wall is lofted through time.
396    samples: Option<Vec<Vec3>>,
397}
398
399/// Steps of a grazing curve's exact samples for a lofted wall.
400const LOFT_SAMPLES: usize = 128;
401
402/// A face of the tool after the split along K: its exact surface (normal
403/// outward), its side, and its loop (curve, traversed forward?, pcurve in the
404/// direction of traversal).
405struct Cap {
406    surface: NurbsSurface,
407    front: bool,
408    coedges: Vec<(usize, bool, NurbsCurve)>,
409    /// The surface's exact parameters of a point (in the tool's frame), where
410    /// known: a lofted edge's pcurve is then its exact image.
411    chart: Option<Chart>,
412}
413
414/// How a cap's surface is parameterized, to invert it exactly.
415#[derive(Clone, Copy, Debug)]
416enum Chart {
417    /// `plane_patch(origin, u, v, …)`: (x − origin)·u, (x − origin)·v.
418    Plane { origin: Vec3, u: Vec3, v: Vec3 },
419    /// `make_revolution(origin, w, generatrix, theta)` with the generatrix in the
420    /// meridian half-plane at angle `alpha` (from u1 towards u2).
421    Revolution { origin: Vec3, w: Vec3, u1: Vec3, u2: Vec3, alpha: f64, theta: f64, spans_u: usize, meridian: MeridianChart },
422}
423
424#[derive(Clone, Copy, Debug)]
425enum MeridianChart {
426    /// A segment from m0 to m1 in (ρ, z), parameter 0 -> 1.
427    Line { m0: [f64; 2], m1: [f64; 2] },
428    /// An arc about `centre` in (ρ, z) from angle `start` through `sweep`, as
429    /// `make_revolution` of a point makes it (`spans` arcs).
430    Arc { centre: [f64; 2], start: f64, sweep: f64, spans: usize },
431}
432
433impl Chart {
434    fn invert(&self, x: Vec3) -> [f64; 2] {
435        match *self {
436            Chart::Plane { origin, u, v } => {
437                let d = x.sub(origin);
438                [d.dot(u), d.dot(v)]
439            }
440            Chart::Revolution { origin, w, u1, u2, alpha, theta, spans_u, meridian } => {
441                let r = x.sub(origin);
442                let z = r.dot(w);
443                let rho = r.sub(w.scale(z)).length();
444                let mid = alpha + 0.5 * theta;
445                let psi = mid + (r.dot(u2).atan2(r.dot(u1)) - mid + PI).rem_euclid(TAU) - PI;
446                let u = crate::circle_angle_to_parameter(spans_u, theta, psi - alpha);
447                let v = match meridian {
448                    MeridianChart::Line { m0, m1 } => {
449                        let (a, b) = (m1[0] - m0[0], m1[1] - m0[1]);
450                        ((rho - m0[0]) * a + (z - m0[1]) * b) / (a * a + b * b)
451                    }
452                    MeridianChart::Arc { centre, start, sweep, spans } => {
453                        let mid = start + 0.5 * sweep;
454                        let angle = mid + ((z - centre[1]).atan2(rho - centre[0]) - mid + PI).rem_euclid(TAU) - PI;
455                        crate::circle_angle_to_parameter(spans, sweep, angle - start)
456                    }
457                };
458                [u, v]
459            }
460        }
461    }
462}
463
464/// The spans `make_revolution` gives a sweep of `theta`.
465fn spans_of(surface_knots_u: &[f64]) -> usize {
466    (surface_knots_u.len() - 6) / 2 + 1
467}
468
469/// The tool's boundary split along K, in the tool's own frame.
470struct Split {
471    points: Vec<Vec3>,
472    curves: Vec<ToolCurve>,
473    caps: Vec<Cap>,
474    /// Flat ends that slide within their own plane all through the motion.
475    slides: Vec<SlideDisc>,
476    /// Cylinders that slide along their own generators all through it.
477    slide_sides: Vec<SlideSide>,
478    scale: f64,
479    shape: Shape,
480}
481
482/// A flat end about the tool's axis whose every point moves within the
483/// end's own plane (a turn about a parallel axis, a slide across the tool's
484/// axis): neither front nor back. What it sweeps is one face of the sweep in
485/// that plane, bounded by the rim's arcs beside back faces where the motion
486/// starts, its arcs beside front faces where it stops, and the paths of the
487/// points between them. `rim`: the rim's arcs counter-clockwise about the
488/// outward normal `u × v`, each with its direction of traversal.
489struct SlideDisc {
490    centre: Vec3,
491    u: Vec3,
492    v: Vec3,
493    rim: Vec<(usize, bool)>,
494}
495
496/// A cylinder about the tool's axis slid along that axis: every point moves
497/// along its own generator. What it sweeps is the cylinder from one rim where
498/// that rim's neighbour sits (back at the start, front at the end) to the
499/// other rim where its neighbour sits. `patches`: per stretch between two
500/// seam angles, (the angle it starts at, its sweep, the start rim's arc, the
501/// end rim's arc, the rim points at the start angle: start rim, end rim).
502struct SlideSide {
503    origin: Vec3,
504    w: Vec3,
505    patches: Vec<(f64, f64, usize, usize, [usize; 2])>,
506}
507
508/// What the membership oracle measures.
509enum Shape {
510    Planes(Vec<(Vec3, f64)>),
511    Revolved { origin: Vec3, axis: Vec3, meridian: Vec<OutlinePiece> },
512}
513
514impl Split {
515    /// Signed distance to the tool in its own frame (negative inside).
516    fn distance(&self, x: Vec3) -> f64 {
517        match &self.shape {
518            Shape::Planes(planes) => planes.iter().map(|(n, d)| n.dot(x) - d).fold(f64::NEG_INFINITY, f64::max),
519            Shape::Revolved { origin, axis, meridian } => {
520                let r = x.sub(*origin);
521                let z = r.dot(*axis);
522                let rho = r.sub(axis.scale(z)).length();
523                outline_signed_distance(meridian, [rho, z])
524            }
525        }
526    }
527
528    /// K: each curve with a front cap on one side and a back cap on the
529    /// other, with the direction in which the back cap runs along it (the back
530    /// on its left seen from outside).
531    fn grazing(&self) -> Result<Vec<(usize, bool)>, KernelRefusal> {
532        let mut uses: HashMap<usize, Vec<(bool, bool)>> = HashMap::new();
533        for cap in &self.caps {
534            for &(curve, forward, _) in &cap.coedges {
535                uses.entry(curve).or_default().push((forward, cap.front));
536            }
537        }
538        let mut grazing = Vec::new();
539        for (curve, list) in uses {
540            if self.curves[curve].degenerate {
541                continue;
542            }
543            // A sliding end's rim carries no wall: it moves within the end's plane.
544            if list.len() == 1
545                && (self.slides.iter().any(|slide| slide.rim.iter().any(|&(c, _)| c == curve))
546                    || self.slide_sides.iter().any(|side| side.patches.iter().any(|patch| patch.2 == curve || patch.3 == curve)))
547            {
548                continue;
549            }
550            match list.as_slice() {
551                [(fa, front_a), (fb, front_b)] => {
552                    if fa == fb {
553                        return Err(refuse("split_topology", "two caps run one boundary curve the same way".into()));
554                    }
555                    if front_a != front_b {
556                        grazing.push((curve, if *front_a { *fb } else { *fa }));
557                    }
558                }
559                _ => return Err(refuse("split_topology", format!("a boundary curve is used {} times", list.len()))),
560            }
561        }
562        grazing.sort_unstable();
563        Ok(grazing)
564    }
565}
566
567fn uv_line(a: [f64; 2], b: [f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
568    frag(make_line(Vec3::new(a[0], a[1], 0.0), Vec3::new(b[0], b[1], 0.0)), "pcurve")
569}
570
571/// The plane through `origin` with in-plane axes u, v (u × v outward), as a
572/// patch covering `points`; and the map to its parameters.
573fn plane_patch(origin: Vec3, u: Vec3, v: Vec3, points: &[Vec3]) -> Result<NurbsSurface, KernelRefusal> {
574    let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
575    for p in points {
576        let q = [p.sub(origin).dot(u), p.sub(origin).dot(v)];
577        for k in 0..2 {
578            lo[k] = lo[k].min(q[k]);
579            hi[k] = hi[k].max(q[k]);
580        }
581    }
582    let at = |s: f64, t: f64| Vec4::from_point(origin.add(u.scale(s)).add(v.scale(t)), 1.0);
583    frag(
584        NurbsSurface::new(
585            1,
586            1,
587            vec![lo[0], lo[0], hi[0], hi[0]],
588            vec![lo[1], lo[1], hi[1], hi[1]],
589            vec![vec![at(lo[0], lo[1]), at(lo[0], hi[1])], vec![at(hi[0], lo[1]), at(hi[0], hi[1])]],
590        ),
591        "plane_surface",
592    )
593}
594
595/// A curve lying in the plane (origin, u, v), as a pcurve of that plane:
596/// its control points projected, weights kept (exact, the map is affine).
597fn plane_pcurve(origin: Vec3, u: Vec3, v: Vec3, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
598    let mut points = Vec::with_capacity(curve.control_points.len());
599    for p in &curve.control_points {
600        let x = frag(p.point(), "control_point")?.sub(origin);
601        points.push(Vec4::from_point(Vec3::new(x.dot(u), x.dot(v), 0.0), p.w));
602    }
603    let pcurve = frag(NurbsCurve::new(curve.degree, curve.knots.clone(), points), "plane_pcurve")?;
604    if forward { Ok(pcurve) } else { frag(pcurve.reversed(), "plane_pcurve") }
605}
606
607fn split_polyhedron(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>]) -> Result<Split, KernelRefusal> {
608    if vertices.len() < 4 || faces.len() < 4 {
609        return Err(input("polyhedron", "a polyhedron needs at least four corners and four faces".into()));
610    }
611    let centroid = vertices.iter().fold(Vec3::new(0.0, 0.0, 0.0), |s, &p| s.add(p)).scale(1.0 / vertices.len() as f64);
612    let scale = vertices.iter().map(|p| p.sub(centroid).length()).fold(0.0f64, f64::max).max(1e-300);
613    let tolerance = 1e-12 * scale.max(screw.a.sub(centroid).length());
614    let mut loops: Vec<Vec<usize>> = Vec::with_capacity(faces.len());
615    let mut planes = Vec::with_capacity(faces.len());
616    for face in faces {
617        if face.len() < 3 || face.iter().any(|&i| i >= vertices.len()) {
618            return Err(input("face", "a face needs three or more existing corners".into()));
619        }
620        let mut normal = Vec3::new(0.0, 0.0, 0.0);
621        for k in 0..face.len() {
622            let (p, q) = (vertices[face[k]], vertices[face[(k + 1) % face.len()]]);
623            normal = normal.add(p.sub(centroid).cross(q.sub(centroid)));
624        }
625        let mut normal = normal.normalized().map_err(|_| input("face", "a face has no area".into()))?;
626        let mut face = face.clone();
627        if normal.dot(vertices[face[0]].sub(centroid)) < 0.0 {
628            normal = normal.scale(-1.0);
629            face.reverse();
630        }
631        let offset = normal.dot(vertices[face[0]]);
632        for p in vertices {
633            if normal.dot(*p) - offset > 1e-9 * scale {
634                return Err(refuse("not_convex", "the polyhedron is not convex".into()));
635            }
636        }
637        for &i in &face {
638            if (normal.dot(vertices[i]) - offset).abs() > 1e-9 * scale {
639                return Err(input("face", "a face's corners are not in one plane".into()));
640            }
641        }
642        loops.push(face);
643        planes.push((normal, offset));
644    }
645    // Each face's grazing value (affine along the face).
646    let g = |face: usize, x: Vec3| -> f64 {
647        let value = screw.velocity(x).dot(planes[face].0);
648        if value.abs() <= tolerance * screw.velocity(x).length().max(1.0) { 0.0 } else { value }
649    };
650    let mut owner: HashMap<(usize, usize), usize> = HashMap::new();
651    for (index, face) in loops.iter().enumerate() {
652        for k in 0..face.len() {
653            owner.insert((face[k], face[(k + 1) % face.len()]), index);
654        }
655    }
656    for &(i, j) in owner.keys() {
657        if !owner.contains_key(&(j, i)) {
658            return Err(input("closed", "the polyhedron's faces do not close".into()));
659        }
660    }
661    // Split points on edges: where either neighbour's grazing value crosses zero.
662    let mut points: Vec<Vec3> = vertices.to_vec();
663    let mut edge_splits: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
664    let mut keys: Vec<(usize, usize)> = owner.keys().copied().filter(|&(i, j)| i < j).collect();
665    keys.sort_unstable();
666    for (i, j) in keys {
667        let mut params: Vec<f64> = Vec::new();
668        for f in [owner[&(i, j)], owner[&(j, i)]] {
669            let (gi, gj) = (g(f, vertices[i]), g(f, vertices[j]));
670            if gi * gj < 0.0 {
671                params.push(gi / (gi - gj));
672            }
673        }
674        params.sort_by(|a, b| a.total_cmp(b));
675        params.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
676        let mut list = Vec::new();
677        for s in params {
678            points.push(vertices[i].add(vertices[j].sub(vertices[i]).scale(s)));
679            list.push(points.len() - 1);
680        }
681        edge_splits.insert((i, j), list);
682    }
683    let splits_along = |i: usize, j: usize| -> Vec<usize> {
684        if i < j {
685            edge_splits[&(i, j)].clone()
686        } else {
687            edge_splits[&(j, i)].iter().rev().copied().collect()
688        }
689    };
690    // Sub-faces as point rings, counter-clockwise from outside.
691    let mut rings: Vec<(Vec<usize>, usize, bool)> = Vec::new();
692    for (index, face) in loops.iter().enumerate() {
693        let mut ring: Vec<usize> = Vec::new();
694        for k in 0..face.len() {
695            ring.push(face[k]);
696            ring.extend(splits_along(face[k], face[(k + 1) % face.len()]));
697        }
698        let values: Vec<f64> = ring.iter().map(|&p| g(index, points[p])).collect();
699        let positive = values.iter().any(|&v| v > 0.0);
700        let negative = values.iter().any(|&v| v < 0.0);
701        if !positive && !negative {
702            return Err(refuse("face_in_own_plane", format!("face {index} moves in its own plane (its sweep is flat)")));
703        }
704        if !(positive && negative) {
705            rings.push((ring, index, positive));
706            continue;
707        }
708        let zeros: Vec<usize> = (0..ring.len()).filter(|&k| values[k] == 0.0).collect();
709        if zeros.len() != 2 {
710            return Err(refuse("grazing_split", format!("face {index} meets its grazing line {} times", zeros.len())));
711        }
712        let (za, zb) = (zeros[0], zeros[1]);
713        if za + 1 == zb || (zb + 1) % ring.len() == za {
714            return Err(refuse("grazing_split", format!("face {index} is split along one of its own edges")));
715        }
716        let first: Vec<usize> = (za..=zb).map(|k| ring[k]).collect();
717        let second: Vec<usize> = (zb..ring.len()).chain(0..=za).map(|k| ring[k]).collect();
718        let first_front = values[za + 1] > 0.0;
719        rings.push((first, index, first_front));
720        rings.push((second, index, !first_front));
721    }
722    // A growing rise moves the grazing lines: every sub-face must keep its side
723    // at both ends of the motion.
724    if screw.c2 != 0.0 {
725        for (ring, face, front) in &rings {
726            for &p in ring {
727                for t in [screw.t0, screw.t1] {
728                    let value = screw.velocity_at(t, points[p]).dot(planes[*face].0);
729                    let slack = tolerance * screw.velocity_at(t, points[p]).length().max(1.0);
730                    if (*front && value < -slack) || (!*front && value > slack) {
731                        return Err(refuse("rising_sides", format!("face {face} changes side as the rise grows")));
732                    }
733                }
734            }
735        }
736    }
737    // Curves: one straight segment per point pair.
738    let mut curves: Vec<ToolCurve> = Vec::new();
739    let mut by_pair: HashMap<(usize, usize), usize> = HashMap::new();
740    let mut caps = Vec::with_capacity(rings.len());
741    for (ring, face, front) in rings {
742        let normal = planes[face].0;
743        let origin = points[ring[0]];
744        let u = frag(points[ring[1]].sub(origin).normalized(), "plane_axis")?;
745        let v = normal.cross(u);
746        let corners: Vec<Vec3> = ring.iter().map(|&p| points[p]).collect();
747        let surface = plane_patch(origin, u, v, &corners)?;
748        let mut coedges = Vec::with_capacity(ring.len());
749        for k in 0..ring.len() {
750            let (p, q) = (ring[k], ring[(k + 1) % ring.len()]);
751            let key = (p.min(q), p.max(q));
752            let index = match by_pair.get(&key) {
753                Some(&index) => index,
754                None => {
755                    curves.push(ToolCurve {
756                        curve: frag(make_line(points[key.0], points[key.1]), "line")?,
757                        start: key.0,
758                        end: key.1,
759                        degenerate: false,
760                        samples: None,
761                    });
762                    by_pair.insert(key, curves.len() - 1);
763                    curves.len() - 1
764                }
765            };
766            let forward = p == key.0;
767            coedges.push((index, forward, plane_pcurve(origin, u, v, &curves[index].curve, forward)?));
768        }
769        caps.push(Cap { surface, front, coedges, chart: Some(Chart::Plane { origin, u, v }) });
770    }
771    Ok(Split { points, curves, caps, slides: Vec::new(), slide_sides: Vec::new(), scale, shape: Shape::Planes(planes) })
772}
773
774/// The grazing value `A cos ψ + B sin ψ + C` of a face of revolution at one
775/// meridian point, from three samples.
776fn harmonic(g: impl Fn(f64) -> f64) -> (f64, f64, f64) {
777    let (g0, g90, g180) = (g(0.0), g(0.5 * PI), g(PI));
778    let c = 0.5 * (g0 + g180);
779    (0.5 * (g0 - g180), g90 - c, c)
780}
781
782fn piece_point(piece: &OutlinePiece, s: f64) -> [f64; 2] {
783    match *piece {
784        OutlinePiece::Line { from, to } => [from[0] + (to[0] - from[0]) * s, from[1] + (to[1] - from[1]) * s],
785        OutlinePiece::Arc { centre, radius, start, end } => {
786            let angle = start + (end - start) * s;
787            [centre[0] + radius * angle.cos(), centre[1] + radius * angle.sin()]
788        }
789    }
790}
791
792fn split_revolved(
793    screw: &Screw,
794    origin: Vec3,
795    axis: Vec3,
796    radial: Vec3,
797    meridian: &[OutlinePiece],
798) -> Result<Split, KernelRefusal> {
799    let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
800    let u1 = radial.sub(w.scale(radial.dot(w))).normalized().map_err(|_| input("radial", "the radial direction lies along the axis".into()))?;
801    let u2 = w.cross(u1);
802    if let [OutlinePiece::Arc { centre, radius, start, end }] = meridian {
803        if ((end - start).abs() - TAU).abs() < 1e-12 && centre[0] > *radius {
804            return split_torus(screw, origin, w, u1, u2, *centre, *radius, meridian);
805        }
806    }
807    let at = |rho: f64, z: f64, psi: f64| origin.add(w.scale(z)).add(u1.scale(rho * psi.cos())).add(u2.scale(rho * psi.sin()));
808    let scale = meridian
809        .iter()
810        .flat_map(|p| [piece_point(p, 0.0), piece_point(p, 1.0)])
811        .map(|q| q[0].abs().max(q[1].abs()))
812        .fold(0.0f64, f64::max)
813        .max(1e-300);
814    let on_axis = |q: [f64; 2]| q[0].abs() <= 1e-12 * scale;
815    // Orientation of the meridian outline (+1 counter-clockwise in (ρ, z)).
816    let mut area = 0.0;
817    for piece in meridian {
818        for k in 0..16 {
819            let (p, q) = (piece_point(piece, k as f64 / 16.0), piece_point(piece, (k + 1) as f64 / 16.0));
820            area += p[0] * q[1] - q[0] * p[1];
821        }
822    }
823    // Work counter-clockwise in (ρ, z): a revolved patch's u × v is then outward.
824    let reversed_meridian: Vec<OutlinePiece>;
825    let meridian: &[OutlinePiece] = if area > 0.0 {
826        meridian
827    } else {
828        reversed_meridian = meridian
829            .iter()
830            .rev()
831            .map(|piece| match *piece {
832                OutlinePiece::Line { from, to } => OutlinePiece::Line { from: to, to: from },
833                OutlinePiece::Arc { centre, radius, start, end } => OutlinePiece::Arc { centre, radius, start: end, end: start },
834            })
835            .collect();
836        &reversed_meridian
837    };
838    let ccw = true;
839    // Outward normal in (ρ, z) of a piece at s.
840    let normal = |piece: &OutlinePiece, s: f64| -> [f64; 2] {
841        let (p, q) = (piece_point(piece, (s - 1e-6).max(0.0)), piece_point(piece, (s + 1e-6).min(1.0)));
842        let (dx, dz) = (q[0] - p[0], q[1] - p[1]);
843        let length = dx.hypot(dz);
844        if ccw { [dz / length, -dx / length] } else { [-dz / length, dx / length] }
845    };
846    let g = |piece: &OutlinePiece, s: f64, psi: f64| -> f64 {
847        let m = piece_point(piece, s);
848        let n = normal(piece, s);
849        let x = at(m[0], m[1], psi);
850        let normal = w.scale(n[1]).add(u1.scale(n[0] * psi.cos())).add(u2.scale(n[0] * psi.sin()));
851        screw.velocity(x).dot(normal)
852    };
853    // The faces: pieces off the axis.
854    let faces: Vec<usize> = (0..meridian.len())
855        .filter(|&i| !(matches!(meridian[i], OutlinePiece::Line { .. }) && on_axis(piece_point(&meridian[i], 0.0)) && on_axis(piece_point(&meridian[i], 1.0))))
856        .collect();
857    // Each face's split angles (where K meets it, constant along it), or its
858    // great circle.
859    enum Kind {
860        /// A cylinder or cone (or a flat end) split along generators at these
861        /// angles (empty: one side).
862        Ruled(Vec<f64>),
863        /// A cylinder or cone whose two grazing curves move round it along
864        /// the meridian (`branches`), from rim to rim.
865        Moving,
866        /// A hemisphere from a pole to an equator rim, split by the great
867        /// circle square to its centre's velocity.
868        Sphere { centre: Vec3, pole_at_start: bool },
869        /// A whole ball (a half circle from pole to pole), split by that
870        /// great circle into two hemispheres.
871        Ball { centre: Vec3, radius: f64 },
872        /// A rounded corner (a zone of a torus: an arc whose centre is off the
873        /// axis), crossed by K as a graph v(ψ) of the tube angle for ψ from
874        /// `psi_a` to `psi_b`, entering and leaving at the same rim (the end
875        /// rim when `at_end`).
876        TorusZone { psi_a: f64, psi_b: f64, at_end: bool },
877        /// A flat end that slides within its own plane (`SlideDisc`).
878        Slide,
879        /// A cylinder slid along its own axis (`SlideSide`).
880        SlideSide,
881        /// A rounded corner split along meridian arcs at these angles (the
882        /// grazing curve keeps its angle about the axis: a turn about a
883        /// parallel axis, a slide across the tool's axis).
884        RuledArc(Vec<f64>),
885    }
886    // The two grazing angles of a cylinder or cone at s, the lower and the
887    // upper, unwrapped against s = 0: the front lies between them.
888    let branches = |piece: &OutlinePiece, s: f64| -> Option<(f64, f64)> {
889        let reference = {
890            let (a, b, _) = harmonic(|psi| g(piece, 0.0, psi));
891            b.atan2(a)
892        };
893        let (a, b, c) = harmonic(|psi| g(piece, s, psi));
894        let r = a.hypot(b);
895        if !(c.abs() < r * (1.0 - 1e-9)) {
896            return None;
897        }
898        let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
899        let spread = (-c / r).acos();
900        Some((base - spread, base + spread))
901    };
902    let mut kinds: Vec<Kind> = Vec::with_capacity(faces.len());
903    for &i in &faces {
904        let piece = &meridian[i];
905        match *piece {
906            OutlinePiece::Line { .. } => {
907                let roots = |s: f64| -> Vec<f64> {
908                    let (a, b, c) = harmonic(|psi| g(piece, s, psi));
909                    let r = a.hypot(b);
910                    if c.abs() >= r * (1.0 - 1e-12) {
911                        return Vec::new();
912                    }
913                    let base = b.atan2(a);
914                    let spread = (-c / r).acos();
915                    let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
916                    list.sort_by(|x, y| x.total_cmp(y));
917                    list
918                };
919                // Where the grazing value vanishes all round (a rim through
920                // the motion's axis turning in its own plane) it names no
921                // angle; read the roots where it does not.
922                let alive = |s: f64| {
923                    let (a, b, c) = harmonic(|psi| g(piece, s, psi));
924                    a.hypot(b).max(c.abs()) > 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound())
925                };
926                let readings: Vec<Vec<f64>> = [0.0, 0.5, 1.0].into_iter().filter(|&t| alive(t)).map(roots).collect();
927                if readings.is_empty() {
928                    let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
929                    let flat = (m0[1] - m1[1]).abs() <= 1e-12 * scale;
930                    if flat && (on_axis(m0) || on_axis(m1)) {
931                        kinds.push(Kind::Slide);
932                        continue;
933                    }
934                    let upright = (m0[0] - m1[0]).abs() <= 1e-12 * scale;
935                    if upright && screw.omega == 0.0 && screw.turns.is_none() && screw.frame.is_none() {
936                        kinds.push(Kind::SlideSide);
937                        continue;
938                    }
939                    return Err(refuse("revolved_grazing", format!("meridian piece {i} grazes all over")));
940                }
941                let r0 = readings[0].clone();
942                let still = readings.iter().all(|r| r.len() == r0.len() && r.iter().zip(&r0).all(|(x, y)| (x - y).abs() < 1e-10));
943                if still {
944                    // A face wholly on one side that is not a disc about the
945                    // axis (a cone to its point, an annulus, a whole side):
946                    // two halves of the same side, seamed at 0 and π.
947                    let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
948                    let disc = (m0[1] - m1[1]).abs() <= 1e-12 * scale && (on_axis(m0) || on_axis(m1));
949                    kinds.push(Kind::Ruled(if r0.is_empty() && !disc { vec![0.0, PI] } else { r0 }));
950                    continue;
951                }
952                let mut previous: Option<(f64, f64)> = None;
953                for k in 0..=64 {
954                    let Some(pair) = branches(piece, k as f64 / 64.0) else {
955                        return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} do not run rim to rim")));
956                    };
957                    if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
958                        return Err(refuse("revolved_grazing", format!("the grazing curves on meridian piece {i} turn too fast")));
959                    }
960                    previous = Some(pair);
961                }
962                kinds.push(Kind::Moving);
963            }
964            OutlinePiece::Arc { centre, radius, start, end } => {
965                let (p0, p1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
966                let span = (end - start).abs();
967                let centre_on_axis = on_axis(centre);
968                let pole_at_start = on_axis(p0);
969                let equator = |p: [f64; 2]| (p[1] - centre[1]).abs() <= 1e-12 * scale && (p[0] - radius).abs() <= 1e-12 * scale;
970                if centre_on_axis && (span - PI).abs() < 1e-12 && pole_at_start && on_axis(p1) {
971                    kinds.push(Kind::Ball { centre: origin.add(w.scale(centre[1])), radius });
972                    continue;
973                }
974                if centre[0] > radius * (1.0 + 1e-9) && end > start && span <= PI + 1e-12 {
975                    let at_rim = |v: f64| [centre[0] + radius * v.cos(), centre[1] + radius * v.sin()];
976                    let g_tube = |psi: f64, v: f64| -> f64 {
977                        let m = at_rim(v);
978                        let x = at(m[0], m[1], psi);
979                        let normal = w.scale(v.sin()).add(u1.scale(v.cos() * psi.cos())).add(u2.scale(v.cos() * psi.sin()));
980                        screw.velocity(x).dot(normal)
981                    };
982                    // K at the same angles about the axis all along the arc?
983                    {
984                        let roots_at = |v: f64| -> Option<Vec<f64>> {
985                            let (a, b, c) = harmonic(|psi| g_tube(psi, v));
986                            let r = a.hypot(b);
987                            let floor = 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound());
988                            if r.max(c.abs()) <= floor || c.abs() >= r * (1.0 - 1e-12) {
989                                return None;
990                            }
991                            let (base, spread) = (b.atan2(a), (-c / r).acos());
992                            let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
993                            list.sort_by(|x, y| x.total_cmp(y));
994                            Some(list)
995                        };
996                        let readings: Vec<Option<Vec<f64>>> = [0.25, 0.5, 0.75].into_iter().map(|t| roots_at(start + (end - start) * t)).collect();
997                        if let [Some(r0), Some(r1), Some(r2)] = readings.as_slice() {
998                            if r0.iter().zip(r1).chain(r0.iter().zip(r2)).all(|(x, y)| (x - y).abs() < 1e-10) {
999                                kinds.push(Kind::RuledArc(r0.clone()));
1000                                continue;
1001                            }
1002                        }
1003                    }
1004                    if span >= PI {
1005                        return Err(refuse("revolved_arc", format!("meridian piece {i}: a half-round rim is swept only where its grazing curves keep their angle")));
1006                    }
1007                    let in_zone = |psi: f64| -> Result<Option<f64>, KernelRefusal> {
1008                        let (a, b, c) = harmonic(|v| g_tube(psi, v));
1009                        let r = a.hypot(b);
1010                        if !(c.abs() < r) {
1011                            return Ok(None);
1012                        }
1013                        let base = b.atan2(a);
1014                        let spread = (-c / r).acos();
1015                        let inside: Vec<f64> = [base - spread, base + spread]
1016                            .into_iter()
1017                            .map(|v| start + (v - start).rem_euclid(TAU))
1018                            .filter(|&v| v <= end)
1019                            .collect();
1020                        match inside.as_slice() {
1021                            [] => Ok(None),
1022                            [v] => Ok(Some(*v)),
1023                            _ => Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner twice at one angle"))),
1024                        }
1025                    };
1026                    let count = 720;
1027                    let has: Vec<bool> = (0..count).map(|k| in_zone(TAU * k as f64 / count as f64).map(|r| r.is_some())).collect::<Result<_, _>>()?;
1028                    let rises: Vec<usize> = (0..count).filter(|&k| has[k] && !has[(k + count - 1) % count]).collect();
1029                    let falls: Vec<usize> = (0..count).filter(|&k| !has[k] && has[(k + count - 1) % count]).collect();
1030                    let ([rise], [fall]) = (rises.as_slice(), falls.as_slice()) else {
1031                        return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve does not cross the rounded corner once")));
1032                    };
1033                    // The crossing's ends: roots of the grazing value along the rim it meets.
1034                    let estimate = |k: usize| TAU * (k as f64 - 0.5) / count as f64;
1035                    let (ea, eb) = (estimate(*rise), estimate(*fall));
1036                    let sample = |k: usize| TAU * (k % count) as f64 / count as f64;
1037                    let probe_v = in_zone(sample(*rise))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
1038                    let at_end = (probe_v - end).abs() < (probe_v - start).abs();
1039                    let rim_v = if at_end { end } else { start };
1040                    let (a, b, c) = harmonic(|psi| g_tube(psi, rim_v));
1041                    let r = a.hypot(b);
1042                    let base = b.atan2(a);
1043                    let spread = (-c / r).acos();
1044                    let roots = [base - spread, base + spread];
1045                    let nearest = |e: f64| roots.iter().copied().min_by(|x, y| ((x - e + PI).rem_euclid(TAU) - PI).abs().total_cmp(&((y - e + PI).rem_euclid(TAU) - PI).abs())).expect("two");
1046                    let (psi_a, psi_b) = (nearest(ea).rem_euclid(TAU), nearest(eb).rem_euclid(TAU));
1047                    let psi_b = if psi_b <= psi_a { psi_b + TAU } else { psi_b };
1048                    let leave_v = in_zone(sample(*fall + count - 1))?.ok_or_else(|| refuse("torus_zone", "the grazing curve's crossing is not found".into()))?;
1049                    if ((leave_v - end).abs() < (leave_v - start).abs()) != at_end {
1050                        return Err(refuse("torus_zone", format!("meridian piece {i}: the grazing curve crosses the rounded corner from rim to rim")));
1051                    }
1052                    kinds.push(Kind::TorusZone { psi_a, psi_b, at_end });
1053                    continue;
1054                }
1055                if !(centre_on_axis && (span - 0.5 * PI).abs() < 1e-12 && (pole_at_start && equator(p1) || on_axis(p1) && equator(p0))) {
1056                    // A cap of a sphere about the axis, from its pole to a rim
1057                    // short of (or past) the equator: swept where its grazing
1058                    // curves keep their angle about the axis (meridian arcs
1059                    // through the pole).
1060                    if centre_on_axis && end > start && span < PI && (pole_at_start != on_axis(p1)) {
1061                        let roots_at = |s: f64| -> Option<Vec<f64>> {
1062                            let (a, b, c) = harmonic(|psi| g(piece, s, psi));
1063                            let r = a.hypot(b);
1064                            let floor = 1e-12 * scale * screw.velocity(origin).length().max(screw.omega.abs() * scale).max(screw.rate_bound());
1065                            if r.max(c.abs()) <= floor || c.abs() >= r * (1.0 - 1e-12) {
1066                                return None;
1067                            }
1068                            let (base, spread) = (b.atan2(a), (-c / r).acos());
1069                            let mut list = vec![(base - spread).rem_euclid(TAU), (base + spread).rem_euclid(TAU)];
1070                            list.sort_by(|x, y| x.total_cmp(y));
1071                            Some(list)
1072                        };
1073                        if let [Some(r0), Some(r1), Some(r2)] = [0.25, 0.5, 0.75].map(roots_at) {
1074                            if r0.iter().zip(&r1).chain(r0.iter().zip(&r2)).all(|(x, y)| (x - y).abs() < 1e-10) {
1075                                kinds.push(Kind::RuledArc(r0));
1076                                continue;
1077                            }
1078                        }
1079                    }
1080                    return Err(refuse("revolved_arc", format!("meridian piece {i}: only a quarter circle from a pole to an equator is swept here")));
1081                }
1082                kinds.push(Kind::Sphere { centre: origin.add(w.scale(centre[1])), pole_at_start });
1083            }
1084        }
1085    }
1086    // Rims: junctions off the axis, with the angles they are split at.
1087    let count = meridian.len();
1088    let junction = |k: usize| piece_point(&meridian[k], 1.0); // between piece k and k + 1
1089    let face_of = |piece: usize| faces.iter().position(|&f| f == piece);
1090    let mut rim_angles: HashMap<usize, Vec<f64>> = HashMap::new();
1091    for k in 0..count {
1092        if on_axis(junction(k)) {
1093            continue;
1094        }
1095        let mut angles: Vec<f64> = Vec::new();
1096        for (piece, s_end) in [(k, 1.0), ((k + 1) % count, 0.0)] {
1097            let Some(f) = face_of(piece) else { continue };
1098            match &kinds[f] {
1099                Kind::Ruled(roots) | Kind::RuledArc(roots) => angles.extend(roots.iter().copied()),
1100                Kind::Ball { .. } | Kind::Slide => {}
1101                Kind::SlideSide => angles.extend([0.0, PI]),
1102                Kind::TorusZone { psi_a, psi_b, .. } => angles.extend([psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)]),
1103                Kind::Moving => {
1104                    let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
1105                    angles.extend([lower.rem_euclid(TAU), upper.rem_euclid(TAU)]);
1106                }
1107                Kind::Sphere { centre, .. } => {
1108                    let d = screw.velocity(*centre).cross(w);
1109                    let base = d.dot(u2).atan2(d.dot(u1)).rem_euclid(TAU);
1110                    angles.extend([base, (base + PI).rem_euclid(TAU)]);
1111                }
1112            }
1113        }
1114        angles.sort_by(|a, b| a.total_cmp(b));
1115        let mut merged: Vec<f64> = Vec::new();
1116        for a in angles {
1117            if merged.last().is_some_and(|&b| (a - b).abs() < 1e-9) || merged.first().is_some_and(|&b| (b + TAU - a).abs() < 1e-9) {
1118                continue;
1119            }
1120            merged.push(a);
1121        }
1122        rim_angles.insert(k, merged);
1123    }
1124    // Every face's own split angles must be its rims' (one arc per patch side).
1125    for (f, &piece) in faces.iter().enumerate() {
1126        for (k, s_end) in [((piece + count - 1) % count, 0.0), (piece, 1.0)] {
1127            let own: Vec<f64> = match &kinds[f] {
1128                Kind::Ruled(roots) | Kind::RuledArc(roots) => roots.clone(),
1129                Kind::Moving => {
1130                    let (lower, upper) = branches(&meridian[piece], s_end).expect("checked");
1131                    let mut list = vec![lower.rem_euclid(TAU), upper.rem_euclid(TAU)];
1132                    list.sort_by(|a, b| a.total_cmp(b));
1133                    list
1134                }
1135                Kind::TorusZone { psi_a, psi_b, .. } => {
1136                    let mut list = vec![psi_a.rem_euclid(TAU), psi_b.rem_euclid(TAU)];
1137                    list.sort_by(|a, b| a.total_cmp(b));
1138                    list
1139                }
1140                Kind::Sphere { .. } | Kind::Ball { .. } | Kind::Slide => continue,
1141                Kind::SlideSide => vec![0.0, PI],
1142            };
1143            if let Some(angles) = rim_angles.get(&k) {
1144                if own.is_empty() {
1145                    continue;
1146                }
1147                if angles.len() != own.len() || angles.iter().zip(&own).any(|(a, b)| (a - b).abs() > 1e-9) {
1148                    return Err(refuse("revolved_rims", format!("meridian piece {piece} splits at other angles than its rim")));
1149                }
1150            }
1151        }
1152    }
1153    let mut points: Vec<Vec3> = Vec::new();
1154    let mut curves: Vec<ToolCurve> = Vec::new();
1155    let mut caps: Vec<Cap> = Vec::new();
1156    let mut slides: Vec<SlideDisc> = Vec::new();
1157    let mut slide_sides: Vec<SlideSide> = Vec::new();
1158    // Rim points and arcs: arc j of rim k runs from angle j to angle j + 1.
1159    let mut rim_points: HashMap<(usize, usize), usize> = HashMap::new();
1160    let mut rim_arcs: HashMap<(usize, usize), usize> = HashMap::new();
1161    let mut rim_keys: Vec<usize> = rim_angles.keys().copied().collect();
1162    rim_keys.sort_unstable();
1163    for &k in &rim_keys {
1164        let q = junction(k);
1165        let angles = rim_angles[&k].clone();
1166        let angles = if angles.is_empty() { vec![0.0] } else { angles };
1167        for (j, &a) in angles.iter().enumerate() {
1168            points.push(at(q[0], q[1], a));
1169            rim_points.insert((k, j), points.len() - 1);
1170        }
1171        for j in 0..angles.len() {
1172            let a = angles[j];
1173            let b = if j + 1 < angles.len() { angles[j + 1] } else { angles[0] + TAU };
1174            let base = at(q[0], q[1], a);
1175            let surface = frag(make_revolution(origin, w, &frag(make_line(base, base.add(w)), "line")?, b - a), "rim")?;
1176            curves.push(ToolCurve {
1177                curve: frag(surface.iso_curve_v(0.0), "rim_arc")?,
1178                start: rim_points[&(k, j)],
1179                end: rim_points[&(k, (j + 1) % angles.len())],
1180                degenerate: false,
1181                samples: Some((0..=LOFT_SAMPLES).map(|i| at(q[0], q[1], a + (b - a) * i as f64 / LOFT_SAMPLES as f64)).collect()),
1182            });
1183            rim_arcs.insert((k, j), curves.len() - 1);
1184        }
1185        rim_angles.insert(k, angles);
1186    }
1187    let arc_index = |k: usize, angle: f64| -> Result<usize, KernelRefusal> {
1188        rim_angles[&k]
1189            .iter()
1190            .position(|&a| ((a - angle).rem_euclid(TAU)).min((angle - a).rem_euclid(TAU)) < 1e-9)
1191            .ok_or_else(|| refuse("revolved_rims", "a patch corner is not on its rim's split".into()))
1192    };
1193    let sign_at = |piece: &OutlinePiece, psi: f64| g(piece, 0.5, psi) > 0.0;
1194    for (f, &piece_index) in faces.iter().enumerate() {
1195        let piece = &meridian[piece_index];
1196        let below = (piece_index + count - 1) % count; // rim at the piece's start
1197        let above = piece_index; // rim at its end
1198        let (m0, m1) = (piece_point(piece, 0.0), piece_point(piece, 1.0));
1199        match &kinds[f] {
1200            Kind::SlideSide => {
1201                if on_axis(m0) || on_axis(m1) {
1202                    return Err(refuse("revolved_grazing", format!("meridian piece {piece_index} grazes all over")));
1203                }
1204                let seams = [0.0, PI];
1205                let mut patches = Vec::with_capacity(2);
1206                for j in 0..2 {
1207                    let (alpha, theta) = (seams[j], PI);
1208                    let (ib, ia) = (arc_index(below, alpha)?, arc_index(above, alpha)?);
1209                    patches.push((alpha, theta, rim_arcs[&(below, ib)], rim_arcs[&(above, ia)], [rim_points[&(below, ib)], rim_points[&(above, ia)]]));
1210                }
1211                slide_sides.push(SlideSide { origin, w, patches });
1212            }
1213            Kind::RuledArc(roots) => {
1214                let OutlinePiece::Arc { centre, radius, start, end } = *piece else { unreachable!() };
1215                let tube_centre = |psi: f64| at(centre[0], centre[1], psi);
1216                let tube_axis = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin())).cross(w);
1217                let tube_point = |psi: f64, v: f64| at(centre[0] + radius * v.cos(), centre[1] + radius * v.sin(), psi);
1218                // The tube's arc (start -> end) in the meridian half-plane at ψ.
1219                let generatrix = |psi: f64| -> Result<NurbsCurve, KernelRefusal> {
1220                    let from = tube_point(psi, start);
1221                    let surface = frag(make_revolution(tube_centre(psi), tube_axis(psi), &frag(make_line(from, from.add(tube_axis(psi))), "line")?, end - start), "tube")?;
1222                    frag(surface.iso_curve_v(0.0), "tube")
1223                };
1224                // A cap to its pole: the meridian arcs start (or end) there,
1225                // and each patch has a pole in place of a rim arc.
1226                let apex = if on_axis(m0) || on_axis(m1) {
1227                    let m = if on_axis(m0) { m0 } else { m1 };
1228                    points.push(at(0.0, m[1], 0.0));
1229                    Some(points.len() - 1)
1230                } else {
1231                    None
1232                };
1233                let rim_point = |rim: usize, on: bool, angle: f64| -> Result<usize, KernelRefusal> {
1234                    match (on, apex) {
1235                        (true, Some(p)) => Ok(p),
1236                        _ => Ok(rim_points[&(rim, arc_index(rim, angle)?)]),
1237                    }
1238                };
1239                let mut generators = Vec::with_capacity(roots.len());
1240                for &root in roots {
1241                    let (a, b) = (rim_point(below, on_axis(m0), root)?, rim_point(above, on_axis(m1), root)?);
1242                    let mut exact: Vec<Vec3> = (0..=LOFT_SAMPLES).map(|i| tube_point(root, start + (end - start) * i as f64 / LOFT_SAMPLES as f64)).collect();
1243                    exact[0] = points[a];
1244                    exact[LOFT_SAMPLES] = points[b];
1245                    curves.push(ToolCurve { curve: generatrix(root)?, start: a, end: b, degenerate: false, samples: Some(exact) });
1246                    generators.push(curves.len() - 1);
1247                }
1248                for j in 0..roots.len() {
1249                    let (alpha, beta) = (roots[j], if j + 1 < roots.len() { roots[j + 1] } else { roots[0] + TAU });
1250                    let surface = frag(make_revolution(origin, w, &generatrix(alpha)?, beta - alpha), "patch")?;
1251                    let (mid_psi, mid_v) = (0.5 * (alpha + beta), 0.5 * (start + end));
1252                    let mid_normal = w.scale(mid_v.sin()).add(u1.scale(mid_v.cos() * mid_psi.cos())).add(u2.scale(mid_v.cos() * mid_psi.sin()));
1253                    let front = screw.velocity(tube_point(mid_psi, mid_v)).dot(mid_normal) > 0.0;
1254                    let mut pole = |curves: &mut Vec<ToolCurve>| -> usize {
1255                        let p = apex.expect("apex");
1256                        curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1257                        curves.len() - 1
1258                    };
1259                    let lower = if on_axis(m0) { pole(&mut curves) } else { rim_arcs[&(below, arc_index(below, alpha)?)] };
1260                    let upper = if on_axis(m1) { pole(&mut curves) } else { rim_arcs[&(above, arc_index(above, alpha)?)] };
1261                    let (g_alpha, g_beta) = (generators[j], generators[(j + 1) % roots.len()]);
1262                    let coedges = vec![
1263                        (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1264                        (g_beta, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1265                        (upper, on_axis(m1), uv_line([1.0, 1.0], [0.0, 1.0])?),
1266                        (g_alpha, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1267                    ];
1268                    let chart = Chart::Revolution {
1269                        origin,
1270                        w,
1271                        u1,
1272                        u2,
1273                        alpha,
1274                        theta: beta - alpha,
1275                        spans_u: spans_of(&surface.knots_u),
1276                        meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_of(&surface.knots_v) },
1277                    };
1278                    caps.push(Cap { surface, front, coedges, chart: Some(chart) });
1279                }
1280            }
1281            Kind::Slide => {
1282                let rim = if on_axis(m0) { above } else { below };
1283                let n = normal(piece, 0.5);
1284                let outward = w.scale(n[1].signum());
1285                let arcs = rim_angles[&rim].len();
1286                // Counter-clockwise about the outward normal: increasing ψ about +w.
1287                let order: Vec<usize> = if n[1] > 0.0 { (0..arcs).collect() } else { (0..arcs).rev().collect() };
1288                slides.push(SlideDisc {
1289                    centre: origin.add(w.scale(m0[1])),
1290                    u: u1,
1291                    v: outward.cross(u1),
1292                    rim: order.into_iter().map(|j| (rim_arcs[&(rim, j)], n[1] > 0.0)).collect(),
1293                });
1294            }
1295            Kind::Ruled(roots) if roots.is_empty() => {
1296                // A flat end wholly on one side: a disc about the axis.
1297                let flat = (m0[1] - m1[1]).abs() <= 1e-12 * scale;
1298                if !flat || !(on_axis(m0) || on_axis(m1)) {
1299                    return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a whole side or ring is not swept here")));
1300                }
1301                let rim = if on_axis(m0) { above } else { below };
1302                let front = sign_at(piece, 0.0);
1303                if (0..64).any(|k| sign_at(piece, TAU * k as f64 / 64.0) != front) {
1304                    return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a flat end meets its grazing line")));
1305                }
1306                let n = normal(piece, 0.5);
1307                let outward = w.scale(n[1].signum());
1308                let centre = origin.add(w.scale(m0[1]));
1309                let (u, v) = (u1, outward.cross(u1));
1310                let arcs = rim_angles[&rim].len();
1311                let mut coedges = Vec::with_capacity(arcs);
1312                let mut corners = Vec::new();
1313                // Counter-clockwise about the outward normal: increasing ψ about +w.
1314                let order: Vec<usize> = if n[1] > 0.0 { (0..arcs).collect() } else { (0..arcs).rev().collect() };
1315                for j in order {
1316                    let c = rim_arcs[&(rim, j)];
1317                    let forward = n[1] > 0.0;
1318                    coedges.push((c, forward, plane_pcurve(centre, u, v, &curves[c].curve, forward)?));
1319                    corners.push(points[curves[c].start]);
1320                }
1321                let r = junction(rim)[0];
1322                corners.extend([centre.add(u.scale(r)).add(v.scale(r)), centre.sub(u.scale(r)).sub(v.scale(r))]);
1323                caps.push(Cap { surface: plane_patch(centre, u, v, &corners)?, front, coedges, chart: Some(Chart::Plane { origin: centre, u, v }) });
1324            }
1325            Kind::Ruled(roots) => {
1326                // A cone to a point: its generators start (or end) at the
1327                // apex, and each patch has a pole there in place of a rim arc.
1328                let apex = if on_axis(m0) || on_axis(m1) {
1329                    let m = if on_axis(m0) { m0 } else { m1 };
1330                    points.push(at(0.0, m[1], 0.0));
1331                    Some(points.len() - 1)
1332                } else {
1333                    None
1334                };
1335                let rim_point = |rim: usize, on: bool, angle: f64| -> Result<usize, KernelRefusal> {
1336                    match (on, apex) {
1337                        (true, Some(p)) => Ok(p),
1338                        _ => Ok(rim_points[&(rim, arc_index(rim, angle)?)]),
1339                    }
1340                };
1341                // Generators at each root, from the start rim to the end rim.
1342                let mut generators = Vec::with_capacity(roots.len());
1343                for &root in roots {
1344                    let (a, b) = (rim_point(below, on_axis(m0), root)?, rim_point(above, on_axis(m1), root)?);
1345                    let line_samples = (0..=LOFT_SAMPLES).map(|i| points[a].add(points[b].sub(points[a]).scale(i as f64 / LOFT_SAMPLES as f64))).collect();
1346                    curves.push(ToolCurve { curve: frag(make_line(points[a], points[b]), "generator")?, start: a, end: b, degenerate: false, samples: Some(line_samples) });
1347                    generators.push(curves.len() - 1);
1348                }
1349                for j in 0..roots.len() {
1350                    let (alpha, beta) = (roots[j], if j + 1 < roots.len() { roots[j + 1] } else { roots[0] + TAU });
1351                    let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1352                    let surface = frag(make_revolution(origin, w, &generatrix, beta - alpha), "patch")?;
1353                    let front = sign_at(piece, 0.5 * (alpha + beta));
1354                    let mut pole = |curves: &mut Vec<ToolCurve>| -> usize {
1355                        let p = apex.expect("apex");
1356                        curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1357                        curves.len() - 1
1358                    };
1359                    let lower = if on_axis(m0) { pole(&mut curves) } else { rim_arcs[&(below, arc_index(below, alpha)?)] };
1360                    let upper = if on_axis(m1) { pole(&mut curves) } else { rim_arcs[&(above, arc_index(above, alpha)?)] };
1361                    let (g_alpha, g_beta) = (generators[j], generators[(j + 1) % roots.len()]);
1362                    let coedges = vec![
1363                        (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1364                        (g_beta, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1365                        (upper, on_axis(m1), uv_line([1.0, 1.0], [0.0, 1.0])?),
1366                        (g_alpha, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1367                    ];
1368                    let chart = Chart::Revolution { origin, w, u1, u2, alpha, theta: beta - alpha, spans_u: spans_of(&surface.knots_u), meridian: MeridianChart::Line { m0, m1 } };
1369                    caps.push(Cap { surface, front, coedges, chart: Some(chart) });
1370                }
1371            }
1372            Kind::Moving => {
1373                if on_axis(m0) || on_axis(m1) {
1374                    return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a split cone to a point is not swept here")));
1375                }
1376                let n = 128;
1377                let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1378                let pairs: Vec<(f64, f64)> = params.iter().map(|&t| branches(piece, t).expect("checked")).collect();
1379                let pick = |pair: (f64, f64), which: usize| if which == 0 { pair.0 } else { pair.1 };
1380                // K: the lower and upper branch, each from the start rim to the end rim.
1381                let mut branch_curves = [0usize; 2];
1382                for which in 0..2 {
1383                    let (psi0, psi1) = (pick(pairs[0], which), pick(pairs[n], which));
1384                    let start = rim_points[&(below, arc_index(below, psi0)?)];
1385                    let end = rim_points[&(above, arc_index(above, psi1)?)];
1386                    let mut samples: Vec<Vec3> = params
1387                        .iter()
1388                        .zip(&pairs)
1389                        .map(|(&t, &pair)| {
1390                            let m = piece_point(piece, t);
1391                            at(m[0], m[1], pick(pair, which))
1392                        })
1393                        .collect();
1394                    samples[0] = points[start];
1395                    samples[n] = points[end];
1396                    let mut exact: Vec<Vec3> = (0..=LOFT_SAMPLES)
1397                        .map(|i| {
1398                            let t = i as f64 / LOFT_SAMPLES as f64;
1399                            let m = piece_point(piece, t);
1400                            at(m[0], m[1], pick(branches(piece, t).expect("checked"), which))
1401                        })
1402                        .collect();
1403                    exact[0] = points[start];
1404                    exact[LOFT_SAMPLES] = points[end];
1405                    curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&samples, 5, &params), "branch")?, start, end, degenerate: false, samples: Some(exact) });
1406                    branch_curves[which] = curves.len() - 1;
1407                }
1408                // Front between lower and upper; back from upper to lower + 2π.
1409                for (lo_which, hi_which, hi_turn) in [(0usize, 1usize, 0.0), (1, 0, TAU)] {
1410                    let lo = |i: usize| pick(pairs[i], lo_which);
1411                    let hi = |i: usize| pick(pairs[i], hi_which) + hi_turn;
1412                    let alpha = (0..=n).map(lo).fold(f64::INFINITY, f64::min) - 1e-3;
1413                    let theta = (0..=n).map(hi).fold(f64::NEG_INFINITY, f64::max) + 1e-3 - alpha;
1414                    if theta >= TAU {
1415                        return Err(refuse("revolved_side", format!("meridian piece {piece_index}: a side patch wraps all the way round")));
1416                    }
1417                    let generatrix = frag(make_line(at(m0[0], m0[1], alpha), at(m1[0], m1[1], alpha)), "generatrix")?;
1418                    let surface = frag(make_revolution(origin, w, &generatrix, theta), "patch")?;
1419                    let spans = (surface.knots_u.len() - 6) / 2 + 1;
1420                    let u_of = |psi: f64| crate::circle_angle_to_parameter(spans, theta, psi - alpha);
1421                    let angle_near = |x: Vec3, guess: f64| -> f64 {
1422                        let r = x.sub(origin);
1423                        let a = r.dot(u2).atan2(r.dot(u1));
1424                        guess + (a - guess + PI).rem_euclid(TAU) - PI
1425                    };
1426                    let front = sign_at(piece, 0.5 * (lo(n / 2) + hi(n / 2)));
1427                    // A rim arc's pcurve on this patch, through its own parameter.
1428                    let rim_pcurve = |arc: usize, v: f64| -> Result<NurbsCurve, KernelRefusal> {
1429                        let curve = &curves[arc].curve;
1430                        let [d0, d1] = frag(curve.domain(), "domain")?;
1431                        let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1432                        let mut uv = Vec::with_capacity(ts.len());
1433                        for &t in &ts {
1434                            let x = frag(curve.evaluate(t), "rim")?;
1435                            uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), v, 0.0));
1436                        }
1437                        frag(crate::interpolate_curve(&uv, 5, &ts), "rim_pcurve")
1438                    };
1439                    // A grazing branch's pcurve: the image of the built edge,
1440                    // through its own parameter (angle about the axis; place
1441                    // along the meridian line).
1442                    let branch_pcurve = |which: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> {
1443                        let curve = &curves[branch_curves[which]].curve;
1444                        let [d0, d1] = frag(curve.domain(), "domain")?;
1445                        let (span_rho, span_z) = (m1[0] - m0[0], m1[1] - m0[1]);
1446                        let length2 = span_rho * span_rho + span_z * span_z;
1447                        let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1448                        let mut uv = Vec::with_capacity(ts.len());
1449                        for &t in &ts {
1450                            let x = frag(curve.evaluate(t), "branch")?;
1451                            let r = x.sub(origin);
1452                            let z = r.dot(w);
1453                            let rho = r.sub(w.scale(z)).length();
1454                            let along = ((rho - m0[0]) * span_rho + (z - m0[1]) * span_z) / length2;
1455                            uv.push(Vec3::new(u_of(angle_near(x, alpha + 0.5 * theta)), along, 0.0));
1456                        }
1457                        frag(crate::interpolate_curve(&uv, 5, &ts), "branch_pcurve")
1458                    };
1459                    let bottom = rim_arcs[&(below, arc_index(below, lo(0))?)];
1460                    let top = rim_arcs[&(above, arc_index(above, lo(n))?)];
1461                    let coedges = vec![
1462                        (bottom, true, rim_pcurve(bottom, 0.0)?),
1463                        (branch_curves[hi_which], true, branch_pcurve(hi_which, hi_turn)?),
1464                        (top, false, frag(rim_pcurve(top, 1.0)?.reversed(), "reversed")?),
1465                        (branch_curves[lo_which], false, frag(branch_pcurve(lo_which, 0.0)?.reversed(), "reversed")?),
1466                    ];
1467                    let chart_spans = spans_of(&surface.knots_u);
1468                    caps.push(Cap { surface, front, coedges, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha, theta, spans_u: chart_spans, meridian: MeridianChart::Line { m0, m1 } }) });
1469                }
1470            }
1471            Kind::TorusZone { psi_a, psi_b, at_end } => {
1472                let OutlinePiece::Arc { centre, radius, start, end } = *piece else { unreachable!() };
1473                let (psi_a, psi_b, at_end) = (*psi_a, *psi_b, *at_end);
1474                let tube_centre = |psi: f64| at(centre[0], centre[1], psi);
1475                let tube_axis = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin())).cross(w);
1476                let tube_point = |psi: f64, v: f64| at(centre[0] + radius * v.cos(), centre[1] + radius * v.sin(), psi);
1477                let normal_at = |psi: f64, v: f64| w.scale(v.sin()).add(u1.scale(v.cos() * psi.cos())).add(u2.scale(v.cos() * psi.sin()));
1478                let root = |psi: f64| -> Option<f64> {
1479                    let (a, b, c) = harmonic(|v| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)));
1480                    let r = a.hypot(b);
1481                    if !(c.abs() < r) {
1482                        return None;
1483                    }
1484                    let base = b.atan2(a);
1485                    let spread = (-c / r).acos();
1486                    [base - spread, base + spread].into_iter().map(|v| start + (v - start).rem_euclid(TAU)).find(|&v| v <= end + 1e-12)
1487                };
1488                // The tube's arc (start -> end) in the meridian half-plane at ψ.
1489                let generatrix = |psi: f64| -> Result<NurbsCurve, KernelRefusal> {
1490                    let from = tube_point(psi, start);
1491                    let surface = frag(make_revolution(tube_centre(psi), tube_axis(psi), &frag(make_line(from, from.add(tube_axis(psi))), "line")?, end - start), "tube")?;
1492                    frag(surface.iso_curve_v(0.0), "tube")
1493                };
1494                let (rim_k, other_k) = if at_end { (above, below) } else { (below, above) };
1495                let ka = rim_points[&(rim_k, arc_index(rim_k, psi_a)?)];
1496                let kb = rim_points[&(rim_k, arc_index(rim_k, psi_b)?)];
1497                // K: the graph over ψ ∈ [ψa, ψb].
1498                let n = 256;
1499                let params: Vec<f64> = (0..=n).map(|j| j as f64 / n as f64).collect();
1500                // Spaced as 1 − cos: where K meets its rim it runs along the
1501                // meridian, so its tube angle goes as √(ψ − ψa); in this
1502                // parameter it is regular.
1503                let psi_of = |t: f64| psi_a + (psi_b - psi_a) * 0.5 * (1.0 - (PI * t).cos());
1504                let mut xyz = Vec::with_capacity(n + 1);
1505                for j in 0..=n {
1506                    let psi = psi_of(params[j]);
1507                    let v = if j == 0 || j == n { if at_end { end } else { start } } else {
1508                        root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1509                    };
1510                    xyz.push(tube_point(psi, v));
1511                }
1512                xyz[0] = points[ka];
1513                xyz[n] = points[kb];
1514                let mut exact = Vec::with_capacity(LOFT_SAMPLES + 1);
1515                for i in 0..=LOFT_SAMPLES {
1516                    let psi = psi_of(i as f64 / LOFT_SAMPLES as f64);
1517                    let v = if i == 0 || i == LOFT_SAMPLES { if at_end { end } else { start } } else {
1518                        root(psi).ok_or_else(|| refuse("torus_zone", "the grazing curve leaves the rounded corner".into()))?
1519                    };
1520                    exact.push(tube_point(psi, v));
1521                }
1522                exact[0] = points[ka];
1523                exact[LOFT_SAMPLES] = points[kb];
1524                curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, &params), "torus_k")?, start: ka, end: kb, degenerate: false, samples: Some(exact) });
1525                let k_curve = curves.len() - 1;
1526                // Cut meridians at ψa and ψb, from the start rim to the end rim.
1527                let mut cut = [0usize; 2];
1528                for (slot, psi) in [(0, psi_a), (1, psi_b)] {
1529                    let (lo, hi) = (rim_points[&(below, arc_index(below, psi)?)], rim_points[&(above, arc_index(above, psi)?)]);
1530                    curves.push(ToolCurve { curve: generatrix(psi)?, start: lo, end: hi, degenerate: false, samples: None });
1531                    cut[slot] = curves.len() - 1;
1532                }
1533                let label = |psi: f64, v: f64| screw.velocity(tube_point(psi, v)).dot(normal_at(psi, v)) > 0.0;
1534                // The patch over [α, β] and its exact inverse.
1535                let patch = |alpha: f64, beta: f64| -> Result<(NurbsSurface, usize, usize), KernelRefusal> {
1536                    let g = generatrix(alpha)?;
1537                    let surface = frag(make_revolution(origin, w, &g, beta - alpha), "torus_patch")?;
1538                    let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1539                    let spans_v = (g.knots.len() - 6) / 2 + 1;
1540                    Ok((surface, spans_u, spans_v))
1541                };
1542                let inverse_pcurve = |curve: &NurbsCurve, alpha: f64, beta: f64, spans_u: usize, spans_v: usize, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
1543                    let [d0, d1] = frag(curve.domain(), "domain")?;
1544                    let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1545                    let mut uv = Vec::with_capacity(ts.len());
1546                    for &t in &ts {
1547                        let x = frag(curve.evaluate(t), "point")?;
1548                        let r = x.sub(origin);
1549                        let mid = 0.5 * (alpha + beta);
1550                        let psi = mid + (r.dot(u2).atan2(r.dot(u1)) - mid + PI).rem_euclid(TAU) - PI;
1551                        let q = x.sub(tube_centre(psi));
1552                        let radial = u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1553                        let vmid = 0.5 * (start + end);
1554                        let v = vmid + (q.dot(w).atan2(q.dot(radial)) - vmid + PI).rem_euclid(TAU) - PI;
1555                        uv.push(Vec3::new(
1556                            crate::circle_angle_to_parameter(spans_u, beta - alpha, psi - alpha),
1557                            crate::circle_angle_to_parameter(spans_v, end - start, v - start),
1558                            0.0,
1559                        ));
1560                    }
1561                    let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "torus_pcurve")?;
1562                    if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
1563                };
1564                // The band away from K: a natural patch over [ψb, ψa + 2π].
1565                {
1566                    let (surface, _, band_spans_v) = patch(psi_b, psi_a + TAU)?;
1567                    let (bottom, top) = (rim_arcs[&(below, arc_index(below, psi_b)?)], rim_arcs[&(above, arc_index(above, psi_b)?)]);
1568                    let coedges = vec![
1569                        (bottom, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1570                        (cut[0], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1571                        (top, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1572                        (cut[1], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1573                    ];
1574                    let mid = psi_b + 0.5 * (psi_a + TAU - psi_b);
1575                    let chart_spans = spans_of(&surface.knots_u);
1576                    caps.push(Cap { surface, front: label(mid, 0.5 * (start + end)), coedges, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_b, theta: psi_a + TAU - psi_b, spans_u: chart_spans, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: band_spans_v } }) });
1577                }
1578                // Over [ψa, ψb]: the lens between K and its rim, and the rest.
1579                let (surface, spans_u, spans_v) = patch(psi_a, psi_b)?;
1580                let rim_arc = rim_arcs[&(rim_k, arc_index(rim_k, psi_a)?)];
1581                let other_arc = rim_arcs[&(other_k, arc_index(other_k, psi_a)?)];
1582                let mid = 0.5 * (psi_a + psi_b);
1583                let v_k = root(mid).ok_or_else(|| refuse("torus_zone", "no grazing point mid-way".into()))?;
1584                let k_pc = |forward: bool| inverse_pcurve(&curves[k_curve].curve, psi_a, psi_b, spans_u, spans_v, forward);
1585                let lens_v = if at_end { 0.5 * (v_k + end) } else { 0.5 * (v_k + start) };
1586                let rest_v = if at_end { 0.5 * (v_k + start) } else { 0.5 * (v_k + end) };
1587                if at_end {
1588                    // Lens: K (ψa -> ψb) under the end rim, then the end rim back.
1589                    let lens = vec![(k_curve, true, k_pc(true)?), (rim_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?)];
1590                    caps.push(Cap { surface: surface.clone(), front: label(mid, lens_v), coedges: lens, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1591                    let rest = vec![
1592                        (other_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
1593                        (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1594                        (k_curve, false, k_pc(false)?),
1595                        (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1596                    ];
1597                    caps.push(Cap { surface, front: label(mid, rest_v), coedges: rest, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1598                } else {
1599                    let lens = vec![(rim_arc, true, uv_line([0.0, 0.0], [1.0, 0.0])?), (k_curve, false, k_pc(false)?)];
1600                    caps.push(Cap { surface: surface.clone(), front: label(mid, lens_v), coedges: lens, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1601                    let rest = vec![
1602                        (k_curve, true, k_pc(true)?),
1603                        (cut[1], true, uv_line([1.0, 0.0], [1.0, 1.0])?),
1604                        (other_arc, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
1605                        (cut[0], false, uv_line([0.0, 1.0], [0.0, 0.0])?),
1606                    ];
1607                    caps.push(Cap { surface, front: label(mid, rest_v), coedges: rest, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre, start, sweep: end - start, spans: spans_v } }) });
1608                }
1609            }
1610            Kind::Ball { centre, radius } => {
1611                let m = frag(screw.velocity(*centre).normalized(), "ball_still")?;
1612                let d = frag(w.cross(m).normalized().or_else(|_| u1.cross(m).normalized()), "ball_axis")?;
1613                let (e1, e2) = (centre.add(d.scale(*radius)), centre.sub(d.scale(*radius)));
1614                points.push(e1);
1615                points.push(e2);
1616                let (i1, i2) = (points.len() - 2, points.len() - 1);
1617                let half = frag(make_revolution(*centre, m, &frag(make_line(e1, e1.add(m)), "line")?, PI), "great")?;
1618                let k_a = frag(half.iso_curve_v(0.0), "great")?;
1619                // Exact samples at even angles from e1 round to e2 (for a
1620                // lofted wall), turned the way the curve runs.
1621                let k_a_samples: Vec<Vec3> = {
1622                    let [a0, a1] = frag(k_a.domain(), "domain")?;
1623                    let middle = frag(k_a.evaluate(0.5 * (a0 + a1)), "middle")?;
1624                    let arm = e1.sub(*centre);
1625                    let sense = if arm.rotate_about(m, 0.5 * PI).add(*centre).sub(middle).length() < arm.rotate_about(m, -0.5 * PI).add(*centre).sub(middle).length() { 1.0 } else { -1.0 };
1626                    let mut list: Vec<Vec3> = (0..=LOFT_SAMPLES).map(|i| centre.add(arm.rotate_about(m, sense * PI * i as f64 / LOFT_SAMPLES as f64))).collect();
1627                    list[0] = e1;
1628                    list[LOFT_SAMPLES] = e2;
1629                    list
1630                };
1631                curves.push(ToolCurve { curve: k_a.clone(), start: i1, end: i2, degenerate: false, samples: Some(k_a_samples.clone()) });
1632                let k_a_index = curves.len() - 1;
1633                let [a0, a1] = frag(k_a.domain(), "domain")?;
1634                let h = frag(k_a.evaluate(0.5 * (a0 + a1)), "middle")?.sub(*centre);
1635                let mut k_b_index: Option<usize> = None;
1636                for front in [true, false] {
1637                    let toward = if front { m } else { m.scale(-1.0) };
1638                    let da = if d.cross(h).dot(toward) > 0.0 { d } else { d.scale(-1.0) };
1639                    let mut generatrix = k_a.clone();
1640                    let mut surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1641                    let outward = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1642                        let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1643                        let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1644                        let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1645                        Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1646                    };
1647                    let mut forward = true;
1648                    if !outward(&surface)? {
1649                        generatrix = frag(generatrix.reversed(), "reversed")?;
1650                        surface = frag(make_revolution(*centre, da, &generatrix, PI), "hemisphere")?;
1651                        forward = false;
1652                    }
1653                    let (start, end) = if forward { (i1, i2) } else { (i2, i1) };
1654                    let [g0, g1] = frag(generatrix.domain(), "domain")?;
1655                    let k_b = match k_b_index {
1656                        Some(index) => index,
1657                        None => {
1658                            // The other half of the great circle: the first half turned half way about `da`.
1659                            let mut samples: Vec<Vec3> = k_a_samples.iter().map(|&x| centre.add(x.sub(*centre).rotate_about(da, PI))).collect();
1660                            if !forward {
1661                                samples.reverse();
1662                            }
1663                            samples[0] = points[start];
1664                            samples[LOFT_SAMPLES] = points[end];
1665                            curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: Some(samples) });
1666                            k_b_index = Some(curves.len() - 1);
1667                            curves.len() - 1
1668                        }
1669                    };
1670                    let k_b_forward = curves[k_b].start == start;
1671                    let k_b_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1672                    let poles = [start, end].map(|p| {
1673                        curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1674                        curves.len() - 1
1675                    });
1676                    let coedges = vec![
1677                        (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1678                        (k_b, k_b_forward, k_b_pcurve),
1679                        (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1680                        (k_a_index, !forward, uv_line([0.0, g1], [0.0, g0])?),
1681                    ];
1682                    caps.push(Cap { surface, front, coedges, chart: None });
1683                }
1684            }
1685            Kind::Sphere { centre, pole_at_start } => {
1686                let rim = if *pole_at_start { above } else { below };
1687                let angles = rim_angles[&rim].clone();
1688                if angles.len() != 2 {
1689                    return Err(refuse("revolved_rims", "a hemisphere's rim is not split in two".into()));
1690                }
1691                let pole_side = w.scale(if *pole_at_start { m0[1] - m1[1] } else { m1[1] - m0[1] }.signum());
1692                let m = screw.velocity(*centre);
1693                let e1 = points[rim_points[&(rim, 0)]];
1694                let d = frag(e1.sub(*centre).normalized(), "lune_axis")?;
1695                let mut great: Option<usize> = None;
1696                for j in 0..2 {
1697                    let arc = rim_arcs[&(rim, j)];
1698                    let [a0, a1] = frag(curves[arc].curve.domain(), "domain")?;
1699                    let middle = frag(curves[arc].curve.evaluate(0.5 * (a0 + a1)), "middle")?;
1700                    let hv = middle.sub(*centre);
1701                    let da = if d.cross(hv).dot(pole_side) > 0.0 { d } else { d.scale(-1.0) };
1702                    // Turn the rim arc about da until its middle reaches K's plane.
1703                    let theta = (-m.dot(hv)).atan2(m.dot(da.cross(hv))).rem_euclid(TAU);
1704                    let theta = if theta > PI { theta - PI } else { theta };
1705                    if !(theta > 1e-9 && theta < PI - 1e-9) {
1706                        return Err(refuse("revolved_lune", "the great circle meets the rim's plane edge-on".into()));
1707                    }
1708                    let mut generatrix = curves[arc].curve.clone();
1709                    let mut surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1710                    // Outward: (x − centre) at the lune's middle.
1711                    let probe = |s: &NurbsSurface| -> Result<bool, KernelRefusal> {
1712                        let p = frag(s.evaluate(0.5, 0.5), "probe")?;
1713                        let su = frag(s.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(s.evaluate(0.5 - 1e-6, 0.5), "probe")?);
1714                        let sv = frag(s.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(s.evaluate(0.5, 0.5 - 1e-6), "probe")?);
1715                        Ok(su.cross(sv).dot(p.sub(*centre)) > 0.0)
1716                    };
1717                    let mut forward = true;
1718                    if !probe(&surface)? {
1719                        generatrix = frag(generatrix.reversed(), "reversed")?;
1720                        surface = frag(make_revolution(*centre, da, &generatrix, theta), "lune")?;
1721                        forward = false;
1722                    }
1723                    let (start, end) = if forward { (curves[arc].start, curves[arc].end) } else { (curves[arc].end, curves[arc].start) };
1724                    let [g0, g1] = frag(generatrix.domain(), "domain")?;
1725                    // The great-circle half: the turned generatrix (u = 1), from
1726                    // `start` to `end`; made once, by the first lune.
1727                    let k_curve = match great {
1728                        Some(index) => index,
1729                        None => {
1730                            curves.push(ToolCurve { curve: frag(surface.iso_curve_u(1.0), "great")?, start, end, degenerate: false, samples: None });
1731                            great = Some(curves.len() - 1);
1732                            curves.len() - 1
1733                        }
1734                    };
1735                    let k_forward = curves[k_curve].start == start;
1736                    let poles = [start, end].map(|p| {
1737                        curves.push(ToolCurve { curve: make_line(points[p], points[p]).expect("point"), start: p, end: p, degenerate: true, samples: None });
1738                        curves.len() - 1
1739                    });
1740                    let k_pcurve = uv_line([1.0, g0], [1.0, g1])?;
1741                    let coedges = vec![
1742                        (poles[0], true, uv_line([0.0, g0], [1.0, g0])?),
1743                        (k_curve, k_forward, k_pcurve),
1744                        (poles[1], true, uv_line([1.0, g1], [0.0, g1])?),
1745                        (arc, !forward, uv_line([0.0, g1], [0.0, g0])?),
1746                    ];
1747                    let mid_psi = if j == 0 { 0.5 * (angles[0] + angles[1]) } else { 0.5 * (angles[1] + angles[0] + TAU) };
1748                    let front = m.dot(at(1.0, 0.0, mid_psi).sub(origin)) > 0.0;
1749                    caps.push(Cap { surface, front, coedges, chart: None });
1750                }
1751            }
1752        }
1753    }
1754    Ok(Split {
1755        points,
1756        curves,
1757        caps,
1758        slides,
1759        slide_sides,
1760        scale,
1761        shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1762    })
1763}
1764
1765/// A ring (torus: the circle of `radius` about `centre` in (ρ, z), off the
1766/// axis), split by its two grazing curves. For each angle ψ about the ring's
1767/// axis the grazing value along the tube's circle is A cos v + B sin v + C, so
1768/// each curve is a graph v(ψ) round the ring; the front band lies between
1769/// them. Each band is built in two halves (ψ in [0, π] and [π, 2π]) so no face
1770/// closes on itself.
1771#[allow(clippy::too_many_arguments)]
1772fn split_torus(
1773    screw: &Screw,
1774    origin: Vec3,
1775    w: Vec3,
1776    u1: Vec3,
1777    u2: Vec3,
1778    centre: [f64; 2],
1779    radius: f64,
1780    meridian: &[OutlinePiece],
1781) -> Result<Split, KernelRefusal> {
1782    let (rho_c, z_c) = (centre[0], centre[1]);
1783    let radial = |psi: f64| u1.scale(psi.cos()).add(u2.scale(psi.sin()));
1784    let tube_centre = |psi: f64| origin.add(w.scale(z_c)).add(radial(psi).scale(rho_c));
1785    let point = |psi: f64, v: f64| tube_centre(psi).add(radial(psi).scale(radius * v.cos())).add(w.scale(radius * v.sin()));
1786    let g = |psi: f64, v: f64| screw.velocity(point(psi, v)).dot(radial(psi).scale(v.cos()).add(w.scale(v.sin())));
1787    let reference = {
1788        let (a, b, _) = harmonic(|v| g(0.0, v));
1789        b.atan2(a)
1790    };
1791    // The lower and upper grazing angles v at ψ, unwrapped; the front between.
1792    let branches = |psi: f64| -> Option<(f64, f64)> {
1793        let (a, b, c) = harmonic(|v| g(psi, v));
1794        let r = a.hypot(b);
1795        if !(c.abs() < r * (1.0 - 1e-9)) {
1796            return None;
1797        }
1798        let base = reference + (b.atan2(a) - reference + PI).rem_euclid(TAU) - PI;
1799        let spread = (-c / r).acos();
1800        Some((base - spread, base + spread))
1801    };
1802    let n = screw.loop_samples;
1803    let halves = [(0.0, PI), (PI, TAU)];
1804    let mut samples: Vec<Vec<(f64, f64, f64)>> = Vec::new(); // per half: (ψ, lower, upper)
1805    for &(a, b) in &halves {
1806        let mut list = Vec::with_capacity(n + 1);
1807        let mut previous: Option<(f64, f64)> = None;
1808        for i in 0..=n {
1809            let psi = a + (b - a) * i as f64 / n as f64;
1810            let Some(pair) = branches(psi) else {
1811                return Err(refuse("ring_grazing", "the ring's grazing curves do not run all the way round it".into()));
1812            };
1813            if previous.is_some_and(|p| (p.0 - pair.0).abs() > 0.5 || (p.1 - pair.1).abs() > 0.5) {
1814                return Err(refuse("ring_grazing", "the ring's grazing curves turn too fast".into()));
1815            }
1816            previous = Some(pair);
1817            list.push((psi, pair.0, pair.1));
1818        }
1819        samples.push(list);
1820    }
1821    let params: Vec<f64> = (0..=n).map(|i| i as f64 / n as f64).collect();
1822    let mut points: Vec<Vec3> = Vec::new();
1823    let mut curves: Vec<ToolCurve> = Vec::new();
1824    // Points: at ψ = 0 and π, on each branch.
1825    let mut corner: HashMap<(usize, usize), usize> = HashMap::new(); // (seam 0 | 1, branch)
1826    for seam in 0..2 {
1827        let (psi, (lower, upper)) = if seam == 0 { (0.0, (samples[0][0].1, samples[0][0].2)) } else { (PI, (samples[1][0].1, samples[1][0].2)) };
1828        for (branch, v) in [(0, lower), (1, upper)] {
1829            points.push(point(psi, v));
1830            corner.insert((seam, branch), points.len() - 1);
1831        }
1832    }
1833    let seam_of_end = |half: usize, at_end: bool| -> usize { if at_end { 1 - half } else { half } };
1834    // K: each branch over each half.
1835    let mut branch_curve: HashMap<(usize, usize), usize> = HashMap::new(); // (half, branch)
1836    for half in 0..2 {
1837        for branch in 0..2 {
1838            let mut xyz: Vec<Vec3> = samples[half].iter().map(|&(psi, lo, hi)| point(psi, if branch == 0 { lo } else { hi })).collect();
1839            let (start, end) = (corner[&(seam_of_end(half, false), branch)], corner[&(seam_of_end(half, true), branch)]);
1840            xyz[0] = points[start];
1841            xyz[n] = points[end];
1842            curves.push(ToolCurve { curve: frag(crate::interpolate_curve(&xyz, 5, &params), "ring_branch")?, start, end, degenerate: false, samples: Some(xyz.clone()) });
1843            branch_curve.insert((half, branch), curves.len() - 1);
1844        }
1845    }
1846    // Seam arcs of each band at ψ = 0 and π: band 0 (front) from lower to
1847    // upper, band 1 (back) from upper to lower + 2π.
1848    let tube_arc = |psi: f64, v0: f64, v1: f64| -> Result<NurbsCurve, KernelRefusal> {
1849        let start = point(psi, v0);
1850        let axis = radial(psi).cross(w);
1851        let surface = frag(make_revolution(tube_centre(psi), axis, &frag(make_line(start, start.add(axis)), "line")?, v1 - v0), "tube_arc")?;
1852        frag(surface.iso_curve_v(0.0), "tube_arc")
1853    };
1854    let mut seam_arc: HashMap<(usize, usize), usize> = HashMap::new(); // (seam, band)
1855    for seam in 0..2 {
1856        let psi = if seam == 0 { 0.0 } else { PI };
1857        let (lower, upper) = (samples[seam][0].1, samples[seam][0].2);
1858        let (lo_point, hi_point) = (corner[&(seam, 0)], corner[&(seam, 1)]);
1859        curves.push(ToolCurve { curve: tube_arc(psi, lower, upper)?, start: lo_point, end: hi_point, degenerate: false, samples: None });
1860        seam_arc.insert((seam, 0), curves.len() - 1);
1861        curves.push(ToolCurve { curve: tube_arc(psi, upper, lower + TAU)?, start: hi_point, end: lo_point, degenerate: false, samples: None });
1862        seam_arc.insert((seam, 1), curves.len() - 1);
1863    }
1864    let mut caps = Vec::new();
1865    for half in 0..2 {
1866        let (psi_a, psi_b) = halves[half];
1867        for band in 0..2 {
1868            // The band's lower and upper edges (v), as branches.
1869            let (lo_branch, hi_branch, hi_turn) = if band == 0 { (0usize, 1usize, 0.0) } else { (1, 0, TAU) };
1870            let pick = |s: &(f64, f64, f64), b: usize| if b == 0 { s.1 } else { s.2 };
1871            // A margin each side: an end cap may be carried a little across its
1872            // grazing curve (see `end_trim`), as far as the tube leaves room.
1873            let low = samples[half].iter().map(|s| pick(s, lo_branch)).fold(f64::INFINITY, f64::min);
1874            let high = samples[half].iter().map(|s| pick(s, hi_branch) + hi_turn).fold(f64::NEG_INFINITY, f64::max);
1875            let margin = (0.25 * (TAU - (high - low))).clamp(1e-3, 0.25);
1876            let (v_low, v_high) = (low - margin, high + margin);
1877            let theta_v = v_high - v_low;
1878            if theta_v >= TAU {
1879                return Err(refuse("ring_grazing", "a band of the ring wraps all the way round its tube".into()));
1880            }
1881            let generatrix = tube_arc(psi_a, v_low, v_high)?;
1882            let surface = frag(make_revolution(origin, w, &generatrix, psi_b - psi_a), "ring_band")?;
1883            let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
1884            let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
1885            let uv_of = |psi: f64, v: f64| {
1886                Vec3::new(
1887                    crate::circle_angle_to_parameter(spans_u, psi_b - psi_a, psi - psi_a),
1888                    crate::circle_angle_to_parameter(spans_v, theta_v, v - v_low),
1889                    0.0,
1890                )
1891            };
1892            // (ψ, v) of a point, unwrapped near the patch.
1893            let angles_of = |x: Vec3| -> (f64, f64) {
1894                let r = x.sub(origin);
1895                let psi = r.dot(u2).atan2(r.dot(u1));
1896                let psi = 0.5 * (psi_a + psi_b) + (psi - 0.5 * (psi_a + psi_b) + PI).rem_euclid(TAU) - PI;
1897                let q = x.sub(tube_centre(psi));
1898                let v = q.dot(w).atan2(q.dot(radial(psi)));
1899                let mid = 0.5 * (v_low + v_high);
1900                (psi, mid + (v - mid + PI).rem_euclid(TAU) - PI)
1901            };
1902            let inverse_pcurve = |curve: &NurbsCurve| -> Result<NurbsCurve, KernelRefusal> {
1903                let [d0, d1] = frag(curve.domain(), "domain")?;
1904                let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
1905                let mut uv = Vec::with_capacity(ts.len());
1906                for &t in &ts {
1907                    let (psi, v) = angles_of(frag(curve.evaluate(t), "point")?);
1908                    uv.push(uv_of(psi, v));
1909                }
1910                frag(crate::interpolate_curve(&uv, 5, &ts), "ring_pcurve")
1911            };
1912            let branch_pcurve = |b: usize, _turn: f64| -> Result<NurbsCurve, KernelRefusal> { inverse_pcurve(&curves[branch_curve[&(half, b)]].curve) };
1913            let lower_edge = branch_curve[&(half, lo_branch)];
1914            let upper_edge = branch_curve[&(half, hi_branch)];
1915            let right = seam_arc[&(seam_of_end(half, true), band)];
1916            let left = seam_arc[&(seam_of_end(half, false), band)];
1917            let coedges = vec![
1918                (lower_edge, true, branch_pcurve(lo_branch, 0.0)?),
1919                (right, true, inverse_pcurve(&curves[right].curve)?),
1920                (upper_edge, false, frag(branch_pcurve(hi_branch, hi_turn)?.reversed(), "reversed")?),
1921                (left, false, frag(inverse_pcurve(&curves[left].curve)?.reversed(), "reversed")?),
1922            ];
1923            let mid = &samples[half][n / 2];
1924            let front = g(mid.0, 0.5 * (pick(mid, lo_branch) + pick(mid, hi_branch) + hi_turn)) > 0.0;
1925            caps.push(Cap { surface, front, coedges, chart: Some(Chart::Revolution { origin, w, u1, u2, alpha: psi_a, theta: psi_b - psi_a, spans_u, meridian: MeridianChart::Arc { centre: [rho_c, z_c], start: v_low, sweep: theta_v, spans: spans_v } }) });
1926        }
1927    }
1928    Ok(Split {
1929        points,
1930        curves,
1931        caps,
1932        slides: Vec::new(),
1933        slide_sides: Vec::new(),
1934        scale: rho_c + radius,
1935        shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() },
1936    })
1937}
1938
1939/// Builder of a closed shell with shared vertices and edges.
1940struct Shell {
1941    vertices: Vec<VertexRecord>,
1942    edges: Vec<EdgeRecord>,
1943    faces: Vec<FaceRecord>,
1944    next: u64,
1945}
1946
1947impl Shell {
1948    fn new() -> Self {
1949        Self { vertices: Vec::new(), edges: Vec::new(), faces: Vec::new(), next: 1 }
1950    }
1951
1952    fn id(&mut self) -> u64 {
1953        self.next += 1;
1954        self.next
1955    }
1956
1957    fn vertex(&mut self, point: Vec3) -> u64 {
1958        let id = self.id();
1959        self.vertices.push(VertexRecord { id, point });
1960        id
1961    }
1962
1963    fn edge(&mut self, curve: NurbsCurve, start: u64, end: u64, degenerate: bool) -> Result<u64, KernelRefusal> {
1964        let domain = frag(curve.domain(), "edge_domain")?;
1965        let id = self.id();
1966        self.edges.push(EdgeRecord { id, curve, t0: domain[0], t1: domain[1], start_vertex_id: start, end_vertex_id: end, degenerate, name: None });
1967        Ok(id)
1968    }
1969
1970    /// A face from its surface and its loop: (edge, traversed forward?, pcurve
1971    /// in the traversal direction).
1972    fn face(&mut self, surface: NurbsSurface, coedges: Vec<(u64, bool, NurbsCurve)>) -> u64 {
1973        let mut records = Vec::with_capacity(coedges.len());
1974        for (edge, forward, pcurve) in coedges {
1975            let id = self.id();
1976            records.push(CoedgeRecord { id, edge_id: edge, forward, pcurve });
1977        }
1978        let loop_id = self.id();
1979        let id = self.id();
1980        self.faces.push(FaceRecord { id, surface, same_sense: true, loops: vec![LoopRecord { id: loop_id, coedges: records }], name: None });
1981        id
1982    }
1983
1984    fn solid(mut self) -> BrepSolid {
1985        let shell = self.id();
1986        let id = self.id();
1987        // One shell: V − E + F = 2 − 2g (every face one loop; a pole's
1988        // degenerate edge is not counted, as the validator counts).
1989        let edges = self.edges.iter().filter(|e| !e.degenerate).count() as i64;
1990        let euler = self.vertices.len() as i64 - edges + self.faces.len() as i64;
1991        let genus = (2 - euler) / 2;
1992        BrepSolid {
1993            mass_properties_cache: Default::default(),
1994            id,
1995            vertices: self.vertices,
1996            edges: self.edges,
1997            shells: vec![ShellRecord { id: shell, faces: self.faces }],
1998            genus,
1999            kind: Default::default(),
2000            cells: Default::default(),
2001        }
2002    }
2003}
2004
2005/// The whole sweep of `tool` along `motion` (a single screw, turn or slide),
2006/// its two end positions included.
2007pub fn sweep_solid(tool: &SolidTool, motion: &SolidMotion) -> Result<BrepSolid, KernelRefusal> {
2008    sweep_with(&Screw::from_motion(motion)?, tool)
2009}
2010
2011fn sweep_with(screw: &Screw, tool: &SolidTool) -> Result<BrepSolid, KernelRefusal> {
2012    let screw = *screw;
2013    if let SolidTool::Revolved { axis_origin, axis_direction, meridian, .. } = tool {
2014        if let Some(body) = ball_across_axis(&screw, *axis_origin, *axis_direction, meridian)? {
2015            return Ok(body);
2016        }
2017    }
2018    if let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool {
2019        if let Some(body) = flat_ring(&screw, *axis_origin, *axis_direction, *radial, meridian)? {
2020            return Ok(body);
2021        }
2022        if let Some(body) = rod_across_axis(&screw, *axis_origin, *axis_direction, meridian)? {
2023            return Ok(body);
2024        }
2025    }
2026    if let SolidTool::Polyhedron { vertices, faces } = tool {
2027        let whole = screw.omega != 0.0 && screw.c == 0.0 && screw.c2 == 0.0 && screw.frame.is_none() && screw.turns.is_none() && screw.omega.abs() * (screw.t1 - screw.t0) >= TAU * (1.0 - 1e-12);
2028        if whole {
2029            return whole_turn_shadow(&screw, vertices, faces);
2030        }
2031    }
2032    let split = match tool {
2033        SolidTool::Polyhedron { vertices, faces } => split_polyhedron(&screw, vertices, faces)?,
2034        SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } => {
2035            split_revolved(&screw, *axis_origin, *axis_direction, *radial, meridian)?
2036        }
2037    };
2038    let grazing = split.grazing()?;
2039    // A tool slid along its own axis has no grazing curve: its sliding sides
2040    // join its back at the start to its front at the end.
2041    if grazing.is_empty() && split.slide_sides.is_empty() {
2042        return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
2043    }
2044    let times = row_times(&screw);
2045    let last = times.len() - 1;
2046    let mut shell = Shell::new();
2047    // Vertices and edges at the row times (0 and `last` are the end positions),
2048    // made on demand.
2049    let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
2050    let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
2051    let mut at_time = |shell: &mut Shell, j: usize, p: usize| -> u64 {
2052        *vertex_at.entry((j, p)).or_insert_with(|| shell.vertex(screw.apply(times[j], split.points[p])))
2053    };
2054    let mut time_edge = |shell: &mut Shell, j: usize, c: usize, vertex: &mut dyn FnMut(&mut Shell, usize, usize) -> u64| -> Result<u64, KernelRefusal> {
2055        if let Some(&edge) = edge_at.get(&(j, c)) {
2056            return Ok(edge);
2057        }
2058        let curve = &split.curves[c];
2059        let (a, b) = (vertex(shell, j, curve.start), vertex(shell, j, curve.end));
2060        let edge = shell.edge(screw.move_curve(times[j], &curve.curve)?, a, b, curve.degenerate)?;
2061        edge_at.insert((j, c), edge);
2062        Ok(edge)
2063    };
2064    // Caps: the back at t0, the front at t1.
2065    for cap in &split.caps {
2066        let j = if cap.front { last } else { 0 };
2067        let surface = screw.move_surface(times[j], &cap.surface)?;
2068        let mut coedges = Vec::with_capacity(cap.coedges.len());
2069        for (c, forward, pcurve) in &cap.coedges {
2070            let edge = time_edge(&mut shell, j, *c, &mut at_time)?;
2071            coedges.push((edge, *forward, pcurve.clone()));
2072        }
2073        shell.face(surface, coedges);
2074    }
2075    // Walls, a row at a time. The back cap at t0 runs K's curve with the back
2076    // on its left, so the wall beside it runs the other way: u along the curve
2077    // that way, v from the row's start to its end, the loop (u0,0) (u1,0)
2078    // (u1,1) (u0,1).
2079    // The paths of K's corners, a row at a time: (row, point) -> edge.
2080    let mut orbit_at: HashMap<(usize, usize), u64> = HashMap::new();
2081    for row in 0..last {
2082        let (ta, tb) = (times[row], times[row + 1]);
2083        let segments = carry_segments(&screw, ta, tb);
2084        let knots_v = carry_knots(segments);
2085        let mut orbits: HashMap<usize, u64> = HashMap::new();
2086        for &(c, _) in &grazing {
2087            for r in [split.curves[c].start, split.curves[c].end] {
2088                if orbits.contains_key(&r) {
2089                    continue;
2090                }
2091                let (v0, v1) = (at_time(&mut shell, row, r), at_time(&mut shell, row + 1, r));
2092                let edge = shell.edge(carry_curve(&screw, split.points[r], ta, tb, segments)?, v0, v1, false)?;
2093                orbits.insert(r, edge);
2094                orbit_at.insert((row, r), edge);
2095            }
2096        }
2097        for &(c, back_forward) in &grazing {
2098            let tool_curve = &split.curves[c];
2099            let along = if back_forward { frag(tool_curve.curve.reversed(), "reversed")? } else { tool_curve.curve.clone() };
2100            let (from, to) = if back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
2101            let mut rows = Vec::with_capacity(along.control_points.len());
2102            for p in &along.control_points {
2103                let x = frag(p.point(), "control_point")?;
2104                rows.push(carry_points(&screw, x, ta, tb, segments).into_iter().map(|q| Vec4::from_point(q, p.w)).collect::<Vec<_>>());
2105            }
2106            let surface = frag(NurbsSurface::new(along.degree, 5, along.knots.clone(), knots_v.clone(), rows), "wall")?;
2107            let [u0, u1] = frag(along.domain(), "domain")?;
2108            let bottom = time_edge(&mut shell, row, c, &mut at_time)?;
2109            let top = time_edge(&mut shell, row + 1, c, &mut at_time)?;
2110            let coedges = vec![
2111                (bottom, !back_forward, uv_line([u0, 0.0], [u1, 0.0])?),
2112                (orbits[&to], true, uv_line([u1, 0.0], [u1, 1.0])?),
2113                (top, back_forward, uv_line([u1, 1.0], [u0, 1.0])?),
2114                (orbits[&from], false, uv_line([u0, 1.0], [u0, 0.0])?),
2115            ];
2116            shell.face(surface, coedges);
2117        }
2118    }
2119    // Sliding ends: each rim arc where its neighbour's cap sits (back at t0,
2120    // front at t1), joined by the paths of the points where that changes.
2121    // Faces the tool slides within: the one-pass probe stands further off them.
2122    let mut sliding: std::collections::HashSet<u64> = std::collections::HashSet::new();
2123    let side_of: HashMap<usize, bool> = split.caps.iter().flat_map(|cap| cap.coedges.iter().map(move |&(c, _, _)| (c, cap.front))).collect();
2124    for slide in &split.slides {
2125        let centre = screw.apply(times[0], slide.centre);
2126        let u = screw.apply(times[0], slide.centre.add(slide.u)).sub(centre);
2127        let v = screw.apply(times[0], slide.centre.add(slide.v)).sub(centre);
2128        let row_of = |c: usize| -> Result<usize, KernelRefusal> {
2129            match side_of.get(&c) {
2130                Some(true) => Ok(last),
2131                Some(false) => Ok(0),
2132                None => Err(refuse("revolved_grazing", "a sliding end has no face beside its rim".into())),
2133            }
2134        };
2135        let mut coedges = Vec::new();
2136        let mut corners: Vec<Vec3> = Vec::new();
2137        let mut push = |coedges: &mut Vec<(u64, bool, NurbsCurve)>, corners: &mut Vec<Vec3>, edge: u64, forward: bool, curve: &NurbsCurve| -> Result<(), KernelRefusal> {
2138            let [d0, d1] = frag(curve.domain(), "domain")?;
2139            for k in 0..=8 {
2140                corners.push(frag(curve.evaluate(d0 + (d1 - d0) * k as f64 / 8.0), "slide")?);
2141            }
2142            coedges.push((edge, forward, plane_pcurve(centre, u, v, curve, forward)?));
2143            Ok(())
2144        };
2145        for (index, &(c, forward)) in slide.rim.iter().enumerate() {
2146            let j = row_of(c)?;
2147            let edge = time_edge(&mut shell, j, c, &mut at_time)?;
2148            push(&mut coedges, &mut corners, edge, forward, &screw.move_curve(times[j], &split.curves[c].curve)?)?;
2149            // On to the next arc's time along the path of the point between them.
2150            let (next, _) = slide.rim[(index + 1) % slide.rim.len()];
2151            let next_j = row_of(next)?;
2152            if next_j == j {
2153                continue;
2154            }
2155            let point = if forward { split.curves[c].end } else { split.curves[c].start };
2156            let rows: Vec<usize> = if next_j > j { (j..next_j).collect() } else { (next_j..j).rev().collect() };
2157            for row in rows {
2158                let Some(&orbit) = orbit_at.get(&(row, point)) else {
2159                    return Err(refuse("revolved_grazing", "a sliding end's rim changes side where no grazing curve meets it".into()));
2160                };
2161                let (ta, tb) = (times[row], times[row + 1]);
2162                let curve = carry_curve(&screw, split.points[point], ta, tb, carry_segments(&screw, ta, tb))?;
2163                push(&mut coedges, &mut corners, orbit, next_j > j, &curve)?;
2164            }
2165        }
2166        sliding.insert(shell.face(plane_patch(centre, u, v, &corners)?, coedges));
2167    }
2168    // Sliding cylinders: from the start rim where its neighbour sits to the
2169    // end rim where its neighbour sits, in halves seamed along generators.
2170    for side in &split.slide_sides {
2171        let row_of = |c: usize| -> Result<usize, KernelRefusal> {
2172            match side_of.get(&c) {
2173                Some(true) => Ok(last),
2174                Some(false) => Ok(0),
2175                None => Err(refuse("revolved_grazing", "a sliding side has no face beside its rim".into())),
2176            }
2177        };
2178        let origin = screw.apply(times[0], side.origin);
2179        let w = screw.apply(times[0], side.origin.add(side.w)).sub(origin);
2180        let rows: Vec<[usize; 2]> = side.patches.iter().map(|patch| Ok([row_of(patch.2)?, row_of(patch.3)?])).collect::<Result<_, KernelRefusal>>()?;
2181        if rows.iter().any(|r| *r != rows[0]) {
2182            return Err(refuse("revolved_grazing", "a sliding side's rim changes side along it".into()));
2183        }
2184        let [jb, ja] = rows[0];
2185        // Seams: the generator at each patch's start angle, rim point to rim point.
2186        let mut seams = Vec::with_capacity(side.patches.len());
2187        for patch in &side.patches {
2188            let (a, b) = (at_time(&mut shell, jb, patch.4[0]), at_time(&mut shell, ja, patch.4[1]));
2189            let line = frag(make_line(screw.apply(times[jb], split.points[patch.4[0]]), screw.apply(times[ja], split.points[patch.4[1]])), "seam")?;
2190            seams.push((shell.edge(line.clone(), a, b, false)?, line));
2191        }
2192        for (k, patch) in side.patches.iter().enumerate() {
2193            let surface = frag(make_revolution(origin, w, &seams[k].1, patch.1), "slide_side")?;
2194            let lower = time_edge(&mut shell, jb, patch.2, &mut at_time)?;
2195            let upper = time_edge(&mut shell, ja, patch.3, &mut at_time)?;
2196            let next = (k + 1) % side.patches.len();
2197            let face = shell.face(
2198                surface,
2199                vec![
2200                    (lower, true, uv_line([0.0, 0.0], [1.0, 0.0])?),
2201                    (seams[next].0, true, uv_line([1.0, 0.0], [1.0, 1.0])?),
2202                    (upper, false, uv_line([1.0, 1.0], [0.0, 1.0])?),
2203                    (seams[k].0, false, uv_line([0.0, 1.0], [0.0, 0.0])?),
2204                ],
2205            );
2206            sliding.insert(face);
2207        }
2208    }
2209    let body = shell.solid();
2210    let issues = body.validate();
2211    if !issues.is_empty() {
2212        return Err(refuse("assembly", format!("the assembled sweep is not valid: {:?}", issues.first())));
2213    }
2214    check_single_pass(&screw, &split, &body, &sliding)?;
2215    Ok(body)
2216}
2217
2218/// A rod (a cylinder with flat ends) TURNED about a line that crosses its
2219/// axis squarely between its ends, if `meridian` is one and the motion such.
2220///
2221/// Arm by arm: the rod is its arm one way from the turn's axis and its arm
2222/// the other way, and sweeps what they sweep. At a height z (half-width
2223/// w = √(r² − z²)) an arm of length L is the rectangle 0 ≤ x ≤ L, |y| ≤ w,
2224/// which a circle about the turn's axis of radius ρ ≤ L meets in one arc
2225/// holding the arm's own direction, and beyond, out to √(L² + w²), in two
2226/// arcs ±[a, b] with a ≤ acos(L / √(L² + r²)). Turned by more than 2a, the
2227/// two arcs sweep the same angles as the arc [−b, b] they leave a gap in:
2228/// the arm with the gap filled, its points beyond its end within
2229/// √(L² + r²) of the crossing point (a cap of that ball). The filled arm
2230/// meets every circle in one arc holding its direction, so it sweeps itself
2231/// where it starts and where it stops and, between, its section by the half
2232/// plane at its direction turned. So the sweep is the union of the filled rod
2233/// (one solid of revolution about the rod's axis) at both ends of the turn
2234/// and the two arms' sections turned.
2235fn rod_across_axis(screw: &Screw, origin: Vec3, axis: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
2236    if screw.frame.is_some() || screw.turns.is_some() || screw.omega == 0.0 || screw.c != 0.0 || screw.c2 != 0.0 {
2237        return Ok(None);
2238    }
2239    let Ok(w) = axis.normalized() else { return Ok(None) };
2240    // A rectangle against the axis: radius r from z0 to z1.
2241    let mut corners: Vec<[f64; 2]> = Vec::new();
2242    for piece in meridian {
2243        let OutlinePiece::Line { from, to } = piece else { return Ok(None) };
2244        corners.push(*from);
2245        corners.push(*to);
2246    }
2247    if meridian.len() != 4 {
2248        return Ok(None);
2249    }
2250    let r = corners.iter().map(|c| c[0]).fold(0.0f64, f64::max);
2251    let (z0, z1) = corners.iter().map(|c| c[1]).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), z| (lo.min(z), hi.max(z)));
2252    let scale = r.max(z0.abs()).max(z1.abs());
2253    let on = |a: f64, b: f64| (a - b).abs() <= 1e-12 * scale;
2254    if !(r > 0.0 && z1 > z0) || !corners.iter().all(|c| (on(c[0], 0.0) || on(c[0], r)) && (on(c[1], z0) || on(c[1], z1))) {
2255        return Ok(None);
2256    }
2257    // The turn's axis squarely across the rod's, between its ends.
2258    if w.dot(screw.e).abs() > 1e-12 {
2259        return Ok(None);
2260    }
2261    let offset = screw.a.sub(origin);
2262    let along = offset.dot(w);
2263    let foot = origin.add(w.scale(along));
2264    let apart = screw.a.sub(foot);
2265    if apart.sub(screw.e.scale(apart.dot(screw.e))).length() > 1e-12 * (scale + offset.length()) {
2266        return Ok(None);
2267    }
2268    if !(along > z0 + 1e-9 * scale && along < z1 - 1e-9 * scale) {
2269        return Ok(None);
2270    }
2271    let theta = screw.omega.abs() * (screw.t1 - screw.t0);
2272    let arms = [z1 - along, along - z0];
2273    let gap = arms.iter().map(|l| (l / l.hypot(r)).acos()).fold(0.0f64, f64::max);
2274    if !(theta > 2.0 * gap * (1.0 + 1e-9) && theta < TAU * (1.0 - 1e-9)) {
2275        return Err(refuse("rod_across", "a rod turned about a line through it is swept only for a turn wider than its ends' gaps and short of a whole turn".into()));
2276    }
2277    let arc = |centre: Vec3, from: Vec3, normal: Vec3, angle: f64| -> Result<NurbsCurve, KernelRefusal> {
2278        let surface = frag(make_revolution(centre, normal, &frag(make_line(from, from.add(normal)), "line")?, angle), "rod_across")?;
2279        frag(surface.iso_curve_v(0.0), "rod_across")
2280    };
2281    let line = |a: Vec3, b: Vec3| frag(make_line(a, b), "rod_across");
2282    // At one time: the crossing point, the rod's direction and the turn's.
2283    let filled_at = |t: f64| -> Result<BrepSolid, KernelRefusal> {
2284        let o = screw.apply(t, foot);
2285        let x = screw.apply(t, foot.add(w)).sub(o);
2286        let up = screw.e;
2287        let at = |rho: f64, s: f64| o.add(up.scale(rho)).add(x.scale(s));
2288        let n = up.cross(x);
2289        let (far_plus, far_minus) = (arms[0].hypot(r), arms[1].hypot(r));
2290        let profile = [
2291            arc(o, at(0.0, -far_minus), n, r.atan2(arms[1]))?,
2292            line(at(r, -arms[1]), at(r, arms[0]))?,
2293            arc(o, at(r, arms[0]), n, r.atan2(arms[0]))?,
2294            line(at(0.0, far_plus), at(0.0, -far_minus))?,
2295        ];
2296        frag(crate::revolve_profile_brep(&profile, o, x, TAU), "rod_across")
2297    };
2298    let o = screw.apply(screw.t0, foot);
2299    let x = screw.apply(screw.t0, foot.add(w)).sub(o);
2300    let turn_axis = screw.e.scale(screw.omega.signum());
2301    let section = |direction: Vec3, length: f64| -> Result<BrepSolid, KernelRefusal> {
2302        let at = |rho: f64, z: f64| o.add(direction.scale(rho)).add(screw.e.scale(z));
2303        let half = (r / length.hypot(r)).asin();
2304        let profile = [
2305            line(at(0.0, -r), at(length, -r))?,
2306            arc(o, at(length, -r), direction.cross(screw.e), 2.0 * half)?,
2307            line(at(length, r), at(0.0, r))?,
2308            line(at(0.0, r), at(0.0, -r))?,
2309        ];
2310        frag(crate::revolve_profile_brep(&profile, o, turn_axis, theta), "rod_across")
2311    };
2312    let parts = [filled_at(screw.t0)?, filled_at(screw.t1)?, section(x, arms[0])?, section(x.scale(-1.0), arms[1])?];
2313    crate::boolean_operation_nary(&parts, crate::BooleanOperation::Union).map(Some)
2314}
2315
2316/// A ring (or another tool with a hole about its axis walled straight
2317/// through) lying flat to its motion (a turn about an axis parallel to the
2318/// tool's, a slide square to it), if `meridian` is one and the motion such.
2319///
2320/// Every point then moves in its own plane square to the ring's axis, where
2321/// the ring's section is an annulus. The tube passes over where the hole was,
2322/// so the sweep does not pass each point once; but an annulus sweeps what its
2323/// filled disc sweeps as soon as it travels far enough that no point stays in
2324/// the hole from start to end (the hole at the start and at the end share
2325/// nothing: travel beyond the hole's greatest width, the tube's centre
2326/// circle). So the ring sweeps what the FILLED ring sweeps: a tool with a
2327/// half-round rim and flat faces, which is swept whole. A whole turn sweeps
2328/// the solid of revolution of that tool's shadow about the turn's axis.
2329fn flat_ring(screw: &Screw, origin: Vec3, axis: Vec3, radial: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
2330    if screw.frame.is_some() || screw.turns.is_some() || screw.c2 != 0.0 {
2331        return Ok(None);
2332    }
2333    let Ok(w) = axis.normalized() else { return Ok(None) };
2334    let flat = if screw.omega != 0.0 { screw.e.cross(w).length() <= 1e-12 && screw.c == 0.0 } else { screw.e.dot(w).abs() <= 1e-12 };
2335    if !flat {
2336        return Ok(None);
2337    }
2338    // The holed tool: a torus (`torus`: its tube), or straight-sided pieces
2339    // whose hole is one upright wall from the tool's top to its bottom.
2340    // `big`: half the hole's greatest width. `filled_meridian`: the tool
2341    // with its hole closed, flat across at its top and bottom.
2342    let (big, level, torus, filled_meridian): (f64, f64, Option<f64>, Vec<OutlinePiece>) = match meridian {
2343        [OutlinePiece::Arc { centre, radius, start, end }] => {
2344            let (big, tube, level) = (centre[0], *radius, centre[1]);
2345            if ((end - start).abs() - TAU).abs() >= 1e-12 || big <= tube {
2346                return Ok(None);
2347            }
2348            (
2349                big,
2350                level,
2351                Some(tube),
2352                vec![
2353                    OutlinePiece::Line { from: [0.0, level - tube], to: [big, level - tube] },
2354                    OutlinePiece::Arc { centre: [big, level], radius: tube, start: -0.5 * PI, end: 0.5 * PI },
2355                    OutlinePiece::Line { from: [big, level + tube], to: [0.0, level + tube] },
2356                    OutlinePiece::Line { from: [0.0, level + tube], to: [0.0, level - tube] },
2357                ],
2358            )
2359        }
2360        pieces => {
2361            let mut ends: Vec<([f64; 2], [f64; 2])> = Vec::with_capacity(pieces.len());
2362            for piece in pieces {
2363                let OutlinePiece::Line { from, to } = piece else { return Ok(None) };
2364                ends.push((*from, *to));
2365            }
2366            let scale = ends.iter().flat_map(|(a, b)| [a[0].abs(), a[1].abs(), b[0].abs(), b[1].abs()]).fold(0.0f64, f64::max).max(1e-300);
2367            let inner = ends.iter().flat_map(|(a, b)| [a[0], b[0]]).fold(f64::INFINITY, f64::min);
2368            let (low, high) = ends.iter().flat_map(|(a, b)| [a[1], b[1]]).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), z| (lo.min(z), hi.max(z)));
2369            if ends.len() < 3 || inner <= 1e-9 * scale {
2370                return Ok(None);
2371            }
2372            let walls: Vec<usize> = (0..ends.len())
2373                .filter(|&k| {
2374                    let (a, b) = ends[k];
2375                    (a[0] - inner).abs() <= 1e-12 * scale && (b[0] - inner).abs() <= 1e-12 * scale && (a[1].min(b[1]) - low).abs() <= 1e-12 * scale && (a[1].max(b[1]) - high).abs() <= 1e-12 * scale
2376                })
2377                .collect();
2378            let [wall] = walls.as_slice() else { return Ok(None) };
2379            let (from, to) = ends[*wall];
2380            // From the wall's end round the outside back to its start, then
2381            // across to the axis and along it.
2382            let mut filled = vec![OutlinePiece::Line { from: [0.0, to[1]], to }];
2383            for k in 1..pieces.len() {
2384                filled.push(pieces[(wall + k) % pieces.len()].clone());
2385            }
2386            filled.push(OutlinePiece::Line { from, to: [0.0, from[1]] });
2387            filled.push(OutlinePiece::Line { from: [0.0, from[1]], to: [0.0, to[1]] });
2388            (inner, 0.5 * (low + high), None, filled)
2389        }
2390    };
2391    let centre0 = origin.add(w.scale(level));
2392    let filled = SolidTool::Revolved { axis_origin: origin, axis_direction: w, radial, meridian: filled_meridian };
2393    let short = || refuse("ring_flat", "a ring lying flat to its motion travels less than its hole is wide: the hole is not swept shut".into());
2394    if screw.omega == 0.0 {
2395        if screw.c.abs() * (screw.t1 - screw.t0) <= 2.0 * big * (1.0 + 1e-9) {
2396            let Some(tube) = torus else { return Err(short()) };
2397            // Short of its hole: what stays open, all the way through, is the
2398            // lens the hole shares where the ring starts and where it stops.
2399            // The hole at one place: inside the ring's inner half, between
2400            // its top and bottom planes.
2401            let hole = |t: f64| -> Result<BrepSolid, KernelRefusal> {
2402                let o = screw.apply(t, centre0);
2403                let u = frag(radial.sub(w.scale(radial.dot(w))).normalized(), "ring_flat")?;
2404                let at = |rho: f64, z: f64| o.add(u.scale(rho)).add(w.scale(z));
2405                let n = w.cross(u);
2406                let from = at(big, -tube);
2407                let inner = frag(frag(make_revolution(at(big, 0.0), n, &frag(make_line(from, from.add(n)), "line")?, PI), "ring_flat")?.iso_curve_v(0.0), "ring_flat")?;
2408                let profile = [
2409                    frag(make_line(at(0.0, -tube), at(big, -tube)), "ring_flat")?,
2410                    inner,
2411                    frag(make_line(at(big, tube), at(0.0, tube)), "ring_flat")?,
2412                    frag(make_line(at(0.0, tube), at(0.0, -tube)), "ring_flat")?,
2413                ];
2414                frag(crate::revolve_profile_brep(&profile, o, w, TAU), "ring_flat")
2415            };
2416            let lens = crate::boolean_operation_nary(&[hole(screw.t0)?, hole(screw.t1)?], crate::BooleanOperation::Intersect)?;
2417            let whole = sweep_with(screw, &filled)?;
2418            return crate::boolean_operation_nary(&[whole, lens], crate::BooleanOperation::Subtract).map(Some);
2419        }
2420        return sweep_with(screw, &filled).map(Some);
2421    }
2422    // A turn: the ring's centre runs a circle of radius `reach` about the axis.
2423    let start_centre = screw.apply(screw.t0, centre0);
2424    let from_axis = start_centre.sub(screw.a);
2425    let across = from_axis.sub(screw.e.scale(from_axis.dot(screw.e)));
2426    let reach = across.length();
2427    let theta = screw.omega.abs() * (screw.t1 - screw.t0);
2428    if theta >= TAU * (1.0 - 1e-12) {
2429        let Some(tube) = torus else {
2430            return Err(refuse("ring_flat", "a whole turn of a holed tool in its own plane is swept for a ring only".into()));
2431        };
2432        if reach <= (big + tube) * (1.0 + 1e-9) {
2433            return Err(refuse("ring_flat", "a ring turned a whole turn in its own plane reaches the turn's axis".into()));
2434        }
2435        let out = frag(across.normalized(), "ring_flat")?;
2436        let foot = start_centre.sub(across);
2437        let at = |rho: f64, z: f64| foot.add(out.scale(rho)).add(screw.e.scale(z));
2438        let half_round = |centre: Vec3, from: Vec3| -> Result<NurbsCurve, KernelRefusal> {
2439            let n = out.cross(screw.e);
2440            let surface = frag(make_revolution(centre, n, &frag(make_line(from, from.add(n)), "line")?, PI), "ring_flat")?;
2441            frag(surface.iso_curve_v(0.0), "ring_flat")
2442        };
2443        let profile = [
2444            frag(make_line(at(reach - big, -tube), at(reach + big, -tube)), "ring_flat")?,
2445            half_round(at(reach + big, 0.0), at(reach + big, -tube))?,
2446            frag(make_line(at(reach + big, tube), at(reach - big, tube)), "ring_flat")?,
2447            half_round(at(reach - big, 0.0), at(reach - big, tube))?,
2448        ];
2449        return frag(crate::revolve_profile_brep(&profile, foot, screw.e, TAU), "ring_flat").map(Some);
2450    }
2451    let covered = if theta >= PI { big < reach } else { reach * (0.5 * theta).sin() > big * (1.0 + 1e-9) };
2452    if !covered {
2453        return Err(short());
2454    }
2455    sweep_with(screw, &filled).map(Some)
2456}
2457
2458/// The sweep of `tool` along `motion`, where it lies in `clip` (the caller
2459/// cuts a workpiece inside that region). A TURN about the clip's own axis
2460/// whose two end positions stay outside the clip's wedge covers the wedge
2461/// wholly with the tool's shadow in (ρ, z): that shadow, turned over the wedge,
2462/// is the sweep there (a convex polyhedron's shadow is bounded by conic arcs
2463/// from its edges and straight lines from its faces, so every surface is
2464/// exact); a turn about that axis whose ends reach the wedge is refused. A
2465/// screw or turn about the region's axis whose tool reaches far past the
2466/// region's radius sweeps only the tool's part that can reach it (see
2467/// `trim_to_clip`). Any other motion gets the whole sweep ([`sweep_solid`]).
2468pub fn sweep_solid_clipped(tool: &SolidTool, motion: &SolidMotion, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
2469    let screw = Screw::from_motion(motion)?;
2470    if let (SolidTool::Polyhedron { vertices, faces }, Some(wedge)) = (tool, clip.angle) {
2471        let axis = clip.axis_direction.normalized().map_err(|_| input("clip_axis", "the clip's axis has no direction".into()))?;
2472        let same_axis = screw.c == 0.0 && screw.c2 == 0.0
2473            && screw.e.cross(axis).length() < 1e-12
2474            && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
2475        if same_axis {
2476            return turn_shadow(&screw, vertices, faces, clip, axis, wedge);
2477        }
2478    }
2479    if let SolidTool::Polyhedron { vertices, faces } = tool {
2480        if let Some((vertices, faces)) = trim_to_clip(&screw, vertices, faces, clip)? {
2481            return sweep_solid(&SolidTool::Polyhedron { vertices, faces }, motion);
2482        }
2483    }
2484    sweep_solid(tool, motion)
2485}
2486
2487/// The sweep of `tool` along a turn whose rise grows as it turns, where it lies
2488/// in `clip` (as [`sweep_solid_clipped`]). Its front and back are those at
2489/// the middle of the motion; a tool whose faces change side along it is refused.
2490pub fn sweep_solid_rising(tool: &SolidTool, motion: &RisingTurn, clip: &crate::SweepClip) -> Result<BrepSolid, KernelRefusal> {
2491    let screw = Screw::from_rising(motion)?;
2492    let SolidTool::Polyhedron { vertices, faces } = tool else {
2493        return Err(refuse("rising_tool", "a growing rise sweeps a polyhedron only".into()));
2494    };
2495    let (vertices, faces) = trim_to_clip(&screw, vertices, faces, clip)?.unwrap_or_else(|| (vertices.clone(), faces.clone()));
2496    sweep_with(&screw, &SolidTool::Polyhedron { vertices, faces })
2497}
2498
2499/// The sweep of `tool` along `motion` as parts whose union it is (a caller cuts
2500/// a workpiece by all of them). One part where the whole sweep passes once. A
2501/// convex polyhedron whose sweep folds over itself (a face facing away from a
2502/// screw's or turn's axis grazes along a line inside it, and the faces beyond
2503/// that line are swept over again) is split by the plane through the axis and
2504/// that line: each half grazes that face only along its cut edge, and is swept
2505/// whole. The halves' walls share cylinders about the axis.
2506pub fn sweep_solid_parts(tool: &SolidTool, motion: &SolidMotion) -> Result<Vec<BrepSolid>, KernelRefusal> {
2507    let folded = match sweep_solid(tool, motion) {
2508        Ok(body) => return Ok(vec![body]),
2509        Err(error) if refused_as(&error) == Some("folds") => error,
2510        Err(error) => return Err(error),
2511    };
2512    let SolidTool::Polyhedron { vertices, faces } = tool else { return Err(folded) };
2513    let screw = Screw::from_motion(motion)?;
2514    if screw.omega == 0.0 {
2515        return Err(folded);
2516    }
2517    let centroid = vertices.iter().fold(Vec3::new(0.0, 0.0, 0.0), |a, &p| a.add(p)).scale(1.0 / vertices.len() as f64);
2518    let mut planes: Vec<(Vec3, f64)> = Vec::new();
2519    for face in faces {
2520        let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
2521        let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
2522        if n.dot(p.sub(centroid)) < 0.0 {
2523            n = n.scale(-1.0);
2524        }
2525        planes.push((n, n.dot(p)));
2526    }
2527    // A face square to the axis' plane through it (its normal square to the
2528    // axis) graze along the line through its foot from the axis; it folds if
2529    // it faces away from the axis and that line crosses it.
2530    for (n, d) in planes.clone() {
2531        if n.dot(screw.e).abs() > 1e-12 {
2532            continue;
2533        }
2534        let foot = screw.a.add(n.scale(d - n.dot(screw.a)));
2535        let radial = foot.sub(screw.a).sub(screw.e.scale(foot.sub(screw.a).dot(screw.e)));
2536        if radial.dot(n) <= 0.0 {
2537            continue; // faces the axis: its grazing line is the sweep's inner wall
2538        }
2539        let cut = match screw.e.cross(radial).normalized() {
2540            Ok(m) => m,
2541            Err(_) => continue,
2542        };
2543        let offset = cut.dot(screw.a);
2544        let mut parts = Vec::with_capacity(2);
2545        for sense in [1.0, -1.0] {
2546            let mut half = planes.clone();
2547            half.push((cut.scale(sense), offset * sense));
2548            let (corners, loops) = polyhedron_of_planes(&half)?;
2549            if corners.len() < 4 || loops.len() < 4 {
2550                parts.clear();
2551                break;
2552            }
2553            match sweep_solid(&SolidTool::Polyhedron { vertices: corners, faces: loops }, motion) {
2554                Ok(body) => parts.push(body),
2555                Err(_) => {
2556                    parts.clear();
2557                    break;
2558                }
2559            }
2560        }
2561        if parts.len() == 2 {
2562            return Ok(parts);
2563        }
2564    }
2565    Err(folded)
2566}
2567
2568/// A screw or turn about the clip's axis maps the clip's cylinder (radius
2569/// ≤ R) onto itself, so inside it the sweep of the tool is the sweep of any
2570/// part of the tool holding the tool's part inside the cylinder. A tool
2571/// reaching far past it (a bar pointing at the axis) is replaced by its part
2572/// inside the prism over the shadow's piece of a slightly larger disc, with
2573/// that disc's arc replaced by the two tangents at its ends: those faces graze
2574/// only along their own end edges, where the far ends of a long tool would
2575/// fold the sweep over itself. None when the tool needs no trim.
2576fn trim_to_clip(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip) -> Result<Option<(Vec<Vec3>, Vec<Vec<usize>>)>, KernelRefusal> {
2577    let Ok(axis) = clip.axis_direction.normalized() else { return Ok(None) };
2578    let same_axis = screw.omega != 0.0
2579        && screw.e.cross(axis).length() < 1e-12
2580        && screw.a.sub(clip.axis_origin).cross(axis).length() < 1e-9 * (1.0 + clip.radius[1]);
2581    if !same_axis || !clip.radius[1].is_finite() {
2582        return Ok(None);
2583    }
2584    let ez = axis;
2585    let ex = Vec3::new(1.0, 0.0, 0.0).sub(ez.scale(ez.x)).normalized().or_else(|_| Vec3::new(0.0, 1.0, 0.0).sub(ez.scale(ez.y)).normalized()).map_err(|e| input("clip_axis", e))?;
2586    let ey = ez.cross(ex);
2587    let flat = |p: Vec3| {
2588        let r = p.sub(clip.axis_origin);
2589        [r.dot(ex), r.dot(ey)]
2590    };
2591    let radius = clip.radius[1] * 1.02;
2592    let shadow: Vec<[f64; 2]> = vertices.iter().map(|&p| flat(p)).collect();
2593    if shadow.iter().all(|q| q[0].hypot(q[1]) <= radius) {
2594        return Ok(None);
2595    }
2596    // The shadow's convex hull, counter-clockwise.
2597    let mut points = shadow.clone();
2598    points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
2599    let cross = |o: [f64; 2], a: [f64; 2], b: [f64; 2]| (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
2600    let mut hull: Vec<[f64; 2]> = Vec::new();
2601    for pass in 0..2 {
2602        let start = hull.len();
2603        let ordered: Vec<[f64; 2]> = if pass == 0 { points.clone() } else { points.iter().rev().copied().collect() };
2604        for p in ordered {
2605            while hull.len() >= start + 2 && cross(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0 {
2606                hull.pop();
2607            }
2608            hull.push(p);
2609        }
2610        hull.pop();
2611    }
2612    // Where the hull's boundary leaves and re-enters the disc.
2613    let m = hull.len();
2614    let inside = |q: [f64; 2]| q[0].hypot(q[1]) <= radius;
2615    // Leave a tool that does not reach the disc, or wraps the axis.
2616    let mut nearest = f64::INFINITY;
2617    let mut wraps = true;
2618    for k in 0..m {
2619        let (a, b) = (hull[k], hull[(k + 1) % m]);
2620        let d = [b[0] - a[0], b[1] - a[1]];
2621        let t = (-(a[0] * d[0] + a[1] * d[1]) / (d[0] * d[0] + d[1] * d[1])).clamp(0.0, 1.0);
2622        nearest = nearest.min((a[0] + d[0] * t).hypot(a[1] + d[1] * t));
2623        if cross(a, b, [0.0, 0.0]) < 0.0 {
2624            wraps = false;
2625        }
2626    }
2627    if wraps || nearest >= radius || (0..m).all(|k| inside(hull[k])) {
2628        return Ok(None);
2629    }
2630    let mut leave: Vec<f64> = Vec::new();
2631    let mut enter: Vec<f64> = Vec::new();
2632    for k in 0..m {
2633        let (a, b) = (hull[k], hull[(k + 1) % m]);
2634        let d = [b[0] - a[0], b[1] - a[1]];
2635        let (qa, qb, qc) = (d[0] * d[0] + d[1] * d[1], 2.0 * (a[0] * d[0] + a[1] * d[1]), a[0] * a[0] + a[1] * a[1] - radius * radius);
2636        let disc = qb * qb - 4.0 * qa * qc;
2637        if disc <= 0.0 {
2638            continue;
2639        }
2640        for t in [(-qb - disc.sqrt()) / (2.0 * qa), (-qb + disc.sqrt()) / (2.0 * qa)] {
2641            if t > 0.0 && t < 1.0 {
2642                let q = [a[0] + d[0] * t, a[1] + d[1] * t];
2643                let outward = 2.0 * (q[0] * d[0] + q[1] * d[1]) > 0.0;
2644                (if outward { &mut leave } else { &mut enter }).push(q[1].atan2(q[0]));
2645            }
2646        }
2647    }
2648    let ([a], [b]) = (leave.as_slice(), enter.as_slice()) else {
2649        return Err(refuse("clip_trim", "the tool's shadow leaves the clip's disc more than once".into()));
2650    };
2651    let width = (b - a).rem_euclid(TAU);
2652    if width >= 0.9 * PI {
2653        return Err(refuse("clip_trim", "the tool's shadow spans too much of the clip's circle to be trimmed".into()));
2654    }
2655    // Planes of the tool, and the two tangents (outward normals in the plane).
2656    let mut planes: Vec<(Vec3, f64)> = Vec::new();
2657    let centroid = vertices.iter().fold(Vec3::new(0.0, 0.0, 0.0), |s, &p| s.add(p)).scale(1.0 / vertices.len() as f64);
2658    for face in faces {
2659        let (p, q, r) = (vertices[face[0]], vertices[face[1]], vertices[face[2]]);
2660        let mut n = frag(q.sub(p).cross(r.sub(p)).normalized(), "face_normal")?;
2661        if n.dot(p.sub(centroid)) < 0.0 {
2662            n = n.scale(-1.0);
2663        }
2664        planes.push((n, n.dot(p)));
2665    }
2666    for angle in [*a, *b] {
2667        let n = ex.scale(angle.cos()).add(ey.scale(angle.sin()));
2668        planes.push((n, n.dot(clip.axis_origin) + radius));
2669    }
2670    Ok(Some(polyhedron_of_planes(&planes)?))
2671}
2672
2673/// The convex polyhedron {n · p ≤ d for every plane}: its corners (where three
2674/// planes meet inside all) and a face per plane that bounds it.
2675fn polyhedron_of_planes(planes: &[(Vec3, f64)]) -> Result<(Vec<Vec3>, Vec<Vec<usize>>), KernelRefusal> {
2676    let k = planes.len();
2677    let mut corners: Vec<Vec3> = Vec::new();
2678    let scale = planes.iter().map(|p| p.1.abs()).fold(1.0f64, f64::max);
2679    for i in 0..k {
2680        for j in i + 1..k {
2681            for l in j + 1..k {
2682                let (a, b, c) = (planes[i].0, planes[j].0, planes[l].0);
2683                let det = a.dot(b.cross(c));
2684                if det.abs() < 1e-12 {
2685                    continue;
2686                }
2687                let p = b.cross(c).scale(planes[i].1).add(c.cross(a).scale(planes[j].1)).add(a.cross(b).scale(planes[l].1)).scale(1.0 / det);
2688                if planes.iter().all(|(n, d)| n.dot(p) - d <= 1e-9 * scale) && !corners.iter().any(|q| q.sub(p).length() <= 1e-9 * scale) {
2689                    corners.push(p);
2690                }
2691            }
2692        }
2693    }
2694    let mut faces = Vec::new();
2695    for (n, d) in planes {
2696        let on: Vec<usize> = (0..corners.len()).filter(|&c| (n.dot(corners[c]) - d).abs() <= 1e-9 * scale).collect();
2697        if on.len() < 3 {
2698            continue;
2699        }
2700        let middle = on.iter().fold(Vec3::new(0.0, 0.0, 0.0), |s, &c| s.add(corners[c])).scale(1.0 / on.len() as f64);
2701        let u = frag(corners[on[0]].sub(middle).normalized(), "face_axis")?;
2702        let v = n.cross(u);
2703        let mut ordered = on.clone();
2704        ordered.sort_by(|&p, &q| {
2705            let (rp, rq) = (corners[p].sub(middle), corners[q].sub(middle));
2706            rp.dot(v).atan2(rp.dot(u)).total_cmp(&rq.dot(v).atan2(rq.dot(u)))
2707        });
2708        faces.push(ordered);
2709    }
2710    Ok((corners, faces))
2711}
2712
2713fn turn_shadow(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>], clip: &crate::SweepClip, axis: Vec3, wedge: [f64; 2]) -> Result<BrepSolid, KernelRefusal> {
2714    let ez = axis;
2715    let ex = clip.reference.sub(ez.scale(clip.reference.dot(ez))).normalized().map_err(|_| input("clip_reference", "the clip's reference lies along its axis".into()))?;
2716    let ey = ez.cross(ex);
2717    // Turning by ω t about e = ±ez moves angles about ez by ±ω t.
2718    let rate = screw.omega * screw.e.dot(ez).signum();
2719    let local = |p: Vec3| {
2720        let r = p.sub(clip.axis_origin);
2721        [r.dot(ex), r.dot(ey), r.dot(ez)]
2722    };
2723    let corners: Vec<[f64; 3]> = vertices.iter().map(|&p| local(p)).collect();
2724    // The tool's angular extent about the axis (its corners'; the tool must
2725    // keep off the axis, so its shadow on the plane misses the origin).
2726    let mid = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
2727    let mid_angle = mid[1].atan2(mid[0]);
2728    let angles: Vec<f64> = corners.iter().map(|c| mid_angle + (c[1].atan2(c[0]) - mid_angle + PI).rem_euclid(TAU) - PI).collect();
2729    let (low, high) = (angles.iter().copied().fold(f64::INFINITY, f64::min), angles.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2730    if high - low >= PI {
2731        return Err(refuse("turn_shadow", "the tool reaches round the turn's axis".into()));
2732    }
2733    let (start, end) = (rate * screw.t0, rate * screw.t1);
2734    let (first, last) = if rate > 0.0 { (start, end) } else { (end, start) };
2735    let reference = 0.5 * (wedge[0] + wedge[1]);
2736    let near = |a: f64| reference + (a - reference + PI).rem_euclid(TAU) - PI;
2737    // Before the wedge at one end, after it at the other (angles taken near the wedge).
2738    let clear = near(high + first) < wedge[0] - 1e-9 && near(low + last) > wedge[1] + 1e-9 && (last - first) < TAU;
2739    if !clear {
2740        return Err(refuse("turn_shadow", "the turned tool's ends reach the clip's wedge; its sweep there is not its turned shadow".into()));
2741    }
2742    let profile = polyhedron_shadow(&corners, faces, false)?;
2743    // The profile in the half-plane at the wedge's start angle.
2744    let (c0, s0) = (wedge[0].cos(), wedge[0].sin());
2745    let world = |rho: f64, z: f64| clip.axis_origin.add(ez.scale(z)).add(ex.scale(rho * c0)).add(ey.scale(rho * s0));
2746    let mut curves = Vec::with_capacity(profile.len());
2747    for piece in &profile {
2748        let points: Vec<Vec4> = piece.iter().map(|&(rho, z, w)| Vec4::from_point(world(rho, z), w)).collect();
2749        let curve = if points.len() == 2 {
2750            frag(make_line(frag(points[0].point(), "point")?, frag(points[1].point(), "point")?), "line")?
2751        } else {
2752            frag(NurbsCurve::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points), "conic")?
2753        };
2754        curves.push(curve);
2755    }
2756    frag(crate::revolve_profile_brep(&curves, clip.axis_origin, ez, wedge[1] - wedge[0]), "turn_shadow")
2757}
2758
2759/// A convex polyhedron turned a whole turn (no advance): the solid of
2760/// revolution of its shadow {(ρ, z)} about the turn's axis, which may pass
2761/// through it. Its corners' paths bound it only where an edge is farthest
2762/// from the axis; an edge skew to the axis turns a hyperboloid.
2763fn whole_turn_shadow(screw: &Screw, vertices: &[Vec3], faces: &[Vec<usize>]) -> Result<BrepSolid, KernelRefusal> {
2764    let ez = screw.e;
2765    let ex = frag(ez.perpendicular(), "turn_shadow")?;
2766    let ex = frag(ex.sub(ez.scale(ex.dot(ez))).normalized(), "turn_shadow")?;
2767    let ey = ez.cross(ex);
2768    let corners: Vec<[f64; 3]> = vertices
2769        .iter()
2770        .map(|&p| {
2771            let r = screw.apply(screw.t0, p).sub(screw.a);
2772            [r.dot(ex), r.dot(ey), r.dot(ez)]
2773        })
2774        .collect();
2775    let profile = polyhedron_shadow(&corners, faces, true)?;
2776    // A point of the outline on the axis to round-off is on it.
2777    let reach = corners.iter().map(|c| c[0].hypot(c[1]).max(c[2].abs())).fold(0.0f64, f64::max);
2778    let world = |rho: f64, z: f64| screw.a.add(ez.scale(z)).add(ex.scale(if rho.abs() <= 1e-10 * reach { 0.0 } else { rho }));
2779    let mut curves = Vec::with_capacity(profile.len());
2780    // The outline's stretch along the axis, as one edge (it comes a piece per
2781    // corner height).
2782    let on_axis = |piece: &ShadowPiece| piece.iter().all(|&(rho, _, _)| rho.abs() <= 1e-10 * reach);
2783    let mut along: Option<(f64, f64)> = None;
2784    for piece in &profile {
2785        if on_axis(piece) {
2786            let (za, zb) = (piece[0].1, piece[piece.len() - 1].1);
2787            along = Some(match along {
2788                Some((from, _)) => (from, zb),
2789                None => (za, zb),
2790            });
2791            continue;
2792        }
2793        if let Some((from, to)) = along.take() {
2794            curves.push(frag(make_line(world(0.0, from), world(0.0, to)), "line")?);
2795        }
2796        let points: Vec<Vec4> = piece.iter().map(|&(rho, z, w)| Vec4::from_point(world(rho, z), w)).collect();
2797        let (first, last) = (frag(points[0].point(), "point")?, frag(points[points.len() - 1].point(), "point")?);
2798        if first.sub(last).length() <= 1e-12 * (1.0 + first.length()) {
2799            continue;
2800        }
2801        let curve = if points.len() == 2 { frag(make_line(first, last), "line")? } else { frag(NurbsCurve::new(2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points), "conic")? };
2802        curves.push(curve);
2803    }
2804    if let Some((from, to)) = along.take() {
2805        curves.push(frag(make_line(world(0.0, from), world(0.0, to)), "line")?);
2806    }
2807    frag(crate::revolve_profile_brep(&curves, screw.a, ez, TAU), "turn_shadow")
2808}
2809
2810/// One piece of the shadow's outline: a segment (two points) or a rational
2811/// quadratic arc (three, the middle with its weight), as (ρ, z, weight).
2812type ShadowPiece = Vec<(f64, f64, f64)>;
2813
2814/// The shadow {(ρ, z)} of a convex polyhedron (corners in the axis frame):
2815/// at each z its section is a convex polygon, the ρ it reaches an interval
2816/// from the section's nearest point to its farthest corner. The farthest is
2817/// always on an edge (ρ² = quadratic in z: a conic arc); the nearest is on a
2818/// face (ρ linear in z: a segment) or an edge.
2819///
2820/// `through`: the axis may pass through the tool (its nearest ρ is then 0 and
2821/// the outline runs along the axis there); refused otherwise.
2822fn polyhedron_shadow(corners: &[[f64; 3]], faces: &[Vec<usize>], through: bool) -> Result<Vec<ShadowPiece>, KernelRefusal> {
2823    let zs: Vec<f64> = corners.iter().map(|c| c[2]).collect();
2824    let (z_low, z_high) = (zs.iter().copied().fold(f64::INFINITY, f64::min), zs.iter().copied().fold(f64::NEG_INFINITY, f64::max));
2825    let scale = corners.iter().map(|c| c[0].hypot(c[1]).max(c[2].abs())).fold(1.0f64, f64::max);
2826    // The hull's edges (from the faces' loops) and the faces beside each.
2827    let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
2828    for (f, face) in faces.iter().enumerate() {
2829        for k in 0..face.len() {
2830            let (i, j) = (face[k], face[(k + 1) % face.len()]);
2831            edge_faces.entry((i.min(j), i.max(j))).or_default().push(f);
2832        }
2833    }
2834    let mut edges: Vec<(usize, usize)> = edge_faces.keys().copied().collect();
2835    edges.sort_unstable();
2836    // Each face's plane in the axis frame: n · p = d.
2837    let planes: Vec<([f64; 3], f64)> = faces
2838        .iter()
2839        .map(|face| {
2840            let (a, b, c) = (corners[face[0]], corners[face[1]], corners[face[2]]);
2841            let (u, v) = ([b[0] - a[0], b[1] - a[1], b[2] - a[2]], [c[0] - a[0], c[1] - a[1], c[2] - a[2]]);
2842            let n = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
2843            (n, n[0] * a[0] + n[1] * a[1] + n[2] * a[2])
2844        })
2845        .collect();
2846    #[derive(Clone, Copy, PartialEq, Debug)]
2847    enum Source {
2848        /// The crossing of edge k.
2849        Edge(usize),
2850        /// The nearest point of face f's section line.
2851        Face(usize),
2852        /// The axis itself, where it passes through the tool.
2853        Axis,
2854    }
2855    let edge_at = |k: usize, z: f64| -> [f64; 2] {
2856        let (a, b) = (corners[edges[k].0], corners[edges[k].1]);
2857        let t = (z - a[2]) / (b[2] - a[2]);
2858        [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
2859    };
2860    let crossing = |z: f64| -> Vec<usize> {
2861        (0..edges.len()).filter(|&k| (corners[edges[k].0][2] - z) * (corners[edges[k].1][2] - z) < 0.0).collect()
2862    };
2863    let farthest = |z: f64| -> (Source, f64) {
2864        let mut best = (Source::Edge(0), f64::NEG_INFINITY);
2865        for k in crossing(z) {
2866            let p = edge_at(k, z);
2867            let r = p[0].hypot(p[1]);
2868            if r > best.1 {
2869                best = (Source::Edge(k), r);
2870            }
2871        }
2872        best
2873    };
2874    // The section polygon: edge crossings ordered by angle about their mean.
2875    let nearest = |z: f64| -> Result<(Source, f64), KernelRefusal> {
2876        let list = crossing(z);
2877        let points: Vec<[f64; 2]> = list.iter().map(|&k| edge_at(k, z)).collect();
2878        let m = points.len();
2879        if m < 3 {
2880            return Err(refuse("turn_shadow", "a section of the tool is not a polygon".into()));
2881        }
2882        let centre = points.iter().fold([0.0, 0.0], |s, p| [s[0] + p[0] / m as f64, s[1] + p[1] / m as f64]);
2883        let mut order: Vec<usize> = (0..m).collect();
2884        order.sort_by(|&a, &b| {
2885            let (pa, pb) = (points[a], points[b]);
2886            (pa[1] - centre[1]).atan2(pa[0] - centre[0]).total_cmp(&(pb[1] - centre[1]).atan2(pb[0] - centre[0]))
2887        });
2888        let mut inside = true;
2889        let mut best = (Source::Edge(0), f64::INFINITY);
2890        for k in 0..m {
2891            let (ia, ib) = (order[k], order[(k + 1) % m]);
2892            let (a, b) = (points[ia], points[ib]);
2893            let d = [b[0] - a[0], b[1] - a[1]];
2894            if d[0] * (0.0 - a[1]) - d[1] * (0.0 - a[0]) < 0.0 {
2895                inside = false;
2896            }
2897            let length2 = d[0] * d[0] + d[1] * d[1];
2898            let t = -(a[0] * d[0] + a[1] * d[1]) / length2;
2899            let (source, r) = if t > 0.0 && t < 1.0 {
2900                let (ea, eb) = (edges[list[ia]], edges[list[ib]]);
2901                let shared = edge_faces[&ea].iter().find(|f| edge_faces[&eb].contains(f)).copied();
2902                let Some(face) = shared else {
2903                    return Err(refuse("turn_shadow", "a section side does not lie on one face".into()));
2904                };
2905                let q = [a[0] + d[0] * t, a[1] + d[1] * t];
2906                (Source::Face(face), q[0].hypot(q[1]))
2907            } else {
2908                let q = if t <= 0.0 { a } else { b };
2909                (Source::Edge(list[if t <= 0.0 { ia } else { ib }]), q[0].hypot(q[1]))
2910            };
2911            if r < best.1 {
2912                best = (source, r);
2913            }
2914        }
2915        if inside {
2916            if !through {
2917                return Err(refuse("turn_shadow", "the turn's axis passes through the tool".into()));
2918            }
2919            return Ok((Source::Axis, 0.0));
2920        }
2921        Ok(best)
2922    };
2923    // ρ of a source at z, from its own geometry (smooth across the piece).
2924    let value = |source: Source, z: f64| -> f64 {
2925        match source {
2926            Source::Edge(k) => {
2927                let p = edge_at(k, z);
2928                p[0].hypot(p[1])
2929            }
2930            Source::Face(f) => {
2931                let (n, d) = planes[f];
2932                (d - n[2] * z).abs() / n[0].hypot(n[1])
2933            }
2934            Source::Axis => 0.0,
2935        }
2936    };
2937    // Breaks: corner heights, and where the farthest or nearest source changes.
2938    let mut heights: Vec<f64> = zs.clone();
2939    heights.sort_by(|a, b| a.total_cmp(b));
2940    heights.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * scale);
2941    let mut pieces_far: Vec<(f64, f64, Source)> = Vec::new();
2942    let mut pieces_near: Vec<(f64, f64, Source)> = Vec::new();
2943    for window in heights.windows(2) {
2944        let (za, zb) = (window[0], window[1]);
2945        let samples = 400;
2946        let inner = |k: usize| za + (zb - za) * (k as f64 + 0.5) / samples as f64;
2947        for near in [false, true] {
2948            let source_at = |z: f64| -> Result<Source, KernelRefusal> { Ok(if near { nearest(z)?.0 } else { farthest(z).0 }) };
2949            let mut runs: Vec<(f64, Source)> = vec![(za, source_at(inner(0))?)];
2950            for k in 1..samples {
2951                let (z0, z1) = (inner(k - 1), inner(k));
2952                let (s0, s1) = (source_at(z0)?, source_at(z1)?);
2953                // Two sources tied over the stretch (edges equally far from
2954                // the axis) are one piece of the outline.
2955                let tied = [z0, 0.5 * (z0 + z1), z1].iter().all(|&z| (value(s0, z) - value(s1, z)).abs() <= 1e-12 * scale);
2956                if s0 == s1 || tied {
2957                    continue;
2958                }
2959                // Where the source changes, by bisection (a side handing over
2960                // to its corner touches it, so their values do not cross).
2961                let (mut lo, mut hi) = (z0, z1);
2962                for _ in 0..200 {
2963                    let midz = 0.5 * (lo + hi);
2964                    if source_at(midz)? == s0 { lo = midz } else { hi = midz }
2965                }
2966                runs.push((0.5 * (lo + hi), s1));
2967            }
2968            let list = if near { &mut pieces_near } else { &mut pieces_far };
2969            for k in 0..runs.len() {
2970                let z_end = if k + 1 < runs.len() { runs[k + 1].0 } else { zb };
2971                list.push((runs[k].0, z_end, runs[k].1));
2972            }
2973        }
2974    }
2975    // Exact pieces: a pair's ρ(z) is a conic arc, a side's a segment.
2976    let piece = |source: Source, za: f64, zb: f64| -> Vec<ShadowPiece> {
2977        let (ra, rb) = (value(source, za), value(source, zb));
2978        if matches!(source, Source::Face(..) | Source::Axis) {
2979            return vec![vec![(ra, za, 1.0), (rb, zb, 1.0)]];
2980        }
2981        // ρ² = α z² + β z + γ; split so each arc turns little.
2982        let rho = |z: f64| value(source, z);
2983        // Differences kept inside the piece (an edge through the axis has a
2984        // corner in ρ at its end there).
2985        let (z_min, z_max) = (za.min(zb), za.max(zb));
2986        let slope = |z: f64| {
2987            let h = 1e-6 * (zb - za).abs().max(1e-300);
2988            let (zp, zm) = ((z + h).min(z_max), (z - h).max(z_min));
2989            (rho(zp) - rho(zm)) / (zp - zm)
2990        };
2991        let parts = 4;
2992        let mut out = Vec::new();
2993        for k in 0..parts {
2994            let (z0, z1) = (za + (zb - za) * k as f64 / parts as f64, za + (zb - za) * (k + 1) as f64 / parts as f64);
2995            let (a, b) = ((rho(z0), z0), (rho(z1), z1));
2996            // Straight (an edge meeting the axis): a segment.
2997            let bend = [0.25, 0.5, 0.75].iter().map(|&t| (rho(z0 + (z1 - z0) * t) - (a.0 + (b.0 - a.0) * t)).abs()).fold(0.0f64, f64::max);
2998            if bend <= 1e-12 * scale {
2999                out.push(vec![(a.0, a.1, 1.0), (b.0, b.1, 1.0)]);
3000                continue;
3001            }
3002            let (ta, tb) = ((slope(z0), 1.0), (slope(z1), 1.0));
3003            // Tangent lines' intersection.
3004            let det = ta.0 * (-tb.1) - ta.1 * (-tb.0);
3005            if det.abs() < 1e-12 {
3006                out.push(vec![(a.0, a.1, 1.0), (b.0, b.1, 1.0)]);
3007                continue;
3008            }
3009            let rhs = (b.0 - a.0, b.1 - a.1);
3010            let s = (rhs.0 * (-tb.1) - rhs.1 * (-tb.0)) / det;
3011            let c = (a.0 + ta.0 * s, a.1 + ta.1 * s);
3012            // Shoulder: the conic's point on the line from the chord's middle to c.
3013            let m = (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
3014            let on = |u: f64| {
3015                let p = (m.0 + (c.0 - m.0) * u, m.1 + (c.1 - m.1) * u);
3016                p.0 - rho(p.1)
3017            };
3018            let (mut lo, mut hi) = (0.0, 1.0);
3019            let sign_lo = on(lo);
3020            for _ in 0..200 {
3021                let mid = 0.5 * (lo + hi);
3022                if (on(mid) > 0.0) == (sign_lo > 0.0) { lo = mid } else { hi = mid }
3023            }
3024            let u = 0.5 * (lo + hi);
3025            let weight = u / (1.0 - u);
3026            out.push(vec![(a.0, a.1, 1.0), (c.0, c.1, weight), (b.0, b.1, 1.0)]);
3027        }
3028        out
3029    };
3030    // Outline: up the near side, across the top, down the far side, across the bottom.
3031    let mut outline: Vec<ShadowPiece> = Vec::new();
3032    for &(za, zb, source) in &pieces_near {
3033        outline.extend(piece(source, za, zb));
3034    }
3035    let (near_top, far_top) = (value(pieces_near.last().expect("near").2, z_high), value(pieces_far.last().expect("far").2, z_high));
3036    if far_top - near_top > 1e-12 * scale {
3037        outline.push(vec![(near_top, z_high, 1.0), (far_top, z_high, 1.0)]);
3038    }
3039    for &(za, zb, source) in pieces_far.iter().rev() {
3040        for mut arc in piece(source, za, zb).into_iter().rev() {
3041            arc.reverse();
3042            outline.push(arc);
3043        }
3044    }
3045    let (near_bottom, far_bottom) = (value(pieces_near[0].2, z_low), value(pieces_far[0].2, z_low));
3046    if far_bottom - near_bottom > 1e-12 * scale {
3047        outline.push(vec![(far_bottom, z_low, 1.0), (near_bottom, z_low, 1.0)]);
3048    }
3049    // Snap consecutive ends together.
3050    for k in 0..outline.len() {
3051        let next = (k + 1) % outline.len();
3052        let end = *outline[k].last().expect("piece");
3053        outline[next][0] = (end.0, end.1, 1.0);
3054    }
3055    Ok(outline)
3056}
3057
3058/// A ball TURNED about an axis that passes through it. Its grazing circle (in
3059/// the plane of the axis and the centre) crosses the axis at two poles N and S;
3060/// the part beyond the axis turns inside the sweep, and near the axis the ends
3061/// overlap. So the sweep has three faces: the wall, the outer arc of that
3062/// circle turned (a spindle torus's outer part, pinched at N and S); the
3063/// ball's back at the start less the ball at the end; and its front at the end
3064/// less the ball at the start. The two caps meet along the far arc, from S to
3065/// N, of the circle where the two spheres meet. None when the motion or tool
3066/// is not of that kind.
3067fn ball_across_axis(screw: &Screw, origin: Vec3, axis: Vec3, meridian: &[OutlinePiece]) -> Result<Option<BrepSolid>, KernelRefusal> {
3068    let [OutlinePiece::Arc { centre, radius, start, end }, OutlinePiece::Line { .. }] = meridian else { return Ok(None) };
3069    let (centre, r) = (*centre, *radius);
3070    if centre[0].abs() > 1e-12 * r || ((end - start).abs() - PI).abs() > 1e-12 || screw.c != 0.0 || screw.c2 != 0.0 || screw.omega == 0.0 {
3071        return Ok(None);
3072    }
3073    let w = axis.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
3074    let centre_tool = origin.add(w.scale(centre[1]));
3075    let turned = screw.omega * (screw.t1 - screw.t0);
3076    if turned.abs() >= TAU - 1e-9 {
3077        return Ok(None);
3078    }
3079    let e = screw.e.scale(screw.omega.signum()); // the sense the ball turns about
3080    let c0 = screw.apply(screw.t0, centre_tool);
3081    let c1 = screw.apply(screw.t1, centre_tool);
3082    let foot = |c: Vec3| screw.a.add(e.scale(c.sub(screw.a).dot(e)));
3083    let rho = c0.sub(foot(c0)).length();
3084    if rho >= r * (1.0 - 1e-9) || rho <= 1e-9 * r {
3085        return Ok(None);
3086    }
3087    let h0 = frag(c0.sub(foot(c0)).normalized(), "radial")?;
3088    let height = (r * r - rho * rho).sqrt();
3089    let (north, south) = (foot(c0).add(e.scale(height)), foot(c0).sub(e.scale(height)));
3090    // An arc from `from` turned about `axis` through `centre` by `angle`.
3091    let arc = |about: Vec3, k: Vec3, from: Vec3, angle: f64| -> Result<NurbsCurve, KernelRefusal> {
3092        let surface = frag(make_revolution(about, k, &frag(make_line(from, from.add(k)), "line")?, angle), "arc")?;
3093        frag(surface.iso_curve_v(0.0), "arc")
3094    };
3095    // The outer arc of the grazing circle, S -> N through c0 + r h0: in the
3096    // (h0, e) plane about c0, from angle -α to α through 0, α = atan2(height, -ρ).
3097    let alpha = height.atan2(-rho);
3098    let outer = arc(c0, h0.cross(e), south, 2.0 * alpha)?;
3099    let wall = frag(make_revolution(screw.a, e, &outer, turned.abs()), "wall")?;
3100    let outer_end = frag(wall.iso_curve_u(1.0), "wall_end")?;
3101    // The far arc where the spheres meet, S -> N beyond the axis.
3102    let mid = c0.add(c1).scale(0.5);
3103    let towards = frag(mid.sub(foot(mid)).normalized(), "bisector")?;
3104    let rho_mid = mid.sub(foot(mid)).length();
3105    let beta = height.atan2(rho_mid); // S and N seen from mid, off the far direction
3106    // From S at angle β − π (in the (towards, e) plane about mid) down through
3107    // π to N at π − β: the decreasing sense, about e × towards.
3108    let meet = arc(mid, e.cross(towards), south, 2.0 * beta)?;
3109    {
3110        // The arc must pass beyond the axis (its middle on the far side).
3111        let [d0, d1] = frag(meet.domain(), "domain")?;
3112        let middle = frag(meet.evaluate(0.5 * (d0 + d1)), "middle")?;
3113        if middle.sub(foot(middle)).dot(towards) > 0.0 {
3114            return Err(refuse("ball_across_axis", "the spheres' common circle runs the wrong way round".into()));
3115        }
3116    }
3117    // A trimmed sphere cap: a chart whose poles and seam lie where the cap is not.
3118    let cap = |c: Vec3, inward: Vec3, side: Vec3, other: Vec3| -> Result<(NurbsSurface, Box<dyn Fn(Vec3) -> Vec3>), KernelRefusal> {
3119        // South pole just off the far end of the grazing circle, on the removed
3120        // side (inside the other end's ball); the seam crosses the grazing
3121        // circle beyond the axis only.
3122        let toward_south = frag(inward.add(side.scale(-0.3)).normalized(), "pole")?;
3123        let polar = toward_south.scale(-1.0);
3124        let seam = frag(side.sub(polar.scale(side.dot(polar))).normalized(), "seam")?;
3125        for k in 0..=64 {
3126            // The seam's part on the cap's side of the grazing circle must lie
3127            // inside the other ball (where the cap is not).
3128            let a = PI * k as f64 / 64.0;
3129            let q = c.add(polar.scale(-r * a.cos())).add(seam.scale(r * a.sin()));
3130            if q.sub(c).dot(side) < 0.0 && q.sub(other).length() >= r * (1.0 - 1e-9) {
3131                return Err(refuse("ball_across_axis", "no seam place for a cap".into()));
3132            }
3133        }
3134        let south_pole = c.sub(polar.scale(r));
3135        if south_pole.sub(other).length() >= r * (1.0 - 1e-6) {
3136            return Err(refuse("ball_across_axis", "no pole place for a cap".into()));
3137        }
3138        let generatrix = arc(c, polar.scale(-1.0).cross(seam), south_pole, PI)?;
3139        let surface = frag(make_revolution(c, polar, &generatrix, TAU), "cap")?;
3140        let spans_u = (surface.knots_u.len() - 6) / 2 + 1;
3141        let spans_v = (generatrix.knots.len() - 6) / 2 + 1;
3142        let around = polar.cross(seam);
3143        let inverse = move |x: Vec3| -> Vec3 {
3144            let d = x.sub(c).scale(1.0 / r);
3145            let latitude = d.dot(polar).clamp(-1.0, 1.0).asin();
3146            let longitude = d.dot(around).atan2(d.dot(seam)).rem_euclid(TAU);
3147            Vec3::new(
3148                crate::circle_angle_to_parameter(spans_u, TAU, longitude),
3149                crate::circle_angle_to_parameter(spans_v, PI, latitude + 0.5 * PI),
3150                0.0,
3151            )
3152        };
3153        Ok((surface, Box::new(inverse)))
3154    };
3155    let moving0 = e.cross(h0);
3156    let h1 = frag(c1.sub(foot(c1)).normalized(), "radial")?;
3157    let moving1 = e.cross(h1);
3158    // The back cap at the start (its removed part near c0 − r h0 on the back);
3159    // the front cap at the end.
3160    let (back, back_inverse) = cap(c0, h0.scale(-1.0), moving0.scale(1.0), c1)?;
3161    let (front, front_inverse) = cap(c1, h1.scale(-1.0), moving1.scale(-1.0), c0)?;
3162    let pcurve = |inverse: &dyn Fn(Vec3) -> Vec3, curve: &NurbsCurve, forward: bool| -> Result<NurbsCurve, KernelRefusal> {
3163        let [d0, d1] = frag(curve.domain(), "domain")?;
3164        let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3165        let uv: Vec<Vec3> = ts.iter().map(|&t| curve.evaluate(t).map(|x| inverse(x))).collect::<Result<_, _>>().map_err(|e| refuse("pcurve", e))?;
3166        let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "cap_pcurve")?;
3167        if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3168    };
3169    let mut shell = Shell::new();
3170    let (vn, vs) = (shell.vertex(north), shell.vertex(south));
3171    let k0 = shell.edge(outer.clone(), vs, vn, false)?;
3172    let k1 = shell.edge(outer_end.clone(), vs, vn, false)?;
3173    let c = shell.edge(meet.clone(), vs, vn, false)?;
3174    let ds = shell.edge(frag(make_line(south, south), "point")?, vs, vs, true)?;
3175    let dn = shell.edge(frag(make_line(north, north), "point")?, vn, vn, true)?;
3176    // Wall: u turns, v runs S -> N; outward needs v the other way round the
3177    // circle's normal, so test and pick the loop's sense.
3178    let wall_outward = {
3179        let p = frag(wall.evaluate(0.5, 0.5), "probe")?;
3180        let su = frag(wall.evaluate(0.5 + 1e-6, 0.5), "probe")?.sub(frag(wall.evaluate(0.5 - 1e-6, 0.5), "probe")?);
3181        let sv = frag(wall.evaluate(0.5, 0.5 + 1e-6), "probe")?.sub(frag(wall.evaluate(0.5, 0.5 - 1e-6), "probe")?);
3182        su.cross(sv).dot(p.sub(foot(p))) > 0.0
3183    };
3184    let [g0, g1] = frag(outer.domain(), "domain")?;
3185    let wall_loop = vec![
3186        (ds, true, uv_line([0.0, g0], [1.0, g0])?),
3187        (k1, true, uv_line([1.0, g0], [1.0, g1])?),
3188        (dn, true, uv_line([1.0, g1], [0.0, g1])?),
3189        (k0, false, uv_line([0.0, g1], [0.0, g0])?),
3190    ];
3191    let reversed = |list: Vec<(u64, bool, NurbsCurve)>| -> Result<Vec<(u64, bool, NurbsCurve)>, KernelRefusal> {
3192        list.into_iter().rev().map(|(e, f, p)| Ok((e, !f, frag(p.reversed(), "reversed")?))).collect()
3193    };
3194    shell.face(wall.clone(), if wall_outward { wall_loop } else { reversed(wall_loop)? });
3195    // Caps: loop (grazing arc S -> N, then the meeting arc N -> S), sense by
3196    // the signed area of its parameters (the charts' u × v points outward).
3197    for (surface, inverse, k_edge, k_curve) in [(back, &back_inverse, k0, &outer), (front, &front_inverse, k1, &outer_end)] {
3198        let mut lp = vec![(k_edge, true, pcurve(inverse.as_ref(), k_curve, true)?), (c, false, pcurve(inverse.as_ref(), &meet, false)?)];
3199        let mut area = 0.0;
3200        for (_, _, pc) in &lp {
3201            let [d0, d1] = frag(pc.domain(), "domain")?;
3202            for j in 0..64 {
3203                let (a, b) = (frag(pc.evaluate(d0 + (d1 - d0) * j as f64 / 64.0), "uv")?, frag(pc.evaluate(d0 + (d1 - d0) * (j + 1) as f64 / 64.0), "uv")?);
3204                area += a.x * b.y - b.x * a.y;
3205            }
3206        }
3207        if area < 0.0 {
3208            lp = reversed(lp)?;
3209        }
3210        shell.face(surface, lp);
3211    }
3212    let body = shell.solid();
3213    let issues = body.validate();
3214    if !issues.is_empty() {
3215        return Err(refuse("assembly", format!("the turned ball's sweep is not valid: {:?}", issues.first())));
3216    }
3217    let probe = Split { points: Vec::new(), curves: Vec::new(), caps: Vec::new(), slides: Vec::new(), slide_sides: Vec::new(), scale: r + rho, shape: Shape::Revolved { origin, axis: w, meridian: meridian.to_vec() } };
3218    check_single_pass(screw, &probe, &body, &std::collections::HashSet::new())?;
3219    Ok(Some(body))
3220}
3221
3222/// A frame riding a cubic Bézier curve (`control`, in a plane square to
3223/// `up`): its origin on the curve, its first axis along the curve's tangent,
3224/// `up` fixed. The tool is given in the frame at the curve's start and carried
3225/// by it over the whole curve.
3226#[derive(Clone, Copy, Debug)]
3227pub struct FrameCurve {
3228    pub control: [Vec3; 4],
3229    pub up: Vec3,
3230}
3231
3232/// The whole sweep of a solid of revolution carried by a frame along a
3233/// curve. In the riding frame the velocity field is the speed along the
3234/// tangent plus the turn about `up`, so the grazing loop moves on the tool with
3235/// the curve's curvature. It is split at sample times (its structure must not
3236/// change), each piece sampled exactly, and the walls lofted through those
3237/// samples (quintic both ways; the samples' count doubles until the wall is
3238/// within 1e-10 of the exact loop at the times between). Caps are the tool's
3239/// back at the start and front at the end.
3240pub fn sweep_solid_along(tool: &SolidTool, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
3241    let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
3242    let base = path.control[0];
3243    if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
3244        return Err(input("plane", "the curve is not in a plane square to up".into()));
3245    }
3246    let tangent = path.control[1].sub(base);
3247    let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
3248    let start_tangent = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
3249    let frame = Frame { control: path.control, up, start_tangent };
3250    let (mut speed, mut rate, mut extent) = (0.0f64, 0.0f64, 0.0f64);
3251    let mut previous = 0.0;
3252    for k in 0..=512 {
3253        let t = k as f64 / 512.0;
3254        let first = frame.first(t);
3255        if first.length() <= 1e-12 {
3256            return Err(input("tangent", "the curve stops (zero tangent)".into()));
3257        }
3258        speed = speed.max(first.length());
3259        rate = rate.max(frame.angle_rate(t).abs());
3260        extent = extent.max(frame.point(t).sub(base).length());
3261        let angle = frame.angle(t);
3262        if (angle - previous).abs() > 0.5 {
3263            return Err(refuse("frame_turns", "the frame turns half way round".into()));
3264        }
3265        previous = angle;
3266    }
3267    let screw = Screw { a: base, e: up, omega: rate, c: speed, c2: 0.0, t0: 0.0, t1: 1.0, frame: Some(frame), turns: None, frozen: None,
3268        loop_samples: LOFT_SAMPLES, extent };
3269    let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool else {
3270        return Err(refuse("along_tool", "a frame along a curve carries a solid of revolution only".into()));
3271    };
3272    let axis = axis_direction.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
3273    // The splits name their pieces by angle about the tool's axis from its
3274    // radial direction: a grazing angle passing that reference renames them,
3275    // so try a few references.
3276    let mut last = None;
3277    for k in 0..8 {
3278        let reference = radial.rotate_about(axis, PI * k as f64 / 4.0);
3279        match lofted(&screw, *axis_origin, axis, reference, meridian) {
3280            Err(error) if refused_as(&error) == Some("loft_structure") => last = Some(error),
3281            other => return other,
3282        }
3283    }
3284    Err(last.expect("tried"))
3285}
3286
3287/// The whole sweep of a prism carried by a frame along a planar curve (its
3288/// axis along the frame's up): the prism `outline` (a convex polygon in the
3289/// start frame: first coordinate along the start tangent, second along
3290/// up × tangent) between `from` and `to` along up. The motion is planar, so the
3291/// sweep is the plane region the moving outline covers, extruded. That region
3292/// is found exactly: its boundary lies on the outline's corner paths, the paths
3293/// of each edge's grazing point (where the edge's normal is square to the
3294/// velocity), and the outline's edges at the two ends; these are split where
3295/// they meet, and a piece is kept where one side of it is covered and the other
3296/// not (folds, where a corner digs past a side, come out right).
3297pub fn sweep_prism_along(outline: &[[f64; 2]], from: f64, to: f64, path: &FrameCurve) -> Result<BrepSolid, KernelRefusal> {
3298    let up = path.up.normalized().map_err(|_| input("up", "the frame's up direction is zero".into()))?;
3299    let base = path.control[0];
3300    if path.control.iter().any(|p| p.sub(base).dot(up).abs() > 1e-9 * (1.0 + p.sub(base).length())) {
3301        return Err(input("plane", "the curve is not in a plane square to up".into()));
3302    }
3303    let tangent = path.control[1].sub(base);
3304    let tangent = if tangent.length() > 0.0 { tangent } else { path.control[2].sub(base) };
3305    let e1 = tangent.normalized().map_err(|_| input("tangent", "the curve has no start direction".into()))?;
3306    let e2 = up.cross(e1);
3307    let frame = Frame { control: path.control, up, start_tangent: e1 };
3308    if outline.len() < 3 || !(to > from) {
3309        return Err(input("prism", "a prism needs three corners and a positive height".into()));
3310    }
3311    // Counter-clockwise, convex.
3312    let n = outline.len();
3313    let mut corners = outline.to_vec();
3314    let area: f64 = (0..n).map(|k| corners[k][0] * corners[(k + 1) % n][1] - corners[(k + 1) % n][0] * corners[k][1]).sum();
3315    if area < 0.0 {
3316        corners.reverse();
3317    }
3318    for k in 0..n {
3319        let (a, b, c) = (corners[k], corners[(k + 1) % n], corners[(k + 2) % n]);
3320        if (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]) <= 0.0 {
3321            return Err(input("prism", "the outline is not convex".into()));
3322        }
3323    }
3324    let scale = corners.iter().map(|c| c[0].hypot(c[1])).fold(1.0f64, f64::max);
3325    // The frame in plane coordinates: origin c(s), turned by φ(s).
3326    let flat = |p: Vec3| [p.sub(base).dot(e1), p.sub(base).dot(e2)];
3327    let place = |s: f64, q: [f64; 2]| -> [f64; 2] {
3328        let o = flat(frame.point(s));
3329        let (sn, cs) = frame.angle(s).sin_cos();
3330        [o[0] + cs * q[0] - sn * q[1], o[1] + sn * q[0] + cs * q[1]]
3331    };
3332    let local = |s: f64, p: [f64; 2]| -> [f64; 2] {
3333        let o = flat(frame.point(s));
3334        let (sn, cs) = frame.angle(s).sin_cos();
3335        let d = [p[0] - o[0], p[1] - o[1]];
3336        [cs * d[0] + sn * d[1], -sn * d[0] + cs * d[1]]
3337    };
3338    // Edges: outward normals and offsets in the start frame.
3339    let edges: Vec<([f64; 2], f64)> = (0..n)
3340        .map(|k| {
3341            let (a, b) = (corners[k], corners[(k + 1) % n]);
3342            let d = [b[0] - a[0], b[1] - a[1]];
3343            let length = d[0].hypot(d[1]);
3344            let normal = [d[1] / length, -d[0] / length];
3345            (normal, normal[0] * a[0] + normal[1] * a[1])
3346        })
3347        .collect();
3348    let inside = |q: [f64; 2]| edges.iter().map(|(nn, d)| nn[0] * q[0] + nn[1] * q[1] - d).fold(f64::NEG_INFINITY, f64::max);
3349    // Is p covered at some time? The least signed distance over the motion.
3350    let depth = |p: [f64; 2]| -> f64 {
3351        let samples = 2000;
3352        let f = |s: f64| inside(local(s, p));
3353        let mut best = (f64::INFINITY, 0usize);
3354        let values: Vec<f64> = (0..=samples).map(|k| f(k as f64 / samples as f64)).collect();
3355        for (k, &v) in values.iter().enumerate() {
3356            if v < best.0 {
3357                best = (v, k);
3358            }
3359        }
3360        // Refine every local minimum near the least.
3361        let mut least = best.0;
3362        for k in 0..=samples {
3363            let left = if k == 0 { f64::INFINITY } else { values[k - 1] };
3364            let right = if k == samples { f64::INFINITY } else { values[k + 1] };
3365            if values[k] <= left && values[k] <= right && values[k] < best.0 + 1e-3 * scale {
3366                let (mut lo, mut hi) = (((k as f64) - 1.0).max(0.0) / samples as f64, ((k as f64) + 1.0).min(samples as f64) / samples as f64);
3367                for _ in 0..80 {
3368                    let (m1, m2) = (lo + (hi - lo) / 3.0, hi - (hi - lo) / 3.0);
3369                    if f(m1) < f(m2) { hi = m2 } else { lo = m1 }
3370                }
3371                least = least.min(f(0.5 * (lo + hi)));
3372            }
3373        }
3374        least
3375    };
3376    // Candidate boundary paths, each a map [0, 1] -> plane.
3377    #[derive(Clone, Copy)]
3378    enum Path {
3379        Corner(usize),
3380        /// Edge k's grazing point over the times [s0, s1].
3381        Graze(usize, f64, f64),
3382        /// Edge k at the start (false) or end (true).
3383        End(usize, bool),
3384    }
3385    let speed_ratio = |s: f64| frame.angle_rate(s); // κ |c'| / |c'| per unit s, over |c'|
3386    let graze_point = |k: usize, s: f64| -> Option<f64> {
3387        // Along edge k at u: v·n = |c'| (e1·n + κ (ẑ × x)·n), κ = φ'/|c'|.
3388        let (a, b) = (corners[k], corners[(k + 1) % n]);
3389        let (nn, _) = edges[k];
3390        let kappa = speed_ratio(s) / frame.first(s).length();
3391        let rot = |x: [f64; 2]| -x[1] * nn[0] + x[0] * nn[1]; // (ẑ × x)·n
3392        // An edge along the start tangent grazes where (ẑ × x)·n = 0 whatever
3393        // the curvature (also where it is zero, at an inflection).
3394        let (ga, gb) = if nn[0].abs() < 1e-15 { (rot(a), rot(b)) } else { (nn[0] + kappa * rot(a), nn[0] + kappa * rot(b)) };
3395        if ga == gb {
3396            return None;
3397        }
3398        let u = ga / (ga - gb);
3399        (u > 0.0 && u < 1.0).then_some(u)
3400    };
3401    let mut paths: Vec<Path> = (0..n).map(Path::Corner).collect();
3402    for k in 0..n {
3403        // Where along the motion edge k has a grazing point inside it.
3404        let steps = 2000;
3405        let mut run: Option<f64> = None;
3406        for j in 0..=steps {
3407            let s = j as f64 / steps as f64;
3408            let on = graze_point(k, s).is_some();
3409            match (on, run) {
3410                (true, None) => run = Some(s),
3411                (false, Some(s0)) => {
3412                    paths.push(Path::Graze(k, s0, s - 1.0 / steps as f64));
3413                    run = None;
3414                }
3415                _ => {}
3416            }
3417        }
3418        if let Some(s0) = run {
3419            paths.push(Path::Graze(k, s0, 1.0));
3420        }
3421        paths.push(Path::End(k, false));
3422        paths.push(Path::End(k, true));
3423    }
3424    let eval = |path: Path, t: f64| -> [f64; 2] {
3425        match path {
3426            Path::Corner(k) => place(t, corners[k]),
3427            Path::Graze(k, s0, s1) => {
3428                let s = s0 + (s1 - s0) * t;
3429                let u = graze_point(k, s).unwrap_or(if t < 0.5 { 0.0 } else { 1.0 });
3430                let (a, b) = (corners[k], corners[(k + 1) % n]);
3431                place(s, [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u])
3432            }
3433            Path::End(k, at_end) => {
3434                let (a, b) = (corners[k], corners[(k + 1) % n]);
3435                place(if at_end { 1.0 } else { 0.0 }, [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t])
3436            }
3437        }
3438    };
3439    // Split parameters: mutual crossings (polyline, then Newton), and each
3440    // path's ends lying on another.
3441    let m = 400;
3442    let polylines: Vec<Vec<[f64; 2]>> = paths.iter().map(|&p| (0..=m).map(|i| eval(p, i as f64 / m as f64)).collect()).collect();
3443    let mut cuts: Vec<Vec<f64>> = vec![vec![0.0, 1.0]; paths.len()];
3444    let tol = 1e-10 * scale;
3445    for a in 0..paths.len() {
3446        for b in a + 1..paths.len() {
3447            for i in 0..m {
3448                for j in 0..m {
3449                    let (p0, p1, q0, q1) = (polylines[a][i], polylines[a][i + 1], polylines[b][j], polylines[b][j + 1]);
3450                    let d1 = [p1[0] - p0[0], p1[1] - p0[1]];
3451                    let d2 = [q1[0] - q0[0], q1[1] - q0[1]];
3452                    let det = d1[0] * d2[1] - d1[1] * d2[0];
3453                    if det.abs() < 1e-300 {
3454                        continue;
3455                    }
3456                    let r = [q0[0] - p0[0], q0[1] - p0[1]];
3457                    let (x, y) = ((r[0] * d2[1] - r[1] * d2[0]) / det, (r[0] * d1[1] - r[1] * d1[0]) / det);
3458                    if !(-1e-9..=1.0 + 1e-9).contains(&x) || !(-1e-9..=1.0 + 1e-9).contains(&y) {
3459                        continue;
3460                    }
3461                    // Newton on the exact paths.
3462                    let (mut ta, mut tb) = ((i as f64 + x) / m as f64, (j as f64 + y) / m as f64);
3463                    for _ in 0..50 {
3464                        let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
3465                        let h = 1e-7;
3466                        let da = { let q = eval(paths[a], (ta + h).min(1.0)); let p = eval(paths[a], (ta - h).max(0.0)); let w = (ta + h).min(1.0) - (ta - h).max(0.0); [(q[0] - p[0]) / w, (q[1] - p[1]) / w] };
3467                        let db = { let q = eval(paths[b], (tb + h).min(1.0)); let p = eval(paths[b], (tb - h).max(0.0)); let w = (tb + h).min(1.0) - (tb - h).max(0.0); [(q[0] - p[0]) / w, (q[1] - p[1]) / w] };
3468                        let f = [pa[0] - pb[0], pa[1] - pb[1]];
3469                        let det = -da[0] * db[1] + da[1] * db[0];
3470                        if det.abs() < 1e-300 {
3471                            break;
3472                        }
3473                        let dta = (-f[0] * db[1] + f[1] * db[0]) / det;
3474                        let dtb = (da[0] * f[1] - da[1] * f[0]) / det;
3475                        ta = (ta - dta).clamp(0.0, 1.0);
3476                        tb = (tb - dtb).clamp(0.0, 1.0);
3477                        if dta.abs() + dtb.abs() < 1e-15 {
3478                            break;
3479                        }
3480                    }
3481                    let (pa, pb) = (eval(paths[a], ta), eval(paths[b], tb));
3482                    if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < tol {
3483                        cuts[a].push(ta);
3484                        cuts[b].push(tb);
3485                    }
3486                }
3487            }
3488        }
3489    }
3490    for c in &mut cuts {
3491        c.sort_by(|x, y| x.total_cmp(y));
3492        c.dedup_by(|x, y| (*x - *y).abs() < 1e-9);
3493    }
3494    // Pieces on the boundary: covered on exactly one side.
3495    struct Piece {
3496        path: usize,
3497        t0: f64,
3498        t1: f64,
3499    }
3500    let eps = 1e-7 * scale;
3501    let mut pieces: Vec<Piece> = Vec::new();
3502    for (index, c) in cuts.iter().enumerate() {
3503        for w in c.windows(2) {
3504            let (t0, t1) = (w[0], w[1]);
3505            if t1 - t0 < 1e-9 {
3506                continue;
3507            }
3508            let (pa, pb) = (eval(paths[index], t0), eval(paths[index], t1));
3509            if (pa[0] - pb[0]).hypot(pa[1] - pb[1]) < 1e-9 * scale {
3510                continue;
3511            }
3512            let tm = 0.5 * (t0 + t1);
3513            let (p, q) = (eval(paths[index], tm), eval(paths[index], (tm + 1e-6).min(1.0)));
3514            let d = [q[0] - p[0], q[1] - p[1]];
3515            let length = d[0].hypot(d[1]);
3516            if length == 0.0 {
3517                continue;
3518            }
3519            let left = [p[0] - d[1] / length * eps, p[1] + d[0] / length * eps];
3520            let right = [p[0] + d[1] / length * eps, p[1] - d[0] / length * eps];
3521            let (l, r) = (depth(left) < -1e-12 * scale, depth(right) < -1e-12 * scale);
3522            match (l, r) {
3523                (true, false) => pieces.push(Piece { path: index, t0, t1 }),
3524                (false, true) => pieces.push(Piece { path: index, t0: t1, t1: t0 }),
3525                _ => {}
3526            }
3527        }
3528    }
3529    // Chain into one loop, the region on its left.
3530    if pieces.is_empty() {
3531        return Err(refuse("prism_along", "no boundary found".into()));
3532    }
3533    let mut order = vec![0usize];
3534    let mut used = vec![false; pieces.len()];
3535    used[0] = true;
3536    loop {
3537        let last = &pieces[*order.last().expect("order")];
3538        let end = eval(paths[last.path], last.t1);
3539        let start = eval(paths[pieces[order[0]].path], pieces[order[0]].t0);
3540        if order.len() > 1 && (end[0] - start[0]).hypot(end[1] - start[1]) < 1e-8 * scale {
3541            break;
3542        }
3543        let next = (0..pieces.len()).filter(|&k| !used[k]).min_by(|&x, &y| {
3544            let (px, py) = (eval(paths[pieces[x].path], pieces[x].t0), eval(paths[pieces[y].path], pieces[y].t0));
3545            (px[0] - end[0]).hypot(px[1] - end[1]).total_cmp(&(py[0] - end[0]).hypot(py[1] - end[1]))
3546        });
3547        let Some(next) = next else {
3548            return Err(refuse("prism_along", "the boundary does not close".into()));
3549        };
3550        let p = eval(paths[pieces[next].path], pieces[next].t0);
3551        if (p[0] - end[0]).hypot(p[1] - end[1]) > 1e-8 * scale {
3552            return Err(refuse("prism_along", format!("the boundary does not close (gap {:e})", (p[0] - end[0]).hypot(p[1] - end[1]))));
3553        }
3554        used[next] = true;
3555        order.push(next);
3556    }
3557    if used.iter().any(|u| !u) {
3558        return Err(refuse("prism_along", "the swept region is not one simple loop".into()));
3559    }
3560    // Plane curves of the loop, then the extrusion.
3561    let world = |q: [f64; 2], h: f64| base.add(e1.scale(q[0])).add(e2.scale(q[1])).add(up.scale(h));
3562    let mut loop_points: Vec<[f64; 2]> = order.iter().map(|&k| eval(paths[pieces[k].path], pieces[k].t0)).collect();
3563    loop_points.push(loop_points[0]);
3564    let count = order.len();
3565    let mut shell = Shell::new();
3566    let (bottom_v, top_v): (Vec<u64>, Vec<u64>) = (0..count).map(|k| (shell.vertex(world(loop_points[k], from)), shell.vertex(world(loop_points[k], to)))).unzip();
3567    let mut bottom_e = Vec::with_capacity(count);
3568    let mut top_e = Vec::with_capacity(count);
3569    let mut curves2d = Vec::with_capacity(count);
3570    for (slot, &k) in order.iter().enumerate() {
3571        let piece = &pieces[k];
3572        let straight = matches!(paths[piece.path], Path::End(..));
3573        let next = (slot + 1) % count;
3574        let curve_at = |h: f64| -> Result<NurbsCurve, KernelRefusal> {
3575            if straight {
3576                return frag(make_line(world(loop_points[slot], h), world(loop_points[next], h)), "line");
3577            }
3578            let samples = 192;
3579            let ts: Vec<f64> = (0..=samples).map(|i| i as f64 / samples as f64).collect();
3580            let mut points: Vec<Vec3> = ts.iter().map(|&t| world(eval(paths[piece.path], piece.t0 + (piece.t1 - piece.t0) * t), h)).collect();
3581            points[0] = world(loop_points[slot], h);
3582            points[samples] = world(loop_points[next], h);
3583            frag(crate::interpolate_curve(&points, 5, &ts), "plane_path")
3584        };
3585        let (cb, ct) = (curve_at(from)?, curve_at(to)?);
3586        bottom_e.push(shell.edge(cb.clone(), bottom_v[slot], bottom_v[next], false)?);
3587        top_e.push(shell.edge(ct.clone(), top_v[slot], top_v[next], false)?);
3588        curves2d.push((cb, ct));
3589    }
3590    let verticals: Vec<u64> = (0..count).map(|k| shell.edge(make_line(world(loop_points[k], from), world(loop_points[k], to)).expect("line"), bottom_v[k], top_v[k], false)).collect::<Result<_, _>>()?;
3591    // Top (outward up) and bottom (outward down).
3592    let top_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, to)).collect();
3593    let top_surface = plane_patch(world([0.0, 0.0], to), e1, e2, &top_points)?;
3594    let mut coedges = Vec::with_capacity(count);
3595    for k in 0..count {
3596        coedges.push((top_e[k], true, plane_pcurve(world([0.0, 0.0], to), e1, e2, &curves2d[k].1, true)?));
3597    }
3598    shell.face(top_surface, coedges);
3599    let bottom_points: Vec<Vec3> = loop_points.iter().map(|&q| world(q, from)).collect();
3600    let bottom_surface = plane_patch(world([0.0, 0.0], from), e2, e1, &bottom_points)?;
3601    let mut coedges = Vec::with_capacity(count);
3602    for k in (0..count).rev() {
3603        coedges.push((bottom_e[k], false, plane_pcurve(world([0.0, 0.0], from), e2, e1, &curves2d[k].0, false)?));
3604    }
3605    shell.face(bottom_surface, coedges);
3606    // Sides: the bottom curve extruded (u along it, v up), outward on its right.
3607    for k in 0..count {
3608        let next = (k + 1) % count;
3609        let (cb, _) = &curves2d[k];
3610        let rows: Vec<Vec<Vec4>> = cb
3611            .control_points
3612            .iter()
3613            .map(|p| {
3614                let x = p.point().expect("point");
3615                vec![Vec4::from_point(x, p.w), Vec4::from_point(x.add(up.scale(to - from)), p.w)]
3616            })
3617            .collect();
3618        let surface = frag(NurbsSurface::new(cb.degree, 1, cb.knots.clone(), vec![0.0, 0.0, 1.0, 1.0], rows), "side")?;
3619        let [u0, u1] = frag(cb.domain(), "domain")?;
3620        let coedges = vec![
3621            (bottom_e[k], true, uv_line([u0, 0.0], [u1, 0.0])?),
3622            (verticals[next], true, uv_line([u1, 0.0], [u1, 1.0])?),
3623            (top_e[k], false, uv_line([u1, 1.0], [u0, 1.0])?),
3624            (verticals[k], false, uv_line([u0, 1.0], [u0, 0.0])?),
3625        ];
3626        shell.face(surface, coedges);
3627    }
3628    let body = shell.solid();
3629    let issues = body.validate();
3630    if !issues.is_empty() {
3631        return Err(refuse("assembly", format!("the swept prism is not valid: {:?}", issues.first())));
3632    }
3633    Ok(body)
3634}
3635
3636/// Two turns driven by one parameter t: first by `rates[0]·t` about the
3637/// first axis (point, direction), then by `rates[1]·t` about the second; both
3638/// axes fixed in the world; t from `from` to `to`.
3639#[derive(Clone, Copy, Debug)]
3640pub struct TwoTurns {
3641    pub axes: [(Vec3, Vec3); 2],
3642    pub rates: [f64; 2],
3643    pub from: f64,
3644    pub to: f64,
3645}
3646
3647/// The whole sweep of a solid of revolution tumbled by two turns about skew
3648/// axes. The grazing loop moves on the tool (the instantaneous screw changes
3649/// with t), so the walls are lofted through exact samples. Where the tumble
3650/// passes over a point twice (the one-pass probe), the time is bisected into
3651/// spans each passing once, and the spans' sweeps are joined (consecutive
3652/// spans meet along a grazing loop; distant ones overlap).
3653pub fn sweep_solid_tumbled(tool: &SolidTool, motion: &TwoTurns) -> Result<BrepSolid, KernelRefusal> {
3654    if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
3655        return Err(input("range", "the motion's range must run forward".into()));
3656    }
3657    let mut points = [Vec3::new(0.0, 0.0, 0.0); 2];
3658    let mut directions = [Vec3::new(0.0, 0.0, 1.0); 2];
3659    for k in 0..2 {
3660        points[k] = motion.axes[k].0;
3661        directions[k] = motion.axes[k].1.normalized().map_err(|_| input("axis", "a turn's axis has no direction".into()))?;
3662    }
3663    let turns = Turns { points, directions, rates: motion.rates };
3664    let SolidTool::Revolved { axis_origin, axis_direction, radial, meridian } = tool else {
3665        return Err(refuse("tumble_tool", "a tumble sweeps a solid of revolution only".into()));
3666    };
3667    let axis = axis_direction.normalized().map_err(|_| input("axis", "the tool's axis has no direction".into()))?;
3668    let base = Screw {
3669        a: points[0],
3670        e: directions[0],
3671        omega: motion.rates[0].abs() + motion.rates[1].abs(),
3672        c: 0.0,
3673        c2: 0.0,
3674        t0: motion.from,
3675        t1: motion.to,
3676        frame: None,
3677        turns: Some(turns),
3678        frozen: None,
3679        loop_samples: LOFT_SAMPLES,
3680        extent: points[1].sub(points[0]).length(),
3681    };
3682    let span = |t0: f64, t1: f64| -> Result<BrepSolid, KernelRefusal> {
3683        let mut screw = base;
3684        screw.t0 = t0;
3685        screw.t1 = t1;
3686        let mut last = None;
3687        // The loop's sampling doubles while the walls' rows miss between
3688        // samples (a thin ring's loop bends sharply before it folds).
3689        'samples: while screw.loop_samples <= 16 * LOFT_SAMPLES {
3690            for k in 0..8 {
3691                let reference = radial.rotate_about(axis, PI * k as f64 / 4.0);
3692                match lofted(&screw, *axis_origin, axis, reference, meridian) {
3693                    // A bulge that crosses a seam high up reads on the next
3694                    // wall as a part held away from its end: other seams too.
3695                    Err(error) if matches!(refused_as(&error), Some("loft_structure") | Some("end_trim_corner") | Some("end_trim_deep")) => last = Some(error),
3696                    Err(error) if refused_as(&error) == Some("loft_rows") => {
3697                        last = Some(error);
3698                        screw.loop_samples *= 2;
3699                        continue 'samples;
3700                    }
3701                    other => return other,
3702                }
3703            }
3704            break;
3705        }
3706        Err(last.expect("tried"))
3707    };
3708    // Bisect where the sweep folds over itself.
3709    let mut pending = vec![(motion.from, motion.to, 0usize)];
3710    let mut spans: Vec<(f64, BrepSolid)> = Vec::new();
3711    while let Some((t0, t1, depth)) = pending.pop() {
3712        match span(t0, t1) {
3713            Ok(body) => spans.push((t0, body)),
3714            Err(error) if refused_as(&error) == Some("folds") && depth < 6 => {
3715                let middle = 0.5 * (t0 + t1);
3716                pending.push((middle, t1, depth + 1));
3717                pending.push((t0, middle, depth + 1));
3718            }
3719            Err(error) => return Err(error),
3720        }
3721    }
3722    spans.sort_by(|a, b| a.0.total_cmp(&b.0));
3723    let mut bodies: Vec<BrepSolid> = spans.into_iter().map(|(_, body)| body).collect();
3724    if bodies.len() == 1 {
3725        return Ok(bodies.pop().expect("one"));
3726    }
3727    crate::boolean_operation_nary(&bodies, crate::BooleanOperation::Union)
3728}
3729
3730/// A cap's pcurve for a lofted edge through the cap's exact chart (the edge
3731/// read back into the tool's frame at the cap's time).
3732fn charted_pcurve(chart: &Chart, screw: &Screw, t: f64, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
3733    let [d0, d1] = frag(curve.domain(), "domain")?;
3734    let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3735    let mut uv = Vec::with_capacity(ts.len());
3736    for &tt in &ts {
3737        let x = screw.invert(t, frag(curve.evaluate(tt), "point")?);
3738        let [u, v] = chart.invert(x);
3739        uv.push(Vec3::new(u, v, 0.0));
3740    }
3741    let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "charted_pcurve")?;
3742    if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3743}
3744
3745/// A cap's pcurve for a lofted edge: its dense image on the cap's surface.
3746fn projected_pcurve(surface: &NurbsSurface, curve: &NurbsCurve, forward: bool) -> Result<NurbsCurve, KernelRefusal> {
3747    let [d0, d1] = frag(curve.domain(), "domain")?;
3748    let ts: Vec<f64> = (0..=PCURVE_SAMPLES).map(|j| d0 + (d1 - d0) * j as f64 / PCURVE_SAMPLES as f64).collect();
3749    let mut uv = Vec::with_capacity(ts.len());
3750    for &t in &ts {
3751        let x = frag(curve.evaluate(t), "point")?;
3752        let foot = frag(crate::project_point_to_surface(surface, x), "project")?;
3753        if foot.distance > 1e-6 * (1.0 + x.length()) {
3754            return Err(refuse("loft_cap", format!("a lofted edge leaves its cap by {} at {x:?}", foot.distance)));
3755        }
3756        uv.push(Vec3::new(foot.u, foot.v, 0.0));
3757    }
3758    let fitted = frag(crate::interpolate_curve(&uv, 5, &ts), "projected_pcurve")?;
3759    if forward { Ok(fitted) } else { frag(fitted.reversed(), "reversed") }
3760}
3761
3762type Signature = (Vec<(usize, usize, bool)>, Vec<(bool, Vec<(usize, bool)>)>, Vec<(usize, bool)>);
3763
3764fn signature(split: &Split) -> Result<Signature, KernelRefusal> {
3765    Ok((
3766        split.curves.iter().map(|c| (c.start, c.end, c.degenerate)).collect(),
3767        split.caps.iter().map(|cap| (cap.front, cap.coedges.iter().map(|&(c, f, _)| (c, f)).collect())).collect(),
3768        split.grazing()?,
3769    ))
3770}
3771
3772fn lofted(screw: &Screw, origin: Vec3, axis: Vec3, radial: Vec3, meridian: &[OutlinePiece]) -> Result<BrepSolid, KernelRefusal> {
3773    let split_at = |t: f64| -> Result<Split, KernelRefusal> {
3774        let mut frozen = *screw;
3775        frozen.frozen = Some(t);
3776        split_revolved(&frozen, origin, axis, radial, meridian)
3777    };
3778    let mut lambdas: Vec<f64> = (0..=LOFT_SAMPLES).map(|i| i as f64 / LOFT_SAMPLES as f64).collect();
3779    // World samples of grazing curve c at time t, in the wall's direction.
3780    let samples_of = |split: &Split, c: usize, back_forward: bool, t: f64| -> Result<Vec<Vec3>, KernelRefusal> {
3781        let Some(list) = &split.curves[c].samples else {
3782            return Err(refuse("loft_samples", "a grazing curve has no exact samples to loft".into()));
3783        };
3784        let mut world: Vec<Vec3> = list.iter().map(|&x| screw.apply(t, x)).collect();
3785        if back_forward {
3786            world.reverse();
3787        }
3788        Ok(world)
3789    };
3790    // Time breaks start even and are refined only in the spans whose middle
3791    // misses, by the quintic's h^6 error: a smooth stretch of the motion keeps
3792    // its long spans (doubling everywhere tripled the wall for one bend).
3793    let mut fractions: Vec<f64> = (0..=15).map(|j| j as f64 / 15.0).collect();
3794    loop {
3795        let count = fractions.len() - 1;
3796        // The walls' second parameter runs 0 -> 1 over the motion.
3797        let times: Vec<f64> = fractions.iter().map(|&f| if f == 1.0 { screw.t1 } else { screw.t0 + (screw.t1 - screw.t0) * f }).collect();
3798        let splits: Vec<Split> = times.iter().map(|&t| split_at(t)).collect::<Result<_, _>>()?;
3799        if splits.iter().any(|split| !split.slides.is_empty() || !split.slide_sides.is_empty()) {
3800            return Err(refuse("loft_structure", "a flat end that slides within its own plane is not lofted".into()));
3801        }
3802        let shape = signature(&splits[0])?;
3803        for split in &splits[1..] {
3804            if signature(split)? != shape {
3805                return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
3806            }
3807        }
3808        let grazing = shape.2.clone();
3809        if grazing.is_empty() {
3810            return Err(refuse("no_grazing", "the tool has no grazing loop under this motion".into()));
3811        }
3812        let scale = splits[0].scale + screw.extent;
3813        let target = 5e-10 * scale;
3814        // Walls: per grazing curve, rows along it at each time, then across time.
3815        let mut walls = Vec::with_capacity(grazing.len());
3816        // Every wall's exact samples at every time.
3817        let mut sampled: Vec<Vec<Vec<Vec3>>> = Vec::with_capacity(grazing.len());
3818        for &(c, back_forward) in &grazing {
3819            let mut at_times = Vec::with_capacity(times.len());
3820            for (j, split) in splits.iter().enumerate() {
3821                let points = samples_of(split, c, back_forward, times[j])?;
3822                if points.len() != lambdas.len() {
3823                    if points.len() < 13 || points.len() % 2 == 0 {
3824                        return Err(refuse("loft_samples", "a grazing curve's exact samples cannot be lofted".into()));
3825                    }
3826                    lambdas = (0..points.len()).map(|i| i as f64 / (points.len() - 1) as f64).collect();
3827                }
3828                at_times.push(points);
3829            }
3830            sampled.push(at_times);
3831        }
3832        // A row through every `stride`-th sample: its worst miss at the
3833        // samples it leaves out, over every wall and time.
3834        let thinned = |stride: usize| -> Result<f64, KernelRefusal> {
3835            let coarse_lambdas: Vec<f64> = lambdas.iter().step_by(stride).copied().collect();
3836            let mut worst = 0.0f64;
3837            for at_times in &sampled {
3838                for points in at_times {
3839                    let coarse_points: Vec<Vec3> = points.iter().step_by(stride).copied().collect();
3840                    let coarse = frag(crate::interpolate_curve(&coarse_points, 5, &coarse_lambdas), "loft_row")?;
3841                    for (i, x) in points.iter().enumerate().filter(|(i, _)| i % stride != 0) {
3842                        worst = worst.max(frag(coarse.evaluate(lambdas[i]), "loft_row")?.sub(*x).length());
3843                    }
3844                }
3845            }
3846            Ok(worst)
3847        };
3848        // Each wall's rows pass through as few of its samples as hold every
3849        // exact sample within a quarter of the target, at every time (a loop
3850        // smooth but for one sharp bend needs its samples only there, and the
3851        // wall's size is what the solid's users pay for): from every 16th,
3852        // a stretch that misses at a sample it leaves out gets its middle one.
3853        let spans = lambdas.len() - 1;
3854        let mut kept: Vec<Vec<usize>> = Vec::with_capacity(sampled.len());
3855        for at_times in &sampled {
3856            let coarsest = if spans % 16 == 0 && spans >= 96 { 16 } else { 1 };
3857            let mut subset: Vec<usize> = (0..=spans).step_by(coarsest).collect();
3858            loop {
3859                let subset_lambdas: Vec<f64> = subset.iter().map(|&i| lambdas[i]).collect();
3860                let mut misses = vec![0.0f64; subset.len() - 1];
3861                for points in at_times {
3862                    let row_points: Vec<Vec3> = subset.iter().map(|&i| points[i]).collect();
3863                    let row = frag(crate::interpolate_curve(&row_points, 5, &subset_lambdas), "loft_row")?;
3864                    for k in 0..subset.len() - 1 {
3865                        for i in subset[k] + 1..subset[k + 1] {
3866                            misses[k] = misses[k].max(frag(row.evaluate(lambdas[i]), "loft_row")?.sub(points[i]).length());
3867                        }
3868                    }
3869                }
3870                let mut refined = Vec::with_capacity(2 * subset.len());
3871                for k in 0..subset.len() - 1 {
3872                    refined.push(subset[k]);
3873                    if misses[k] > 0.25 * target {
3874                        refined.push((subset[k] + subset[k + 1]) / 2);
3875                    }
3876                }
3877                refined.push(spans);
3878                if refined.len() == subset.len() {
3879                    break;
3880                }
3881                subset = refined;
3882            }
3883            kept.push(subset);
3884        }
3885        // Where a wall keeps neighbouring samples there is none left out
3886        // between them to measure at: a row passes through its samples, and
3887        // between them it is as good as the loop is smooth at that spacing.
3888        // Its miss there is estimated from the rows through every other
3889        // sample, scaled by the quintic's 2^-6.
3890        let dense = kept.iter().any(|subset| subset.windows(2).any(|pair| pair[1] == pair[0] + 1));
3891        let between = if dense { thinned(2)? / 64.0 } else { 0.0 };
3892        for (index, &(c, back_forward)) in grazing.iter().enumerate() {
3893            let row_lambdas: Vec<f64> = kept[index].iter().map(|&i| lambdas[i]).collect();
3894            let mut rows: Vec<NurbsCurve> = Vec::with_capacity(times.len());
3895            for points in &sampled[index] {
3896                let row_points: Vec<Vec3> = kept[index].iter().map(|&i| points[i]).collect();
3897                rows.push(frag(crate::interpolate_curve(&row_points, 5, &row_lambdas), "loft_row")?);
3898            }
3899            let knots_u = rows[0].knots.clone();
3900            let mut net: Vec<Vec<Vec4>> = Vec::with_capacity(rows[0].control_points.len());
3901            let mut knots_v = Vec::new();
3902            for k in 0..rows[0].control_points.len() {
3903                let column: Vec<Vec3> = rows.iter().map(|row| row.control_points[k].point()).collect::<Result<_, _>>().map_err(|e| refuse("loft", e))?;
3904                let across = frag(crate::interpolate_curve(&column, 5, &fractions), "loft_column")?;
3905                knots_v = across.knots.clone();
3906                net.push(across.control_points.clone());
3907            }
3908            let surface = frag(NurbsSurface::new(5, 5, knots_u, knots_v, net), "loft_wall")?;
3909            walls.push((c, back_forward, surface, rows.first().cloned().expect("rows"), rows.last().cloned().expect("rows")));
3910        }
3911        if between > target {
3912            return Err(refuse("loft_rows", format!("the lofted wall's rows stay about {between:e} off the grazing loop between its samples")));
3913        }
3914        // Against the exact loop in the middle of each span, every sample.
3915        let mut misses = vec![0.0f64; count];
3916        for j in 0..count {
3917            let t = 0.5 * (times[j] + times[j + 1]);
3918            let fraction = 0.5 * (fractions[j] + fractions[j + 1]);
3919            let split = split_at(t)?;
3920            if signature(&split)? != shape {
3921                return Err(refuse("loft_structure", "the grazing loop changes its structure along the curve".into()));
3922            }
3923            for (c, back_forward, surface, _, _) in &walls {
3924                let exact = samples_of(&split, *c, *back_forward, t)?;
3925                for (i, x) in exact.iter().enumerate() {
3926                    misses[j] = misses[j].max(frag(surface.evaluate(lambdas[i], fraction), "loft_check")?.sub(*x).length());
3927                }
3928            }
3929        }
3930        let worst = misses.iter().copied().fold(0.0f64, f64::max);
3931        if worst > target {
3932            if count > 2000 {
3933                return Err(refuse("loft_accuracy", format!("the lofted wall stays {worst:e} off the grazing loop")));
3934            }
3935            // A span missing by more than half the target is cut into
3936            // ceil((e / (target / 4))^(1/6)) pieces; the rest stay.
3937            let mut refined = Vec::with_capacity(2 * fractions.len());
3938            for j in 0..count {
3939                let pieces = if misses[j] > 0.5 * target { ((misses[j] / (0.25 * target)).powf(1.0 / 6.0).ceil() as usize).max(2) } else { 1 };
3940                for k in 0..pieces {
3941                    refined.push(fractions[j] + (fractions[j + 1] - fractions[j]) * k as f64 / pieces as f64);
3942                }
3943            }
3944            refined.push(1.0);
3945            fractions = refined;
3946            continue;
3947        }
3948        // Each K corner's path through the sample times.
3949        let mut paths: HashMap<usize, Vec<Vec3>> = HashMap::new();
3950        for &(c, _) in &grazing {
3951            for r in [splits[0].curves[c].start, splits[0].curves[c].end] {
3952                paths.entry(r).or_insert_with(|| splits.iter().zip(&times).map(|(split, &t)| screw.apply(t, split.points[r])).collect());
3953            }
3954        }
3955        return assemble_lofted(screw, &splits[0], &splits[count], &walls, &fractions, &paths);
3956    }
3957}
3958
3959/// Refuse a lofted wall with an edge of regression: across one the wall's
3960/// normal turns over, and the band beside it lies inside nearby poses (the
3961/// sweep folds within a few degrees of the motion, which no probe walk is
3962/// dense enough to be sure of seeing). Normals are compared between
3963/// neighbours on every time row and between rows, outside what the end trims
3964/// take away (`removed`, chart rings) and a few columns off its edge (a fold
3965/// line meets the end at the cut's own ends).
3966fn wall_regular(surface: &NurbsSurface, fractions: &[f64], removed: &[&[[f64; 2]]]) -> Result<(), KernelRefusal> {
3967    const COLUMNS: usize = 4 * LOFT_SAMPLES;
3968    let mut rows: Vec<f64> = Vec::with_capacity(2 * fractions.len());
3969    for j in 0..fractions.len() {
3970        rows.push(fractions[j]);
3971        if j + 1 < fractions.len() {
3972            rows.push(0.5 * (fractions[j] + fractions[j + 1]));
3973        }
3974    }
3975    let normal = |u: f64, v: f64| -> Result<Option<Vec3>, KernelRefusal> {
3976        let h = 1e-6;
3977        let (ua, ub, va, vb) = ((u - h).max(0.0), (u + h).min(1.0), (v - h).max(0.0), (v + h).min(1.0));
3978        let du = frag(surface.evaluate(ub, v), "regular")?.sub(frag(surface.evaluate(ua, v), "regular")?).scale(1.0 / (ub - ua));
3979        let dv = frag(surface.evaluate(u, vb), "regular")?.sub(frag(surface.evaluate(u, va), "regular")?).scale(1.0 / (vb - va));
3980        Ok(du.cross(dv).normalized().ok())
3981    };
3982    let mut previous: Option<Vec<Option<Vec3>>> = None;
3983    for &v in &rows {
3984        // What a trim takes away is not part of the body.
3985        let row: Vec<Option<Vec3>> = (0..=COLUMNS)
3986            .map(|i| {
3987                let u = i as f64 / COLUMNS as f64;
3988                if removed.iter().any(|ring| inside_ring(ring, [u, v]) || near_ring(ring, [u, v], 4.0 / COLUMNS as f64)) { Ok(None) } else { normal(u, v) }
3989            })
3990            .collect::<Result<_, _>>()?;
3991        for i in 0..COLUMNS {
3992            if let (Some(a), Some(b)) = (row[i], row[i + 1]) {
3993                if a.dot(b) < -0.5 {
3994                    return Err(refuse("wall_folds", format!("a lofted wall turns over (an edge of regression near parameter ({:.4}, {v:.4})); the sweep folds onto itself", i as f64 / COLUMNS as f64)));
3995                }
3996            }
3997            if let Some(Some(Some(a))) = previous.as_ref().map(|p| p.get(i).copied()) {
3998                if let Some(b) = row[i] {
3999                    if a.dot(b) < -0.5 {
4000                        return Err(refuse("wall_folds", format!("a lofted wall turns over (an edge of regression near parameter ({:.4}, {v:.4})); the sweep folds onto itself", i as f64 / COLUMNS as f64)));
4001                    }
4002                }
4003            }
4004        }
4005        previous = Some(row);
4006    }
4007    Ok(())
4008}
4009
4010/// A stretch of a wall's row at v, from u = a to u = b, as an edge's curve.
4011fn row_piece(wall: &NurbsSurface, v: f64, a: f64, b: f64) -> Result<NurbsCurve, KernelRefusal> {
4012    let ts: Vec<f64> = (0..=128).map(|j| j as f64 / 128.0).collect();
4013    let along: Vec<Vec3> = ts.iter().map(|&s| frag(wall.evaluate((a + (b - a) * s).clamp(0.0, 1.0), v), "wall")).collect::<Result<_, _>>()?;
4014    frag(crate::interpolate_curve(&along, 5, &ts), "loop_piece")
4015}
4016
4017/// A wall's end row in pieces around its trims and breaks (one more than the
4018/// spans), at v.
4019fn row_pieces(wall: &NurbsSurface, v: f64, spans: &[TrimSpan]) -> Result<Vec<NurbsCurve>, KernelRefusal> {
4020    let mut pieces = Vec::with_capacity(spans.len() + 1);
4021    let mut from = 0.0;
4022    for span in spans {
4023        pieces.push(row_piece(wall, v, from, span.lambdas[0])?);
4024        from = span.lambdas[1];
4025    }
4026    pieces.push(row_piece(wall, v, from, 1.0)?);
4027    Ok(pieces)
4028}
4029
4030/// The wall over u ∈ [a, b] only, in the same parameters.
4031fn restrict_u(surface: &NurbsSurface, a: f64, b: f64) -> Result<NurbsSurface, KernelRefusal> {
4032    let [u0, u1] = frag(surface.domain_u(), "domain")?;
4033    if a <= u0 + 1e-12 && b >= u1 - 1e-12 {
4034        return Ok(surface.clone());
4035    }
4036    let columns = surface.control_points[0].len();
4037    let mut knots = Vec::new();
4038    let mut net: Vec<Vec<Vec4>> = Vec::new();
4039    for j in 0..columns {
4040        let line: Vec<Vec4> = surface.control_points.iter().map(|row| row[j]).collect();
4041        let mut curve = frag(NurbsCurve::new(surface.degree_u, surface.knots_u.clone(), line), "restrict")?;
4042        if b < u1 - 1e-12 {
4043            curve = frag(curve.split(b), "restrict")?.0;
4044        }
4045        if a > u0 + 1e-12 {
4046            curve = frag(curve.split(a), "restrict")?.1;
4047        }
4048        if j == 0 {
4049            knots = curve.knots.clone();
4050            net = vec![Vec::with_capacity(columns); curve.control_points.len()];
4051        }
4052        for (i, point) in curve.control_points.iter().enumerate() {
4053            net[i].push(*point);
4054        }
4055    }
4056    frag(NurbsSurface::new(surface.degree_u, surface.degree_v, knots, surface.knots_v.clone(), net), "restrict")
4057}
4058
4059/// One place a wall is cut back where an end pose bulges past it: the wall
4060/// parameter range [λa, λb] along the grazing loop where the trim leaves and
4061/// rejoins it, the cut (from the point at λa to the point at λb, on both the
4062/// wall and the pose), the wall's side of the trim from λa to λb (`chain`),
4063/// the double curve and its swallowtail point if the trim follows one, and
4064/// the part of the wall's chart it takes away (`removed`, (u, v) points
4065/// around it).
4066struct TrimSpan {
4067    lambdas: [f64; 2],
4068    points: [Vec3; 2],
4069    /// None: no cut at all, only a break in the end's row at λa = λb (where
4070    /// a line splitting the wall lands on it).
4071    cut: Option<NurbsCurve>,
4072    chain: Vec<(TrimPiece, bool, NurbsCurve)>,
4073    slot: Option<(NurbsCurve, Vec3)>,
4074    /// The swallowtail's point in the wall's chart (u, v), where the trim
4075    /// follows a double curve.
4076    tip: Option<[f64; 2]>,
4077    removed: Vec<[f64; 2]>,
4078}
4079
4080/// Every place an end pose bulges past one wall, in order along the loop, and
4081/// the loop's pieces around them (one more than the spans).
4082struct EndTrim {
4083    spans: Vec<TrimSpan>,
4084    pieces: Vec<NurbsCurve>,
4085}
4086
4087/// Which edge a piece of a trimmed wall's end runs on: the cut it shares with
4088/// the cap, or the wall's own double curve (both of the slot's sides).
4089#[derive(Clone, Copy, PartialEq, Eq, Debug)]
4090enum TrimPiece {
4091    Cut,
4092    Slot,
4093}
4094
4095/// The cut of a lofted wall (u along the grazing loop, v the motion's time
4096/// fraction) by the pose at its end (`end` 0: v = 0, 1: v = 1), if the pose
4097/// bulges past it, else None.
4098///
4099/// The wall meets the pose along K and leaves it to second order (K grazes),
4100/// so with ε the time fraction from the end, f = d / ε² (d the pose's signed
4101/// distance) is smooth up to the end. The part of the wall inside the pose is
4102/// where f < 0; its boundary is traced as the curve f = 0 from the end and back
4103/// to it. It need not be a graph over u: a tongue of it may run on past where
4104/// it left the end.
4105fn end_trim(screw: &Screw, split: &Split, t: f64, wall: &NurbsSurface, end: usize) -> Result<Option<EndTrim>, KernelRefusal> {
4106    let v_of = |e: f64| if end == 1 { 1.0 - e } else { e };
4107    let at = |u: f64, e: f64| -> Result<Vec3, KernelRefusal> { frag(wall.evaluate(u.clamp(0.0, 1.0), v_of(e)), "wall") };
4108    let f = |u: f64, e: f64| -> Result<f64, KernelRefusal> { Ok(split.distance(screw.invert(t, at(u, e)?)) / (e * e)) };
4109    // Below `floor` the walls' fit (5e-10 of the scale) swamps d / ε².
4110    let floor = 1e-3;
4111    // How far into the motion (as a fraction of it) a trim may reach (a fold
4112    // band can run most of a span); past that the end pose meets a distant
4113    // part of the sweep (`end_trim_deep`).
4114    let window = 0.6;
4115    let noise = 1e-9 * split.scale / (floor * floor);
4116    // Where the wall dips into the pose just off the end.
4117    let n = 2000;
4118    let edge: Vec<f64> = (0..=n).map(|i| f(i as f64 / n as f64, floor)).collect::<Result<_, _>>()?;
4119    let inside: Vec<bool> = edge.iter().map(|&x| x < -noise).collect();
4120    if !inside.iter().any(|&x| x) {
4121        // Nothing near the end; a pocket deeper in is caught below.
4122        return pierced(&f, floor, window, noise, &[]);
4123    }
4124    if inside[0] || inside[n] || inside[1] || inside[n - 1] {
4125        return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
4126    }
4127    // Crossings of the floor line, by bisection between samples of either sign.
4128    let mut crossings: Vec<f64> = Vec::new();
4129    for i in 0..n {
4130        if inside[i] != inside[i + 1] {
4131            let (mut a, mut b) = (i as f64 / n as f64, (i + 1) as f64 / n as f64);
4132            let a_inside = inside[i];
4133            for _ in 0..60 {
4134                let mid = 0.5 * (a + b);
4135                if (f(mid, floor)? < 0.0) == a_inside { a = mid } else { b = mid }
4136            }
4137            crossings.push(0.5 * (a + b));
4138        }
4139    }
4140    if crossings.len() % 2 != 0 || crossings.is_empty() {
4141        return Err(refuse("end_trim", "an end pose's bulges past a wall do not pair up".into()));
4142    }
4143    // Trace f = 0 from the first crossing of each pair into the wall until it
4144    // returns to the floor; it must come back at the pair's other crossing.
4145    let gradient = |u: f64, e: f64| -> Result<[f64; 2], KernelRefusal> {
4146        let h = 1e-6;
4147        Ok([(f(u + h, e)? - f(u - h, e)?) / (2.0 * h), (f(u, e + h)? - f(u, e - h)?) / (2.0 * h)])
4148    };
4149    let settle = |mut p: [f64; 2]| -> Result<[f64; 2], KernelRefusal> {
4150        for _ in 0..30 {
4151            let value = f(p[0], p[1])?;
4152            let g = gradient(p[0], p[1])?;
4153            let norm = g[0] * g[0] + g[1] * g[1];
4154            if norm == 0.0 {
4155                return Err(refuse("end_trim", "the cut of an end pose has a flat point".into()));
4156            }
4157            p = [p[0] - value * g[0] / norm, p[1] - value * g[1] / norm];
4158            if value.abs() * p[1] * p[1] < 1e-14 * split.scale {
4159                break;
4160            }
4161        }
4162        Ok(p)
4163    };
4164    let step = 1e-3;
4165    let mut cuts: Vec<Vec<[f64; 2]>> = Vec::new();
4166    // Each cut starts at the lowest crossing not yet used and ends at the
4167    // unused one it comes back to.
4168    let mut unused: Vec<f64> = crossings.clone();
4169    while !unused.is_empty() {
4170        let start = unused.remove(0);
4171        let mut points: Vec<[f64; 2]> = vec![[start, floor]];
4172        let mut heading = [0.0, 1.0];
4173        loop {
4174            let last = *points.last().expect("points");
4175            let g = gradient(last[0], last[1])?;
4176            let length = g[0].hypot(g[1]);
4177            let mut tangent = [-g[1] / length, g[0] / length];
4178            if tangent[0] * heading[0] + tangent[1] * heading[1] < 0.0 {
4179                tangent = [-tangent[0], -tangent[1]];
4180            }
4181            let next = settle([last[0] + step * tangent[0], last[1] + step * tangent[1]])?;
4182            if next[1] < floor {
4183                break;
4184            }
4185            if next[0] <= 1e-3 || next[0] >= 1.0 - 1e-3 {
4186                return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
4187            }
4188            if next[1] > window {
4189                return Err(refuse("end_trim_deep", "an end pose holds a deep part of a wall".into()));
4190            }
4191            if points.len() > 20_000 {
4192                return Err(refuse("end_trim", "the cut of an end pose does not close".into()));
4193            }
4194            heading = [next[0] - last[0], next[1] - last[1]];
4195            points.push(next);
4196        }
4197        let back = *points.last().expect("points");
4198        let Some(at_end) = unused.iter().position(|&c| (back[0] - c).abs() <= 2.0 * step) else {
4199            return Err(refuse("end_trim", "the cut of an end pose does not come back where it should".into()));
4200        };
4201        let back_at = unused.remove(at_end);
4202        // A dip that never rises off the floor and comes straight back is
4203        // the fit's noise at the end, not a bulge.
4204        if points.len() == 1 && (back_at - start).abs() <= step {
4205            continue;
4206        }
4207        points.push([back_at, floor]);
4208        // Down to the end itself along the cut's direction at the floor (taken
4209        // from the first point clearly above it).
4210        let to_end = |p: [f64; 2], rest: &[[f64; 2]], reversed: bool| -> Result<[f64; 2], KernelRefusal> {
4211            // Along the cut's own tangent there (across the gradient of f), so
4212            // the run to the end continues it smoothly.
4213            let g = gradient(p[0], p[1])?;
4214            if g[0].abs() > 1e-3 * g[1].abs() {
4215                return Ok([p[0] + g[1] / g[0] * p[1], 0.0]);
4216            }
4217            let clear = |q: &&[f64; 2]| q[1] - p[1] > 0.25 * step;
4218            let found = if reversed { rest.iter().rev().find(clear) } else { rest.iter().find(clear) };
4219            Ok(match found {
4220                Some(q) => [p[0] - (q[0] - p[0]) / (q[1] - p[1]) * p[1], 0.0],
4221                None => [p[0], 0.0],
4222            })
4223        };
4224        let count = points.len();
4225        let (first, last) = (to_end(points[0], &points[1..], false)?, to_end(points[count - 1], &points[..count - 1], true)?);
4226        if [first[0], last[0]].iter().any(|&u| u <= 1e-3 || u >= 1.0 - 1e-3) {
4227            return Err(refuse("end_trim_corner", "an end pose bulges past a wall across a corner of its grazing loop".into()));
4228        }
4229        points.insert(0, first);
4230        points.push(last);
4231        if points[0][0] > points[points.len() - 1][0] {
4232            points.reverse();
4233        }
4234        cuts.push(points);
4235    }
4236    let rings: Vec<&[[f64; 2]]> = cuts.iter().map(|c| c.as_slice()).collect();
4237    pierced(&f, floor, window, noise, &rings)?;
4238    let chart = |p: [f64; 2]| [p[0], v_of(p[1])];
4239    // A curve on the wall through chart points and its pcurve, both on the
4240    // given parameters (normalized to [0, 1]); chart-length parameters by
4241    // default (the wall's own speed collapses near an edge of regression, so
4242    // 3D chord lengths would bunch them).
4243    let chart_lengths = |chart_points: &[[f64; 2]]| -> Vec<f64> {
4244        let mut params = vec![0.0];
4245        for k in 1..chart_points.len() {
4246            let gap = (chart_points[k][0] - chart_points[k - 1][0]).hypot(chart_points[k][1] - chart_points[k - 1][1]);
4247            params.push(params[k - 1] + gap.max(1e-15));
4248        }
4249        params
4250    };
4251    // Returns the curve, its pcurve, and how far the pcurve's image strays
4252    // from the curve between the points.
4253    let curve_through = |chart_points: &[[f64; 2]], params: &[f64], what: &'static str| -> Result<(NurbsCurve, NurbsCurve, f64), KernelRefusal> {
4254        let total = *params.last().expect("params");
4255        let params: Vec<f64> = params.iter().map(|x| x / total).collect();
4256        let world: Vec<Vec3> = chart_points.iter().map(|p| at(p[0], p[1])).collect::<Result<_, _>>()?;
4257        let uv: Vec<Vec3> = chart_points.iter().map(|p| Vec3::new(p[0], v_of(p[1]), 0.0)).collect();
4258        let (curve, pcurve) = (frag(crate::interpolate_curve(&world, 5, &params), what)?, frag(crate::interpolate_curve(&uv, 5, &params), what)?);
4259        // Between the points too: the pcurve's image stays on the curve.
4260        let mut worst = 0.0f64;
4261        for k in 0..params.len() - 1 {
4262            let s = 0.5 * (params[k] + params[k + 1]);
4263            let q = frag(pcurve.evaluate(s), "pcurve")?;
4264            let e = if end == 1 { 1.0 - q.y } else { q.y };
4265            worst = worst.max(at(q.x, e)?.sub(frag(curve.evaluate(s), "curve")?).length());
4266        }
4267        Ok((curve, pcurve, worst))
4268    };
4269    // A trim curve is refined until its pcurve's image is within `fine` of
4270    // it, and taken as near as it gets if within `loose` (a double curve has
4271    // a cusp at its swallowtail's point, where no finer trace helps; a
4272    // tighter bar costs a thin tool many rounds and buys no reader anything).
4273    let (fine, loose) = (1e-7 * split.scale, 1e-7 * split.scale);
4274    let missed = |what: &str| refuse("end_trim", format!("the {what}'s pcurve leaves it between its points however finely it is traced"));
4275    // The cut, refined where its fit misses: a settled point between each
4276    // pair of traced ones (on the stretches run straight to the end, their
4277    // middles).
4278    let cut_through = |mut chart_points: Vec<[f64; 2]>| -> Result<(NurbsCurve, NurbsCurve), KernelRefusal> {
4279        let mut best: Option<(NurbsCurve, NurbsCurve, f64)> = None;
4280        for _ in 0..7 {
4281            let fitted = curve_through(&chart_points, &chart_lengths(&chart_points), "cut")?;
4282            let done = fitted.2 <= fine;
4283            if best.as_ref().map_or(true, |b| fitted.2 < b.2) {
4284                best = Some(fitted);
4285            }
4286            if done {
4287                break;
4288            }
4289            let mut finer = Vec::with_capacity(2 * chart_points.len());
4290            for k in 0..chart_points.len() - 1 {
4291                finer.push(chart_points[k]);
4292                let (p, q) = (chart_points[k], chart_points[k + 1]);
4293                let middle = [0.5 * (p[0] + q[0]), 0.5 * (p[1] + q[1])];
4294                // Below the floor the cut runs straight to the end: split it there as a line.
4295                finer.push(if p[1] >= floor && q[1] >= floor { settle(middle)? } else { middle });
4296            }
4297            finer.push(chart_points[chart_points.len() - 1]);
4298            chart_points = finer;
4299        }
4300        match best {
4301            Some((curve, pcurve, worst)) if worst <= loose => Ok((curve, pcurve)),
4302            _ => Err(missed("cut")),
4303        }
4304    };
4305    let piece = |a: f64, b: f64| -> Result<NurbsCurve, KernelRefusal> {
4306        let ts: Vec<f64> = (0..=128).map(|j| j as f64 / 128.0).collect();
4307        let along: Vec<Vec3> = ts.iter().map(|&s| at(a + (b - a) * s, 0.0)).collect::<Result<_, _>>()?;
4308        frag(crate::interpolate_curve(&along, 5, &ts), "loop_piece")
4309    };
4310    // Where the wall turns over near the end (a swallowtail), the cut passes
4311    // through a point of the end's own loop K a second time, on the wall's far
4312    // sheet: there the wall's double curve starts, and the trim follows it to
4313    // the swallowtail's point and back instead of the cut's first stretch.
4314    let span_of = |points: &Vec<[f64; 2]>| -> Result<TrimSpan, KernelRefusal> {
4315        if let Some((k, ua, b)) = double_start(&at, points, floor)? {
4316            // The double curve, traced more finely while its fit misses.
4317            let mut traced: Option<(Vec<[f64; 2]>, Vec<[f64; 2]>, NurbsCurve, NurbsCurve, NurbsCurve, f64)> = None;
4318            for round in 0..5 {
4319                let (a_side, b_side) = double_curve(&at, [ua, 0.0], b, window, 2e-3 / f64::from(1u32 << round))?;
4320                // Both sides on the continuation's own arc length, so the double
4321                // curve and its two pcurves share one parameter.
4322                let mut sigma = vec![0.0];
4323                for k in 1..a_side.len() {
4324                    let step = (a_side[k][0] - a_side[k - 1][0]).hypot(a_side[k][1] - a_side[k - 1][1]).hypot((b_side[k][0] - b_side[k - 1][0]).hypot(b_side[k][1] - b_side[k - 1][1]));
4325                    sigma.push(sigma[k - 1] + step.max(1e-15));
4326                }
4327                let ((slot, pa, miss_a), (_, pb, miss_b)) = (curve_through(&a_side, &sigma, "double_curve")?, curve_through(&b_side, &sigma, "double_curve")?);
4328                let worst = miss_a.max(miss_b);
4329                if traced.as_ref().map_or(true, |t| worst < t.5) {
4330                    traced = Some((a_side, b_side, slot, pa, pb, worst));
4331                }
4332                if worst <= fine {
4333                    break;
4334                }
4335            }
4336            let Some((a_side, b_side, slot, pa, pb, _)) = traced.filter(|t| t.5 <= loose) else { return Err(missed("double curve")) };
4337            // The cut keeps the stretch from the far sheet's point to its end away
4338            // from where the double curve leaves K.
4339            let low = ua < points[0][0];
4340            if !low && ua <= points[points.len() - 1][0] {
4341                return Err(refuse("end_trim", "the double curve of a wall leaves its end inside the cut".into()));
4342            }
4343            let mut kept: Vec<[f64; 2]> = if low { points[k..].to_vec() } else { points[..=k].to_vec() };
4344            if low { kept[0] = b } else { *kept.last_mut().expect("kept") = b }
4345            let (cut, cut_pcurve) = cut_through(kept.clone())?;
4346            let tip = at(a_side[a_side.len() - 1][0], a_side[a_side.len() - 1][1])?;
4347            // The wall's side of the trim from the low end to the high one.
4348            let chain = if low {
4349                vec![(TrimPiece::Slot, true, pa), (TrimPiece::Slot, false, frag(pb.reversed(), "reversed")?), (TrimPiece::Cut, true, cut_pcurve)]
4350            } else {
4351                vec![(TrimPiece::Cut, true, cut_pcurve), (TrimPiece::Slot, true, pb), (TrimPiece::Slot, false, frag(pa.reversed(), "reversed")?)]
4352            };
4353            let lambdas = if low { [ua, points[points.len() - 1][0]] } else { [points[0][0], ua] };
4354            let mut removed: Vec<[f64; 2]> = Vec::new();
4355            if low {
4356                removed.extend(a_side.iter().map(|&p| chart(p)));
4357                removed.extend(b_side.iter().rev().map(|&p| chart(p)));
4358                removed.extend(kept.iter().map(|&p| chart(p)));
4359            } else {
4360                removed.extend(kept.iter().map(|&p| chart(p)));
4361                removed.extend(b_side.iter().map(|&p| chart(p)));
4362                removed.extend(a_side.iter().rev().map(|&p| chart(p)));
4363            }
4364            // Everything the end pose holds must be in what goes.
4365            let dropped: Vec<[f64; 2]> = if low { points[..k].to_vec() } else { points[k + 1..].to_vec() };
4366            for p in dropped.iter().filter(|p| p[1] > 2.0 * floor && (p[0] - b[0]).hypot(p[1] - b[1]) > 5e-3) {
4367                if !inside_ring(&removed, chart(*p)) {
4368                    return Err(refuse("end_trim", "the cut of an end pose leaves the part of the wall its double curve takes away".into()));
4369                }
4370            }
4371            let points3 = [at(lambdas[0], 0.0)?, at(lambdas[1], 0.0)?];
4372            // The double curve's sides part about its end: one keeps to
4373            // each side of the swallowtail's point along the loop, so a
4374            // line through that point splits the wall between them.
4375            let tip_chart = chart(a_side[a_side.len() - 1]);
4376            let side = |points: &[[f64; 2]]| points.iter().map(|q| q[0] - tip_chart[0]).fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| (lo.min(d), hi.max(d)));
4377            let ((a_lo, a_hi), (b_lo, b_hi)) = (side(&a_side), side(&b_side));
4378            let parted = (a_lo >= -1e-9 && b_hi <= 1e-9) || (a_hi <= 1e-9 && b_lo >= -1e-9);
4379            if !parted {
4380                return Err(refuse("end_trim", "the two sides of a wall's double curve do not keep to either side of its end".into()));
4381            }
4382            return Ok(TrimSpan { lambdas, points: points3, cut: Some(cut), chain, slot: Some((slot, tip)), tip: Some(tip_chart), removed });
4383        }
4384        let (cut, wall_pcurve) = cut_through(points.clone())?;
4385        let (la, lb) = (points[0][0], points[points.len() - 1][0]);
4386        let removed: Vec<[f64; 2]> = points.iter().map(|&p| chart(p)).collect();
4387        Ok(TrimSpan { lambdas: [la, lb], points: [at(la, 0.0)?, at(lb, 0.0)?], cut: Some(cut), chain: vec![(TrimPiece::Cut, true, wall_pcurve)], slot: None, tip: None, removed })
4388    };
4389    if cuts.is_empty() {
4390        return pierced(&f, floor, window, noise, &[]);
4391    }
4392    let mut spans = Vec::with_capacity(cuts.len());
4393    for points in &cuts {
4394        spans.push(span_of(points)?);
4395    }
4396    spans.sort_by(|a, b| a.lambdas[0].total_cmp(&b.lambdas[0]));
4397    // A trim wholly inside what another takes away adds nothing (a bulge of
4398    // the end pose inside a swallowtail's slot).
4399    let edge_row = v_of(0.0);
4400    let within = |inner: &TrimSpan, outer: &TrimSpan| -> bool {
4401        inner.lambdas[0] >= outer.lambdas[0]
4402            && inner.lambdas[1] <= outer.lambdas[1]
4403            && inner.removed.iter().filter(|p| (p[1] - edge_row).abs() > floor).all(|&p| inside_ring(&outer.removed, p) || near_ring(&outer.removed, p, 1e-3))
4404    };
4405    let mut k = 0;
4406    while k < spans.len() {
4407        if (0..spans.len()).any(|j| j != k && within(&spans[k], &spans[j])) {
4408            spans.remove(k);
4409        } else {
4410            k += 1;
4411        }
4412    }
4413    for pair in spans.windows(2) {
4414        if pair[0].lambdas[1] >= pair[1].lambdas[0] {
4415            return Err(refuse("end_trim", "an end pose's trims of a wall overlap".into()));
4416        }
4417    }
4418    let mut pieces = Vec::with_capacity(spans.len() + 1);
4419    let mut from = 0.0;
4420    for span in &spans {
4421        pieces.push(piece(from, span.lambdas[0])?);
4422        from = span.lambdas[1];
4423    }
4424    pieces.push(piece(from, 1.0)?);
4425    Ok(Some(EndTrim { spans, pieces }))
4426}
4427
4428
4429
4430/// Is q within `reach` of the closed polygon `ring`'s sides?
4431fn near_ring(ring: &[[f64; 2]], q: [f64; 2], reach: f64) -> bool {
4432    (0..ring.len()).any(|k| {
4433        let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
4434        let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
4435        let length = dx * dx + dy * dy;
4436        let s = if length > 0.0 { (((q[0] - a[0]) * dx + (q[1] - a[1]) * dy) / length).clamp(0.0, 1.0) } else { 0.0 };
4437        (q[0] - a[0] - s * dx).hypot(q[1] - a[1] - s * dy) < reach
4438    })
4439}
4440
4441/// Is q inside the closed polygon `ring` (its last point joins its first)?
4442fn inside_ring(ring: &[[f64; 2]], q: [f64; 2]) -> bool {
4443    let mut crossings = 0;
4444    for k in 0..ring.len() {
4445        let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
4446        if (a[1] > q[1]) != (b[1] > q[1]) && q[0] < a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) {
4447            crossings += 1;
4448        }
4449    }
4450    crossings % 2 == 1
4451}
4452
4453/// The point of an end pose's cut (chart points (u, ε), ε the time fraction
4454/// from the end) that lies on the end's own loop K a second time, away from
4455/// the cut's ends: (its index along the cut, the loop parameter u of the point
4456/// of K it meets, and the refined (u, ε) on the cut), if there is one.
4457fn double_start(at: &dyn Fn(f64, f64) -> Result<Vec3, KernelRefusal>, points: &[[f64; 2]], floor: f64) -> Result<Option<(usize, f64, [f64; 2])>, KernelRefusal> {
4458    let n = 4000;
4459    let row: Vec<Vec3> = (0..=n).map(|i| at(i as f64 / n as f64, 0.0)).collect::<Result<_, _>>()?;
4460    let scale = row.iter().fold(0.0f64, |m, p| m.max(p.sub(row[0]).length())).max(1e-300);
4461    let mut best: Option<(f64, usize, usize)> = None;
4462    for (k, p) in points.iter().enumerate() {
4463        if p[1] < 4.0 * floor {
4464            continue;
4465        }
4466        let x = at(p[0], p[1])?;
4467        let (i, d) = row.iter().enumerate().map(|(i, q)| (i, q.sub(x).length())).fold((0, f64::INFINITY), |a, b| if b.1 < a.1 { b } else { a });
4468        // The cut nears K at its own ends in proportion to ε; the second
4469        // meeting is where that ratio drops to nothing.
4470        if best.map_or(true, |b| d / p[1] < b.0 / points[b.1][1]) {
4471            best = Some((d, k, i));
4472        }
4473    }
4474    let Some((distance, k, i)) = best else { return Ok(None) };
4475    // Coarse: the cut's points are a few thousandths of the chart apart.
4476    if distance > 5e-3 * scale {
4477        return Ok(None);
4478    }
4479    // Newton on at(b) = at(u, 0) for (b_u, b_ε, u).
4480    let mut x = [points[k][0], points[k][1], i as f64 / n as f64];
4481    let h = 1e-7;
4482    for _ in 0..40 {
4483        let g = at(x[0], x[1])?.sub(at(x[2], 0.0)?);
4484        if g.length() < 1e-13 * scale {
4485            // Not the trivial meeting of the end row with itself.
4486            if x[1] < 2.0 * floor || !(0.0..=1.0).contains(&x[0]) || !(0.0..=1.0).contains(&x[2]) {
4487                return Ok(None);
4488            }
4489            return Ok(Some((k, x[2], [x[0], x[1]])));
4490        }
4491        let c0 = at(x[0] + h, x[1])?.sub(at(x[0] - h, x[1])?).scale(0.5 / h);
4492        let c1 = at(x[0], x[1] + h)?.sub(at(x[0], x[1] - h)?).scale(0.5 / h);
4493        let c2 = at(x[2] + h, 0.0)?.sub(at(x[2] - h, 0.0)?).scale(-0.5 / h);
4494        let det = c0.dot(c1.cross(c2));
4495        if det.abs() < 1e-300 {
4496            break;
4497        }
4498        // Cramer's rule for [c0 c1 c2] d = -g.
4499        let r = g.scale(-1.0);
4500        let d = [r.dot(c1.cross(c2)) / det, c0.dot(r.cross(c2)) / det, c0.dot(c1.cross(r)) / det];
4501        x = [x[0] + d[0], x[1] + d[1], x[2] + d[2]];
4502    }
4503    if distance < 1e-6 * scale {
4504        return Err(refuse("end_trim", "an end pose's cut meets its own loop, but not exactly enough to follow".into()));
4505    }
4506    Ok(None)
4507}
4508
4509/// The double curve of a wall from where it leaves the end (a, on the end's
4510/// loop) and its partner b on the far sheet, traced by continuation of
4511/// at(a) = at(b) in (u, ε) for both, to the swallowtail point where the two
4512/// sides meet. Returns both sides' chart points, ending at the same point.
4513fn double_curve(at: &dyn Fn(f64, f64) -> Result<Vec3, KernelRefusal>, a: [f64; 2], b: [f64; 2], window: f64, step: f64) -> Result<(Vec<[f64; 2]>, Vec<[f64; 2]>), KernelRefusal> {
4514    let h = 1e-7;
4515    let scale = at(0.0, 0.0)?.sub(at(1.0, 0.0)?).length().max(at(0.5, 0.0)?.sub(at(0.0, 0.0)?).length()).max(1e-300);
4516    let jacobian = |x: [f64; 4]| -> Result<[Vec3; 4], KernelRefusal> {
4517        Ok([
4518            at(x[0] + h, x[1])?.sub(at(x[0] - h, x[1])?).scale(0.5 / h),
4519            at(x[0], x[1] + h)?.sub(at(x[0], (x[1] - h).max(0.0))?).scale(1.0 / (h + x[1].min(h))),
4520            at(x[2] + h, x[3])?.sub(at(x[2] - h, x[3])?).scale(-0.5 / h),
4521            at(x[2], x[3] + h)?.sub(at(x[2], (x[3] - h).max(0.0))?).scale(-1.0 / (h + x[3].min(h))),
4522        ])
4523    };
4524    let residual = |x: [f64; 4]| -> Result<Vec3, KernelRefusal> { Ok(at(x[0], x[1])?.sub(at(x[2], x[3])?)) };
4525    // The null direction of the 3x4 Jacobian: its signed 3x3 minors.
4526    let null = |j: &[Vec3; 4]| -> [f64; 4] {
4527        let minor = |p: usize, q: usize, r: usize| j[p].dot(j[q].cross(j[r]));
4528        let n = [minor(1, 2, 3), -minor(0, 2, 3), minor(0, 1, 3), -minor(0, 1, 2)];
4529        let length = n.iter().map(|x| x * x).sum::<f64>().sqrt().max(1e-300);
4530        [n[0] / length, n[1] / length, n[2] / length, n[3] / length]
4531    };
4532    // Minimum-norm Newton back onto at(a) = at(b).
4533    let correct = |mut x: [f64; 4]| -> Result<Option<[f64; 4]>, KernelRefusal> {
4534        for _ in 0..30 {
4535            let g = residual(x)?;
4536            if g.length() < 1e-13 * scale {
4537                return Ok(Some(x));
4538            }
4539            let j = jacobian(x)?;
4540            // J Jᵀ y = -g, step = Jᵀ y.
4541            let mut m = [[0.0; 3]; 3];
4542            let rows = [[j[0].x, j[1].x, j[2].x, j[3].x], [j[0].y, j[1].y, j[2].y, j[3].y], [j[0].z, j[1].z, j[2].z, j[3].z]];
4543            for p in 0..3 {
4544                for q in 0..3 {
4545                    m[p][q] = (0..4).map(|c| rows[p][c] * rows[q][c]).sum();
4546                }
4547            }
4548            let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
4549            if det.abs() < 1e-300 {
4550                return Ok(None);
4551            }
4552            let r = [-g.x, -g.y, -g.z];
4553            let solve = |col: usize| -> f64 {
4554                let mut a = m;
4555                for p in 0..3 {
4556                    a[p][col] = r[p];
4557                }
4558                (a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0]) + a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])) / det
4559            };
4560            let y = [solve(0), solve(1), solve(2)];
4561            for c in 0..4 {
4562                x[c] += (0..3).map(|p| rows[p][c] * y[p]).sum::<f64>();
4563            }
4564        }
4565        Ok(None)
4566    };
4567    let mut x = [a[0], a[1], b[0], b[1]];
4568    let mut a_side = vec![a];
4569    let mut b_side = vec![b];
4570    let mut heading: Option<[f64; 4]> = None;
4571    loop {
4572        let separation = (x[0] - x[2]).hypot(x[1] - x[3]);
4573        if separation < 3.0 * step && a_side.len() > 2 {
4574            let tip = [0.5 * (x[0] + x[2]), 0.5 * (x[1] + x[3])];
4575            a_side.push(tip);
4576            b_side.push(tip);
4577            return Ok((a_side, b_side));
4578        }
4579        let mut t = null(&jacobian(x)?);
4580        let reference = heading.unwrap_or([0.0, 1.0, 0.0, 0.0]);
4581        if (0..4).map(|c| t[c] * reference[c]).sum::<f64>() < 0.0 {
4582            t = [-t[0], -t[1], -t[2], -t[3]];
4583        }
4584        let mut next = None;
4585        let mut length = step;
4586        for _ in 0..6 {
4587            let guess = [x[0] + length * t[0], x[1] + length * t[1], x[2] + length * t[2], x[3] + length * t[3]];
4588            if let Some(y) = correct(guess)? {
4589                next = Some(y);
4590                break;
4591            }
4592            length *= 0.5;
4593        }
4594        let Some(y) = next else {
4595            // Near the swallowtail's point the two sheets turn tangent and
4596            // Newton gives out; close there if the sides are that near.
4597            if separation < 2e-2 && a_side.len() > 2 {
4598                let tip = [0.5 * (x[0] + x[2]), 0.5 * (x[1] + x[3])];
4599                a_side.push(tip);
4600                b_side.push(tip);
4601                return Ok((a_side, b_side));
4602            }
4603            return Err(refuse("end_trim", format!("the double curve of a wall cannot be followed (sides {separation:e} apart)")));
4604        };
4605        if [y[0], y[2]].iter().any(|&u| u <= 1e-3 || u >= 1.0 - 1e-3) {
4606            return Err(refuse("end_trim_corner", "the double curve of a wall reaches a corner of its grazing loop".into()));
4607        }
4608        if y[1] > window || y[3] > window || y[1] < 0.0 || y[3] < 0.0 {
4609            return Err(refuse("end_trim_deep", format!("the double curve of a wall runs deep into the motion or back past its end (its sides at time fractions {:.4} and {:.4} from the end, {} points in)", y[1], y[3], a_side.len())));
4610        }
4611        if a_side.len() > 20_000 {
4612            return Err(refuse("end_trim", "the double curve of a wall does not close".into()));
4613        }
4614        heading = Some([y[0] - x[0], y[1] - x[1], y[2] - x[2], y[3] - x[3]]);
4615        x = y;
4616        a_side.push([x[0], x[1]]);
4617        b_side.push([x[2], x[3]]);
4618    }
4619}
4620
4621/// Refuse a wall the end pose holds a part of that no cut encloses: a pocket
4622/// of the wall inside the pose away from the end.
4623fn pierced(
4624    f: &dyn Fn(f64, f64) -> Result<f64, KernelRefusal>,
4625    floor: f64,
4626    window: f64,
4627    noise: f64,
4628    cuts: &[&[[f64; 2]]],
4629) -> Result<Option<EndTrim>, KernelRefusal> {
4630    let enclosed = |q: [f64; 2]| -> bool { cuts.iter().any(|ring| inside_ring(ring, q)) };
4631    for i in 1..200 {
4632        for j in 1..=40 {
4633            let (u, e) = (i as f64 / 200.0, floor + (window - floor) * (j as f64 / 40.0).powi(2));
4634            // d itself against the fit's noise: f grows as 1 / ε² near the end.
4635            if f(u, e)? * e * e < -noise * floor * floor && !enclosed([u, e]) {
4636                return Err(refuse("end_trim_deep", "an end pose holds a part of a wall away from its end".into()));
4637            }
4638        }
4639    }
4640    Ok(None)
4641}
4642
4643#[allow(clippy::too_many_arguments)]
4644fn assemble_lofted(
4645    screw: &Screw,
4646    first: &Split,
4647    last: &Split,
4648    walls: &[(usize, bool, NurbsSurface, NurbsCurve, NurbsCurve)],
4649    times: &[f64],
4650    paths: &HashMap<usize, Vec<Vec3>>,
4651) -> Result<BrepSolid, KernelRefusal> {
4652    let mut shell = Shell::new();
4653    let ends = [(first, screw.t0), (last, screw.t1)];
4654    // Where an end pose bulges past a wall near its grazing loop (a saddle side
4655    // curving past the wall it touches), cut the wall back to where it
4656    // re-enters the pose and carry the end cap across the loop to there.
4657    let mut trims: HashMap<(usize, usize), EndTrim> = HashMap::new();
4658    // Walls split into strips along lines through their swallowtails' points.
4659    let mut split_walls: std::collections::HashSet<usize> = std::collections::HashSet::new();
4660    for (index, (_, _, surface, _, _)) in walls.iter().enumerate() {
4661        for end in 0..2 {
4662            if let Some(trim) = end_trim(screw, ends[end].0, ends[end].1, surface, end)? {
4663                trims.insert((index, end), trim);
4664            }
4665        }
4666        let removed: Vec<&[[f64; 2]]> = (0..2).filter_map(|end| trims.get(&(index, end))).flat_map(|trim| trim.spans.iter().map(|span| span.removed.as_slice())).collect();
4667        wall_regular(surface, times, &removed)?;
4668        // A trim along a double curve has that curve as one edge on both its
4669        // sides: on one face its two uses could not be told apart from its
4670        // 3D curve alone (the wall passes through it twice). The wall is split
4671        // along the parameter line from each swallowtail's point to the far
4672        // end's row, which that row is broken at.
4673        let tips_at = |end: usize| -> Vec<[f64; 2]> { trims.get(&(index, end)).map(|trim| trim.spans.iter().filter_map(|span| span.tip).collect()).unwrap_or_default() };
4674        let (tips0, tips1) = (tips_at(0), tips_at(1));
4675        if !tips0.is_empty() && !tips1.is_empty() {
4676            return Err(refuse("wall_split", "a wall turns over near both its ends".into()));
4677        }
4678        let (far, tips) = if tips0.is_empty() { (0usize, tips1) } else { (1usize, tips0) };
4679        // A straight split line serves only where the trimmed row keeps to
4680        // its own side of it: all that comes before the swallowtail's point
4681        // along the row at or below its u, all after at or above. (A cut
4682        // that bulges across it would be crossed by the line; such a wall
4683        // stays one face.)
4684        let straight = |trim: &EndTrim| -> Result<bool, KernelRefusal> {
4685            for (k, span) in trim.spans.iter().enumerate() {
4686                let Some(tip) = span.tip else { continue };
4687                let first_slot = span.chain.iter().position(|piece| piece.0 == TrimPiece::Slot).unwrap_or(0);
4688                for (j, other) in trim.spans.iter().enumerate() {
4689                    for (n, (_, _, pcurve)) in other.chain.iter().enumerate() {
4690                        let before = j < k || (j == k && n <= first_slot);
4691                        let [d0, d1] = frag(pcurve.domain(), "domain")?;
4692                        for i in 0..=32 {
4693                            let u = frag(pcurve.evaluate(d0 + (d1 - d0) * i as f64 / 32.0), "pcurve")?.x;
4694                            if (before && u > tip[0] + 1e-9) || (!before && u < tip[0] - 1e-9) {
4695                                return Ok(false);
4696                            }
4697                        }
4698                    }
4699                }
4700            }
4701            Ok(true)
4702        };
4703        let split_here = !tips.is_empty() && trims.get(&(index, 1 - far)).map(straight).transpose()?.unwrap_or(false);
4704        if split_here {
4705            split_walls.insert(index);
4706            let v_far = far as f64;
4707            let mut spans = trims.remove(&(index, far)).map(|trim| trim.spans).unwrap_or_default();
4708            for tip in &tips {
4709                if spans.iter().any(|span| tip[0] > span.lambdas[0] - 1e-9 && tip[0] < span.lambdas[1] + 1e-9) || tip[0] <= 1e-9 || tip[0] >= 1.0 - 1e-9 {
4710                    return Err(refuse("wall_split", "a wall's split line lands in a trim of its far end".into()));
4711                }
4712                let point = frag(surface.evaluate(tip[0], v_far), "wall")?;
4713                spans.push(TrimSpan { lambdas: [tip[0], tip[0]], points: [point, point], cut: None, chain: Vec::new(), slot: None, tip: None, removed: Vec::new() });
4714            }
4715            spans.sort_by(|a, b| a.lambdas[0].total_cmp(&b.lambdas[0]));
4716            let pieces = row_pieces(surface, v_far, &spans)?;
4717            trims.insert((index, far), EndTrim { spans, pieces });
4718        }
4719    }
4720    let mut vertex_at: HashMap<(usize, usize), u64> = HashMap::new();
4721    let mut vertex = |shell: &mut Shell, end: usize, p: usize| -> u64 {
4722        *vertex_at.entry((end, p)).or_insert_with(|| shell.vertex(screw.apply(ends[end].1, ends[end].0.points[p])))
4723    };
4724    let grazing: HashMap<usize, usize> = walls.iter().enumerate().map(|(index, w)| (w.0, index)).collect();
4725    // Edges at each end: K curves from the walls' end rows (in the tool
4726    // curve's direction), the rest the end split's own curves moved.
4727    let mut edge_at: HashMap<(usize, usize), u64> = HashMap::new();
4728    let mut edge_curve: HashMap<(usize, usize), NurbsCurve> = HashMap::new();
4729    // Per trimmed wall end: the loop's piece edges, and each span's cut and
4730    // double-curve edges (u64::MAX where a span has none).
4731    let mut trim_edges: HashMap<(usize, usize), (Vec<u64>, Vec<[u64; 2]>)> = HashMap::new();
4732    // Per trimmed wall end and span: the vertex at its low end, and at the
4733    // swallowtail's point where it has one.
4734    let mut trim_vertices: HashMap<(usize, usize), Vec<(u64, Option<u64>)>> = HashMap::new();
4735    for end in 0..2 {
4736        let (split, t) = ends[end];
4737        for (c, tool_curve) in split.curves.iter().enumerate() {
4738            let used = split.caps.iter().any(|cap| cap.front == (end == 1) && cap.coedges.iter().any(|e| e.0 == c));
4739            if !used {
4740                continue;
4741            }
4742            if let Some(&index) = grazing.get(&c) {
4743                if let Some(trim) = trims.get(&(index, end)) {
4744                    // Pieces in the wall's direction: K up to the cut, the cut, K after it.
4745                    let back_forward = walls[index].1;
4746                    let (from, to) = if back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
4747                    let (vf, vt) = (vertex(&mut shell, end, from), vertex(&mut shell, end, to));
4748                    let mut piece_edges = Vec::with_capacity(trim.pieces.len());
4749                    let mut span_edges = Vec::with_capacity(trim.spans.len());
4750                    let mut start = vf;
4751                    let mut span_vertices = Vec::with_capacity(trim.spans.len());
4752                    for (span, piece) in trim.spans.iter().zip(&trim.pieces) {
4753                        let va = shell.vertex(span.points[0]);
4754                        piece_edges.push(shell.edge(piece.clone(), start, va, false)?);
4755                        let Some(cut_curve) = &span.cut else {
4756                            // A break only: the row goes on from the same vertex.
4757                            span_edges.push([u64::MAX, u64::MAX]);
4758                            span_vertices.push((va, None));
4759                            start = va;
4760                            continue;
4761                        };
4762                        let vb = shell.vertex(span.points[1]);
4763                        let cut = shell.edge(cut_curve.clone(), va, vb, false)?;
4764                        // The double curve runs from where it leaves K (at
4765                        // the low end of the span if its chain starts with
4766                        // it) to the swallowtail's point.
4767                        let (slot, tip_vertex) = match &span.slot {
4768                            Some((curve, tip)) => {
4769                                let from = if span.chain[0].0 == TrimPiece::Slot { va } else { vb };
4770                                let tip = shell.vertex(*tip);
4771                                (shell.edge(curve.clone(), from, tip, false)?, Some(tip))
4772                            }
4773                            None => (u64::MAX, None),
4774                        };
4775                        span_edges.push([cut, slot]);
4776                        span_vertices.push((va, tip_vertex));
4777                        start = vb;
4778                    }
4779                    trim_vertices.insert((index, end), span_vertices);
4780                    piece_edges.push(shell.edge(trim.pieces[trim.pieces.len() - 1].clone(), start, vt, false)?);
4781                    trim_edges.insert((index, end), (piece_edges, span_edges));
4782                    continue;
4783                }
4784            }
4785            let curve = match grazing.get(&c) {
4786                Some(&index) => {
4787                    let (_, back_forward, _, row0, row1) = &walls[index];
4788                    let row = if end == 0 { row0 } else { row1 };
4789                    if *back_forward { frag(row.reversed(), "reversed")? } else { row.clone() }
4790                }
4791                None => screw.move_curve(t, &tool_curve.curve)?,
4792            };
4793            let (a, b) = (vertex(&mut shell, end, tool_curve.start), vertex(&mut shell, end, tool_curve.end));
4794            let edge = shell.edge(curve.clone(), a, b, tool_curve.degenerate)?;
4795            edge_at.insert((end, c), edge);
4796            edge_curve.insert((end, c), curve);
4797        }
4798    }
4799    for end in 0..2 {
4800        let (split, t) = ends[end];
4801        for cap in split.caps.iter().filter(|cap| cap.front == (end == 1)) {
4802            let surface = screw.move_surface(t, &cap.surface)?;
4803            let mut coedges = Vec::with_capacity(cap.coedges.len());
4804            for (c, forward, pcurve) in &cap.coedges {
4805                if let Some(&index) = grazing.get(c) {
4806                    if let (Some(trim), Some((piece_edges, span_edges))) = (trims.get(&(index, end)), trim_edges.get(&(index, end))) {
4807                        let Some(chart) = cap.chart.as_ref() else {
4808                            return Err(refuse("end_trim", "an end pose bulges past a wall beside a cap without a chart".into()));
4809                        };
4810                        // The cap runs the tool's curve forward or back; the
4811                        // pieces run the wall's way: loop piece, cut, loop
4812                        // piece, ... loop piece.
4813                        let along = *forward != walls[index].1;
4814                        let mut pieces: Vec<(u64, &NurbsCurve)> = Vec::with_capacity(2 * trim.pieces.len());
4815                        for (k, piece) in trim.pieces.iter().enumerate() {
4816                            pieces.push((piece_edges[k], piece));
4817                            if let Some(cut) = trim.spans.get(k).and_then(|span| span.cut.as_ref()) {
4818                                pieces.push((span_edges[k][0], cut));
4819                            }
4820                        }
4821                        if !along {
4822                            pieces.reverse();
4823                        }
4824                        for (edge, curve) in pieces {
4825                            coedges.push((edge, along, charted_pcurve(chart, screw, t, curve, along)?));
4826                        }
4827                        continue;
4828                    }
4829                }
4830                let pcurve = if grazing.contains_key(c) && cap.chart.is_some() {
4831                    charted_pcurve(cap.chart.as_ref().expect("chart"), screw, t, &edge_curve[&(end, *c)], *forward)?
4832                } else if grazing.contains_key(c) {
4833                    match projected_pcurve(&surface, &edge_curve[&(end, *c)], *forward) {
4834                        Ok(p) => p,
4835                        Err(error) => {
4836                            // Which: the exact samples themselves, or the row between them?
4837                            let samples = split.curves[*c].samples.as_ref().map(|list| {
4838                                list.iter()
4839                                    .map(|&x| crate::project_point_to_surface(&surface, screw.apply(t, x)).map(|f| f.distance).unwrap_or(f64::NAN))
4840                                    .fold(0.0f64, f64::max)
4841                            });
4842                            let line = split.curves[*c].curve.degree;
4843                            return Err(KernelRefusal::unsupported(
4844                                KernelStage::Classify,
4845                                "loft_cap",
4846                                format!("{} (curve {c} of degree {line}, end {end}, its exact samples off the cap by {samples:?}, cap degrees {}x{})", error.message, surface.degree_u, surface.degree_v),
4847                            ));
4848                        }
4849                    }
4850                } else {
4851                    pcurve.clone()
4852                };
4853                coedges.push((edge_at[&(end, *c)], *forward, pcurve));
4854            }
4855            shell.face(surface, coedges);
4856        }
4857    }
4858    // Orbits of K's corners: interpolated through the sample times as the
4859    // walls' end columns are.
4860    let mut orbits: HashMap<usize, u64> = HashMap::new();
4861    let mut keys: Vec<usize> = paths.keys().copied().collect();
4862    keys.sort_unstable();
4863    for r in keys {
4864        let curve = frag(crate::interpolate_curve(&paths[&r], 5, times), "orbit")?;
4865        let (a, b) = (vertex(&mut shell, 0, r), vertex(&mut shell, 1, r));
4866        orbits.insert(r, shell.edge(curve, a, b, false)?);
4867    }
4868    // One piece of a wall's end row in the direction of u: (edge, traversed
4869    // forward?, pcurve that way), and where a split line meets the row right
4870    // after it: (u, v, vertex).
4871    type RowPiece = (u64, bool, NurbsCurve, Option<(f64, f64, u64)>);
4872    for (index, (c, back_forward, surface, _, _)) in walls.iter().enumerate() {
4873        let tool_curve = &first.curves[*c];
4874        let (from, to) = if *back_forward { (tool_curve.end, tool_curve.start) } else { (tool_curve.start, tool_curve.end) };
4875        let row = |end: usize| -> Result<Vec<RowPiece>, KernelRefusal> {
4876            let v = end as f64;
4877            let (Some(trim), Some((piece_edges, span_edges))) = (trims.get(&(index, end)), trim_edges.get(&(index, end))) else {
4878                return Ok(vec![(edge_at[&(end, *c)], !*back_forward, uv_line([0.0, v], [1.0, v])?, None)]);
4879            };
4880            let vertices = &trim_vertices[&(index, end)];
4881            let mut list: Vec<RowPiece> = Vec::new();
4882            let mut at = 0.0;
4883            for (k, span) in trim.spans.iter().enumerate() {
4884                let broken = if span.cut.is_none() { Some((span.lambdas[0], v, vertices[k].0)) } else { None };
4885                list.push((piece_edges[k], true, uv_line([at, v], [span.lambdas[0], v])?, broken));
4886                for (n, (piece, forward, pcurve)) in span.chain.iter().enumerate() {
4887                    let edge = if *piece == TrimPiece::Cut { span_edges[k][0] } else { span_edges[k][1] };
4888                    // The swallowtail's point lies between the double curve's two uses.
4889                    let between = *piece == TrimPiece::Slot && span.chain.get(n + 1).is_some_and(|next| next.0 == TrimPiece::Slot);
4890                    let tip = match (span.tip, vertices[k].1) {
4891                        (Some(tip), Some(vertex)) if between && split_walls.contains(&index) => Some((tip[0], tip[1], vertex)),
4892                        _ => None,
4893                    };
4894                    list.push((edge, *forward, pcurve.clone(), tip));
4895                }
4896                at = span.lambdas[1];
4897            }
4898            list.push((piece_edges[trim.spans.len()], true, uv_line([at, v], [1.0, v])?, None));
4899            Ok(list)
4900        };
4901        let (bottom, top) = (row(0)?, row(1)?);
4902        let (low, high): (Vec<(f64, f64, u64)>, Vec<(f64, f64, u64)>) = (bottom.iter().filter_map(|piece| piece.3).collect(), top.iter().filter_map(|piece| piece.3).collect());
4903        if low.len() != high.len() || low.iter().zip(&high).any(|(a, b)| (a.0 - b.0).abs() > 1e-9) {
4904            return Err(refuse("wall_split", "a wall's split lines do not run from one end's row to the other's".into()));
4905        }
4906        // The split lines, each from its lower point to its upper.
4907        let mut lines: Vec<(u64, f64, f64, f64)> = Vec::with_capacity(low.len());
4908        for (below, above) in low.iter().zip(&high) {
4909            let ts: Vec<f64> = (0..=128).map(|j| j as f64 / 128.0).collect();
4910            let along: Vec<Vec3> = ts.iter().map(|&s| frag(surface.evaluate(below.0, below.1 + (above.1 - below.1) * s), "wall")).collect::<Result<_, _>>()?;
4911            let edge = shell.edge(frag(crate::interpolate_curve(&along, 5, &ts), "split_line")?, below.2, above.2, false)?;
4912            lines.push((edge, below.0, below.1, above.1));
4913        }
4914        // Each row in the stretches between the split lines.
4915        let stretches = |list: Vec<RowPiece>| -> Vec<Vec<(u64, bool, NurbsCurve)>> {
4916            let mut out = vec![Vec::new()];
4917            for (edge, forward, pcurve, cut) in list {
4918                out.last_mut().expect("stretch").push((edge, forward, pcurve));
4919                if cut.is_some() {
4920                    out.push(Vec::new());
4921                }
4922            }
4923            out
4924        };
4925        let (bottoms, tops) = (stretches(bottom), stretches(top));
4926        // A strip between two split lines (or the wall's sides): along the
4927        // start row, up its far side, back along the end row, down its near
4928        // side; on the wall over its own stretch of u only.
4929        for j in 0..=lines.len() {
4930            let mut coedges = bottoms[j].clone();
4931            match lines.get(j) {
4932                Some(&(edge, u, v0, v1)) => coedges.push((edge, true, uv_line([u, v0], [u, v1])?)),
4933                None => coedges.push((orbits[&to], true, uv_line([1.0, 0.0], [1.0, 1.0])?)),
4934            }
4935            for (edge, forward, pcurve) in tops[j].iter().rev() {
4936                coedges.push((*edge, !*forward, frag(pcurve.reversed(), "reversed")?));
4937            }
4938            match j.checked_sub(1).map(|k| lines[k]) {
4939                Some((edge, u, v0, v1)) => coedges.push((edge, false, uv_line([u, v1], [u, v0])?)),
4940                None => coedges.push((orbits[&from], false, uv_line([0.0, 1.0], [0.0, 0.0])?)),
4941            }
4942            let (ua, ub) = (if j == 0 { 0.0 } else { lines[j - 1].1 }, lines.get(j).map_or(1.0, |line| line.1));
4943            shell.face(restrict_u(surface, ua, ub)?, coedges);
4944        }
4945    }
4946    let body = shell.solid();
4947    let issues = body.validate();
4948    if !issues.is_empty() {
4949        // Name the faces whose coedges stray: their pcurves' ends on the surface
4950        // against their edges' ends.
4951        let mut notes = Vec::new();
4952        for face in body.shells.iter().flat_map(|s| &s.faces) {
4953            for coedge in &face.loops[0].coedges {
4954                let edge = body.edges.iter().find(|e| e.id == coedge.edge_id).expect("edge");
4955                let [p0, p1] = frag(coedge.pcurve.domain(), "domain")?;
4956                let a = frag(coedge.pcurve.evaluate(p0), "uv")?;
4957                let on = frag(face.surface.evaluate_extended(a.x, a.y), "on")?;
4958                let at = frag(edge.curve.evaluate(if coedge.forward { edge.t0 } else { edge.t1 }), "at")?;
4959                if on.sub(at).length() > 1e-6 {
4960                    notes.push(format!("face {} (degrees {}x{}) coedge on edge {} starts {:?} on the surface, its edge {:?}", face.id, face.surface.degree_u, face.surface.degree_v, edge.id, on, at));
4961                }
4962                let _ = p1;
4963            }
4964        }
4965        return Err(refuse("assembly", format!("the lofted sweep is not valid: {:?}; {}", issues.first(), notes.iter().take(4).cloned().collect::<Vec<_>>().join("; "))));
4966    }
4967    check_single_pass(screw, first, &body, &std::collections::HashSet::new())?;
4968    Ok(body)
4969}
4970
4971/// Is the world point y inside the moving tool at some time of the motion?
4972/// Steps t by the distance over a bound on the speed of any tool point near y.
4973fn covered(screw: &Screw, split: &Split, y: Vec3, margin: f64) -> Result<bool, KernelRefusal> {
4974    let reach = y.sub(screw.a).length() + split.scale + screw.extent;
4975    let speed = (screw.omega.abs() * reach + screw.rate_bound()).max(1e-300);
4976    let floor = (screw.t1 - screw.t0) * 1e-7;
4977    let mut t = screw.t0;
4978    let mut steps = 0usize;
4979    loop {
4980        let d = split.distance(screw.invert(t, y));
4981        if d < -margin {
4982            return Ok(true);
4983        }
4984        if t >= screw.t1 {
4985            return Ok(false);
4986        }
4987        if steps > 2_000_000 {
4988            return Err(refuse("probe_budget", "the one-pass check could not walk the motion past a point near the sweep".into()));
4989        }
4990        t = (t + ((d.abs() + margin) / speed).max(floor)).min(screw.t1);
4991        steps += 1;
4992    }
4993}
4994
4995/// Refuse a sweep that covers points just outside its own faces.
4996///
4997/// `sliding`: faces the tool slides within all through the motion. A probe
4998/// just off one stays that near the tool the whole way, and the walk's step
4999/// is that distance: it stands a thousand times further off them (whatever
5000/// of the tool rides higher than that over such a face is still found).
5001fn check_single_pass(screw: &Screw, split: &Split, body: &BrepSolid, sliding: &std::collections::HashSet<u64>) -> Result<(), KernelRefusal> {
5002    let margin = 1e-9 * split.scale;
5003    for face in body.shells.iter().flat_map(|s| &s.faces) {
5004        let step = if sliding.contains(&face.id) { 1e-3 * split.scale } else { 1e-6 * split.scale };
5005        let surface = &face.surface;
5006        // Probe inside the face: towards each corner of its loop from the
5007        // average of its corners (caps are convex in their parameters), and on
5008        // a grid for the four-sided walls.
5009        let mut corners: Vec<[f64; 2]> = Vec::new();
5010        for coedge in &face.loops[0].coedges {
5011            let domain = frag(coedge.pcurve.domain(), "domain")?;
5012            for k in 0..4 {
5013                let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 4.0), "pcurve")?;
5014                corners.push([p.x, p.y]);
5015            }
5016        }
5017        let middle = corners.iter().fold([0.0, 0.0], |s, c| [s[0] + c[0], s[1] + c[1]]);
5018        let middle = [middle[0] / corners.len() as f64, middle[1] / corners.len() as f64];
5019        let mut probes: Vec<[f64; 2]> = vec![middle];
5020        for c in &corners {
5021            for w in [0.25, 0.5, 0.75, 0.9, 0.99] {
5022                probes.push([middle[0] + (c[0] - middle[0]) * w, middle[1] + (c[1] - middle[1]) * w]);
5023            }
5024        }
5025        if surface.degree_v == 5 {
5026            let [u0, u1] = frag(surface.domain_u(), "domain")?;
5027            for i in 1..8 {
5028                for j in 1..64 {
5029                    probes.push([u0 + (u1 - u0) * i as f64 / 8.0, j as f64 / 64.0]);
5030                }
5031            }
5032        }
5033        let ([u0, u1], [v0, v1]) = (frag(surface.domain_u(), "domain")?, frag(surface.domain_v(), "domain")?);
5034        // Keep the probes inside the face's own loop (a face under a grazing
5035        // curve that dips is not convex in its parameters).
5036        let mut ring: Vec<[f64; 2]> = Vec::new();
5037        for coedge in &face.loops[0].coedges {
5038            let domain = frag(coedge.pcurve.domain(), "domain")?;
5039            for k in 0..32 {
5040                let p = frag(coedge.pcurve.evaluate(domain[0] + (domain[1] - domain[0]) * k as f64 / 32.0), "pcurve")?;
5041                ring.push([p.x, p.y]);
5042            }
5043        }
5044        let (lo, hi) = ring.iter().fold(([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]), |(lo, hi), q| ([lo[0].min(q[0]), lo[1].min(q[1])], [hi[0].max(q[0]), hi[1].max(q[1])]));
5045        for i in 1..12 {
5046            for j in 1..12 {
5047                probes.push([lo[0] + (hi[0] - lo[0]) * i as f64 / 12.0, lo[1] + (hi[1] - lo[1]) * j as f64 / 12.0]);
5048            }
5049        }
5050        let inside = |q: [f64; 2]| -> bool {
5051            let mut crossings = 0;
5052            for k in 0..ring.len() {
5053                let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
5054                if (a[1] > q[1]) != (b[1] > q[1]) && q[0] < a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) {
5055                    crossings += 1;
5056                }
5057            }
5058            crossings % 2 == 1
5059        };
5060        for [u, v] in probes.into_iter().filter(|&q| inside(q)) {
5061            let p = frag(surface.evaluate(u, v), "probe")?;
5062            let (hu, hv) = (1e-6 * (u1 - u0), 1e-6 * (v1 - v0));
5063            // Derivatives, not raw differences: their cross product is
5064            // measured against an absolute zero.
5065            let (ua, ub, va, vb) = ((u - hu).max(u0), (u + hu).min(u1), (v - hv).max(v0), (v + hv).min(v1));
5066            let du = frag(surface.evaluate(ub, v), "probe")?.sub(frag(surface.evaluate(ua, v), "probe")?).scale(1.0 / (ub - ua));
5067            let dv = frag(surface.evaluate(u, vb), "probe")?.sub(frag(surface.evaluate(u, va), "probe")?).scale(1.0 / (vb - va));
5068            let Ok(n) = du.cross(dv).normalized() else { continue };
5069            let n = if face.same_sense { n } else { n.scale(-1.0) };
5070            if covered(screw, split, p.add(n.scale(step)), margin)? {
5071                return Err(refuse("folds", format!("the sweep passes over a point twice (just outside its own face at {p:?}); not one pass")));
5072            }
5073        }
5074    }
5075    Ok(())
5076}
5077