Skip to main content

brep_kernel/construction/
solid_sweep.rs

1//! The sweep of a SOLID: everything a tool passes through as it moves, inside a
2//! region the caller names (the workpiece it is cut from).
3//!
4//! ## What is built, and why it reduces to a plane
5//!
6//! The boundary of a swept solid is an envelope: at each instant the tool's
7//! surface touches the sweep along its characteristic curve (where the motion's
8//! velocity is tangent to the surface), and the sweep's wall is the union of
9//! those curves. In general that wall has no simpler description. In the three
10//! classes built here it is invariant under a motion, so the region is a planar
11//! region carried by that motion:
12//!
13//! | class | tool | invariance | planar region | solid |
14//! |---|---|---|---|---|
15//! | SCREW | a solid of revolution | the screw | the section square to the screw axis | a loft through screwed copies |
16//! | TURN | a solid of revolution | the turn | the meridian section | a revolve |
17//! | PLANAR | a prism | the prism's direction | the union of the moving outline | an extrude |
18//!
19//! For a SCREW (axial advance `c` per radian) the reduced coordinates of a point
20//! are `(ℓ, o) = (ρ, α − z/c)`, both unchanged by the screw; for a TURN they are
21//! `(ρ, z)`; for a PLANAR motion `(ρ, α)` in the workpiece's frame. In each the
22//! region is described per LEVEL `ℓ` as a set of intervals of `o`.
23//!
24//! ## The region engine
25//!
26//! At a level the interval ends are local extrema of `o` over the images of the
27//! tool's boundary: on a face patch `S(u, v)` (a meridian piece revolved; an
28//! outline piece carried through time) Newton solves `ℓ(S) = ℓ₀`,
29//! `det(∇o, ∇ℓ) = 0` from seeds on a grid; on a sharp edge (a meridian corner
30//! revolved; an outline vertex carried through time; the outline at either end of
31//! the motion) the ends are the roots of `ℓ = ℓ₀`. These are CANDIDATES; a
32//! membership oracle (does the point's preimage under the motion meet the tool,
33//! for some time in the range, by the tool's signed distance) classifies the
34//! gaps between them, so the intervals are exact at every level. This is what
35//! survives where the characteristic degenerates: a grinding wheel set along its
36//! helix has a whole circle of characteristic points, which tracing the
37//! characteristic by parameter cannot follow, but the level extremes can.
38//!
39//! The levels are scanned for EVENTS — an interval born at a tip, two lobes
40//! merging, an interval end changing the boundary piece it lies on — solved by
41//! bisection. Between events each interval end is a smooth function of the
42//! level, sampled at levels clustered at both ends (`ℓ − ℓa ∝ σ²`, so a square
43//! root tip is smooth in `σ`) and interpolated as its own cubic edge. Along
44//! each event level the boundary runs along the level itself (an arc, or a
45//! straight line for the turn) where the intervals above and below differ.
46//!
47//! ## The contract
48//!
49//! Each returned body equals the sweep inside the clip region: levels in
50//! `clip.radius`, axial positions in `clip.axial`, angles in `clip.angle`. The
51//! caller cuts a workpiece that lies inside that region. A screw or turn whose
52//! first or last position reaches the clip region is refused, because there the
53//! sweep is not invariant; a planar sweep includes its end positions exactly.
54//!
55//! FOLDS. Two lobes of a space can merge where one face's envelope turns back
56//! (the plus-cut wheel's arm tip, ρ = 20). Where the face's fold Newton reaches
57//! the point it is a critical point like a tip; where it cannot (that envelope's
58//! level varies by 1e-4 along it, too flat for nested differences), the merge is
59//! located from the gap's own √ law: gap² fitted as a quadratic in the level at
60//! three levels just below, its root the merge level (4e-11 from ρ = 20).
61//!
62//! ## Not built yet (refused, never approximated)
63//!
64//! - A source whose level is constant along it (a vertex turning about the
65//!   clip's own axis) reads as roots everywhere; keep the clip's axis off such
66//!   a turn's axis.
67//! - Motions outside the three classes (two turns about skew axes: the
68//!   tumbling torus).
69use crate::{
70    extrude_profile_brep, interpolate_curve, loft_profile_brep, make_arc, make_line,
71    revolve_profile_brep, BrepSolid, KernelRefusal, KernelStage, NurbsCurve, OrRefuse, Vec3,
72};
73use std::f64::consts::{PI, TAU};
74
75/// One piece of a planar outline, in the outline's own 2D coordinates.
76#[derive(Clone, Copy, Debug, PartialEq)]
77pub enum OutlinePiece {
78    /// The segment from `from` to `to`.
79    Line { from: [f64; 2], to: [f64; 2] },
80    /// The arc about `centre` of `radius`, from angle `start` to angle `end`
81    /// (radians; `end < start` runs clockwise). A full circle is `0 → 2π`.
82    Arc { centre: [f64; 2], radius: f64, start: f64, end: f64 },
83}
84
85impl OutlinePiece {
86    fn point(&self, s: f64) -> [f64; 2] {
87        match *self {
88            OutlinePiece::Line { from, to } => {
89                [from[0] + (to[0] - from[0]) * s, from[1] + (to[1] - from[1]) * s]
90            }
91            OutlinePiece::Arc { centre, radius, start, end } => {
92                let angle = start + (end - start) * s;
93                [centre[0] + radius * angle.cos(), centre[1] + radius * angle.sin()]
94            }
95        }
96    }
97
98    fn is_full_circle(&self) -> bool {
99        matches!(*self, OutlinePiece::Arc { start, end, .. } if ((end - start).abs() - TAU).abs() < 1e-12)
100    }
101
102    /// Unsigned distance from `p` to the piece.
103    fn distance(&self, p: [f64; 2]) -> f64 {
104        match *self {
105            OutlinePiece::Line { from, to } => {
106                let d = [to[0] - from[0], to[1] - from[1]];
107                let length2 = d[0] * d[0] + d[1] * d[1];
108                let t = if length2 > 0.0 {
109                    (((p[0] - from[0]) * d[0] + (p[1] - from[1]) * d[1]) / length2).clamp(0.0, 1.0)
110                } else {
111                    0.0
112                };
113                let q = [from[0] + d[0] * t - p[0], from[1] + d[1] * t - p[1]];
114                q[0].hypot(q[1])
115            }
116            OutlinePiece::Arc { centre, radius, start, end } => {
117                let v = [p[0] - centre[0], p[1] - centre[1]];
118                let angle = v[1].atan2(v[0]);
119                if self.is_full_circle() || angle_within(angle, start, end) {
120                    (v[0].hypot(v[1]) - radius).abs()
121                } else {
122                    let a = self.point(0.0);
123                    let b = self.point(1.0);
124                    (a[0] - p[0]).hypot(a[1] - p[1]).min((b[0] - p[0]).hypot(b[1] - p[1]))
125                }
126            }
127        }
128    }
129
130    /// Crossings of the ray from `p` towards +x with the piece (for the parity
131    /// inside test). Half-open in y so a vertex is counted once.
132    fn ray_crossings(&self, p: [f64; 2]) -> usize {
133        match *self {
134            OutlinePiece::Line { from, to } => {
135                let (a, b) = (from, to);
136                if (a[1] > p[1]) != (b[1] > p[1]) {
137                    let x = a[0] + (p[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
138                    usize::from(x > p[0])
139                } else {
140                    0
141                }
142            }
143            OutlinePiece::Arc { centre, radius, start, end } => {
144                let dy = p[1] - centre[1];
145                if dy.abs() >= radius {
146                    return 0;
147                }
148                let dx = (radius * radius - dy * dy).sqrt();
149                let mut count = 0;
150                for x in [centre[0] - dx, centre[0] + dx] {
151                    if x <= p[0] {
152                        continue;
153                    }
154                    let angle = dy.atan2(x - centre[0]);
155                    if self.is_full_circle() || angle_within(angle, start, end) {
156                        // Half-open at the arc's own ends, like the segment rule:
157                        // count an end only where the arc leaves upwards.
158                        count += 1;
159                    }
160                }
161                count
162            }
163        }
164    }
165}
166
167/// Is `angle` on the arc that runs from `start` to `end` (either sense)?
168fn angle_within(angle: f64, start: f64, end: f64) -> bool {
169    let (low, high) = if end >= start { (start, end) } else { (end, start) };
170    let shifted = low + (angle - low).rem_euclid(TAU);
171    shifted <= high
172}
173
174fn scale_of(outline: &[OutlinePiece]) -> f64 {
175    outline
176        .iter()
177        .flat_map(|piece| [piece.point(0.0), piece.point(1.0)])
178        .map(|p| p[0].abs().max(p[1].abs()))
179        .fold(1.0f64, f64::max)
180}
181
182/// Signed distance to a closed outline (negative inside).
183fn outline_signed_distance(outline: &[OutlinePiece], p: [f64; 2]) -> f64 {
184    let distance = outline
185        .iter()
186        .map(|piece| piece.distance(p))
187        .fold(f64::INFINITY, f64::min);
188    let crossings: usize = outline.iter().map(|piece| piece.ray_crossings(p)).sum();
189    if crossings % 2 == 1 {
190        -distance
191    } else {
192        distance
193    }
194}
195
196fn validate_outline(outline: &[OutlinePiece], what: &str) -> Result<(), KernelRefusal> {
197    if outline.is_empty() {
198        return Err(KernelRefusal::input(
199            KernelStage::Collect,
200            "outline_empty",
201            format!("sweepSolidWithin: the {what} outline is empty"),
202        ));
203    }
204    let scale = outline
205        .iter()
206        .flat_map(|piece| [piece.point(0.0), piece.point(1.0)])
207        .map(|p| p[0].abs().max(p[1].abs()))
208        .fold(1.0f64, f64::max);
209    for (index, piece) in outline.iter().enumerate() {
210        let next = &outline[(index + 1) % outline.len()];
211        let a = piece.point(1.0);
212        let b = next.point(0.0);
213        if (a[0] - b[0]).hypot(a[1] - b[1]) > 1e-9 * scale {
214            return Err(KernelRefusal::input(
215                KernelStage::Collect,
216                "outline_open",
217                format!("sweepSolidWithin: the {what} outline is not closed after piece {index}"),
218            ));
219        }
220    }
221    Ok(())
222}
223
224/// The moving tool.
225#[derive(Clone, Debug)]
226pub enum SweptTool {
227    /// A solid of revolution: the region `meridian` bounds in the half-plane
228    /// spanned by `radial` (first coordinate, the distance from the axis, ≥ 0)
229    /// and `axis_direction` (second coordinate), turned about the axis through
230    /// `axis_origin`.
231    Revolved {
232        axis_origin: Vec3,
233        axis_direction: Vec3,
234        radial: Vec3,
235        meridian: Vec<OutlinePiece>,
236    },
237    /// A prism: the region `outline` bounds in the plane through `origin`
238    /// spanned by `x_axis` and `normal × x_axis`, extended along `normal` from
239    /// `from` to `to`.
240    Prism {
241        origin: Vec3,
242        normal: Vec3,
243        x_axis: Vec3,
244        outline: Vec<OutlinePiece>,
245        from: f64,
246        to: f64,
247    },
248}
249
250/// One elementary motion, linear in the motion parameter `t`: a turn of
251/// `turn·t` radians, right-handed about the axis through `axis_origin` along
252/// `axis_direction`, together with an advance of `advance·t` along it. A turn
253/// alone is `advance = 0`, a slide alone is `turn = 0`.
254#[derive(Clone, Copy, Debug)]
255pub struct ScrewStep {
256    pub axis_origin: Vec3,
257    pub axis_direction: Vec3,
258    pub turn: f64,
259    pub advance: f64,
260}
261
262/// A rigid motion: the steps applied in order (the first to the tool first),
263/// for `t` from `from` to `to`.
264#[derive(Clone, Debug)]
265pub struct SolidMotion {
266    pub steps: Vec<ScrewStep>,
267    pub from: f64,
268    pub to: f64,
269}
270
271/// The region the result must agree with the sweep in, about an axis: levels
272/// (distance from the axis) in `radius`, axial positions in `axial` (measured
273/// from `axis_origin`), and polar angles in `angle` (from `reference`,
274/// right-handed about the axis), when given.
275#[derive(Clone, Copy, Debug)]
276pub struct SweepClip {
277    pub axis_origin: Vec3,
278    pub axis_direction: Vec3,
279    pub reference: Vec3,
280    pub radius: [f64; 2],
281    pub axial: [f64; 2],
282    pub angle: Option<[f64; 2]>,
283}
284
285/// The prefix of every refusal of this operation.
286pub const SOLID_SWEEP_REFUSAL: &str = "sweepSolidWithin";
287
288/// Words of the refusal for a level the sweep covers at every angle.
289const WHOLE_LEVEL: &str = "the sweep covers every angle at level";
290
291/// Turns of a thread each row of faces carries (measured: shorter rows cost more
292/// meshing, longer ones more edge reading).
293const TURNS_PER_ROW: f64 = 2.0;
294
295/// `BREP_SWEEP_THREAD_ROWS` (default on; `=0` one body-long face per wall and the
296/// boolean for the clip): a thread is built a few turns at a time, since readings
297/// over a face cost the square of its length.
298fn thread_rows_on() -> bool {
299    crate::switch!("BREP_SWEEP_THREAD_ROWS", true)
300}
301
302/// `BREP_SWEEP_WHOLE_LEVEL` (default on; `=0` restores the refusal): a screw's
303/// sweep whose turns meet at a crest, built as the clip less what stays.
304fn whole_level_on() -> bool {
305    crate::switch!("BREP_SWEEP_WHOLE_LEVEL", true)
306}
307
308fn refuse(what: &'static str, message: String) -> KernelRefusal {
309    KernelRefusal::unsupported(KernelStage::Classify, what, format!("{SOLID_SWEEP_REFUSAL}: {message}"))
310}
311
312fn rotate(x: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Vec3 {
313    origin.add(x.sub(origin).rotate_about(axis, angle))
314}
315
316impl ScrewStep {
317    fn apply(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
318        rotate(x, self.axis_origin, axis, self.turn * t).add(axis.scale(self.advance * t))
319    }
320    fn invert(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
321        rotate(x.sub(axis.scale(self.advance * t)), self.axis_origin, axis, -self.turn * t)
322    }
323}
324
325/// Orthonormal frame of the clip: (ex, ey, ez).
326#[derive(Clone, Copy, Debug)]
327struct Frame {
328    origin: Vec3,
329    ex: Vec3,
330    ey: Vec3,
331    ez: Vec3,
332}
333
334impl Frame {
335    fn local(&self, x: Vec3) -> [f64; 3] {
336        let d = x.sub(self.origin);
337        [d.dot(self.ex), d.dot(self.ey), d.dot(self.ez)]
338    }
339    fn world(&self, p: [f64; 3]) -> Vec3 {
340        self.origin
341            .add(self.ex.scale(p[0]))
342            .add(self.ey.scale(p[1]))
343            .add(self.ez.scale(p[2]))
344    }
345}
346
347#[derive(Clone, Copy, Debug, PartialEq)]
348enum Class {
349    /// Axial advance per radian of the screw, in the clip frame (nonzero).
350    Screw(f64),
351    Turn,
352    Planar,
353}
354
355/// A tool-boundary source: a face patch over a 2D parameter box, or an edge
356/// over a 1D parameter interval. `periodic` marks a parameter that wraps.
357#[derive(Clone, Copy, Debug)]
358enum Source {
359    /// Revolved meridian piece `piece`, parameters (s ∈ [0,1], φ ∈ [0,2π)).
360    RevolvedFace { piece: usize },
361    /// Revolved meridian corner at the start of piece `piece`, parameter φ.
362    RevolvedCorner { piece: usize },
363    /// Prism outline piece `piece` carried through time: (s ∈ [0,1], t).
364    PrismFace { piece: usize },
365    /// Prism outline vertex at the start of piece `piece`, parameter t.
366    PrismVertex { piece: usize },
367    /// Prism outline piece `piece` at the motion's first (`end = false`) or last
368    /// position, parameter s.
369    PrismEnd { piece: usize, end: bool },
370    /// Under a screw, at the tool's own place (the screw's reduction is invariant
371    /// along it): side face of outline piece `piece`, (s ∈ [0,1], height).
372    ScrewPrismSide { piece: usize },
373    /// Under a screw: the upright edge at the start of piece `piece`, parameter height.
374    ScrewPrismEdge { piece: usize },
375    /// Under a screw: outline piece `piece` at the prism's low (`end = false`) or
376    /// high end, parameter s. An end cap itself maps one-to-one into the reduced
377    /// plane (polar coordinates at a fixed height), so only its rim can bound.
378    ScrewPrismRim { piece: usize, end: bool },
379}
380
381struct Engine<'a> {
382    tool: &'a SweptTool,
383    motion: &'a SolidMotion,
384    axes: Vec<Vec3>,
385    frame: Frame,
386    class: Class,
387    clip: &'a SweepClip,
388    /// The clip was narrowed to a crest level: below it some material is left.
389    crest_gap: bool,
390    sources: Vec<Source>,
391    /// Per face source: its parameter grid (nu × nv) of reduced coordinates.
392    grids: Vec<Option<Grid>>,
393    /// Per source: its critical points of the level, as (level, o).
394    critical: Vec<Vec<(f64, f64)>>,
395    /// Per face source: the parameters of its critical points (tips), which
396    /// seed the extremum Newton where the level's contour is smaller than a cell.
397    critical_params: Vec<Vec<(f64, f64)>>,
398    /// Angular `o` (wraps at 2π) or not.
399    angular: bool,
400    /// Prism height (along its normal) the planar sources are evaluated at.
401    prism_height: f64,
402    scale: f64,
403    /// `intervals_at` by level bits: the sampling revisits levels (a refined
404    /// band keeps every level of the coarser one).
405    cache: std::cell::RefCell<std::collections::HashMap<u64, Vec<Interval>>>,
406    /// The motion ends where it began as a set (the plus wheel: one turn of the
407    /// wheel, one and a half of the tool), so there are no end positions; piece
408    /// `k` at the end continues as piece `wrap[k]` at the start.
409    wrap: Option<Vec<usize>>,
410    /// Debug counters: (levels evaluated, oracle calls, oracle steps, seconds in
411    /// candidates, seconds in the oracle).
412    counters: std::cell::Cell<(usize, usize, usize, f64, f64)>,
413}
414
415struct Grid {
416    nu: usize,
417    nv: usize,
418    u: [f64; 2],
419    v: [f64; 2],
420    v_periodic: bool,
421    values: Vec<(f64, f64)>,
422}
423
424/// One interval of the region at a level, with the sources of its ends.
425#[derive(Clone, Copy, Debug)]
426struct Interval {
427    lo: f64,
428    hi: f64,
429    lo_source: usize,
430    hi_source: usize,
431}
432
433fn wrap_pi(angle: f64) -> f64 {
434    let wrapped = (angle + PI).rem_euclid(TAU) - PI;
435    wrapped
436}
437
438impl<'a> Engine<'a> {
439    fn motion_apply(&self, t: f64, x: Vec3) -> Vec3 {
440        self.motion
441            .steps
442            .iter()
443            .zip(&self.axes)
444            .fold(x, |x, (step, axis)| step.apply(t, x, *axis))
445    }
446
447    fn motion_invert(&self, t: f64, x: Vec3) -> Vec3 {
448        self.motion
449            .steps
450            .iter()
451            .zip(&self.axes)
452            .rev()
453            .fold(x, |x, (step, axis)| step.invert(t, x, *axis))
454    }
455
456    /// Reduced coordinates of a clip-frame point.
457    fn reduce(&self, p: [f64; 3]) -> (f64, f64) {
458        let level = p[0].hypot(p[1]);
459        match self.class {
460            Class::Screw(c) => (level, p[1].atan2(p[0]) - p[2] / c),
461            Class::Turn => (level, p[2]),
462            Class::Planar => (level, p[1].atan2(p[0])),
463        }
464    }
465
466    fn tool_signed_distance(&self, x: Vec3) -> f64 {
467        match self.tool {
468            SweptTool::Revolved { axis_origin, axis_direction, meridian, .. } => {
469                let axis = axis_direction.normalized_or(*axis_direction);
470                let d = x.sub(*axis_origin);
471                let e = d.dot(axis);
472                let radial = d.sub(axis.scale(e)).length();
473                outline_signed_distance(meridian, [radial, e])
474            }
475            SweptTool::Prism { origin, normal, x_axis, outline, from, to } => {
476                let n = normal.normalized_or(*normal);
477                let xa = x_axis.normalized_or(*x_axis);
478                let ya = n.cross(xa);
479                let d = x.sub(*origin);
480                let h = d.dot(n);
481                let planar = outline_signed_distance(outline, [d.dot(xa), d.dot(ya)]);
482                planar.max(from - h).max(h - to)
483            }
484        }
485    }
486
487    /// The world point of a source at its parameters (at the tool's own place
488    /// for revolved tools; carried by the motion for prisms).
489    fn source_point(&self, source: Source, u: f64, v: f64) -> Vec3 {
490        match (self.tool, source) {
491            (SweptTool::Revolved { axis_origin, axis_direction, radial, meridian }, _) => {
492                let axis = axis_direction.normalized_or(*axis_direction);
493                let r0 = radial.sub(axis.scale(radial.dot(axis))).normalized_or(*radial);
494                let r1 = axis.cross(r0);
495                let (m, phi) = match source {
496                    Source::RevolvedFace { piece } => (meridian[piece].point(u), v),
497                    Source::RevolvedCorner { piece } => (meridian[piece].point(0.0), u),
498                    _ => unreachable!(),
499                };
500                axis_origin
501                    .add(r0.scale(m[0] * phi.cos()))
502                    .add(r1.scale(m[0] * phi.sin()))
503                    .add(axis.scale(m[1]))
504            }
505            (SweptTool::Prism { origin, normal, x_axis, outline, from, to }, _) => {
506                let n = normal.normalized_or(*normal);
507                let xa = x_axis.normalized_or(*x_axis);
508                let ya = n.cross(xa);
509                let at_own_place = match source {
510                    Source::ScrewPrismSide { piece } => Some((outline[piece].point(u), v)),
511                    Source::ScrewPrismEdge { piece } => Some((outline[piece].point(0.0), u)),
512                    Source::ScrewPrismRim { piece, end } => Some((outline[piece].point(u), if end { *to } else { *from })),
513                    _ => None,
514                };
515                if let Some((q, h)) = at_own_place {
516                    return origin.add(xa.scale(q[0])).add(ya.scale(q[1])).add(n.scale(h));
517                }
518                let (q, t) = match source {
519                    Source::PrismFace { piece } => (outline[piece].point(u), v),
520                    Source::PrismVertex { piece } => (outline[piece].point(0.0), u),
521                    Source::PrismEnd { piece, end } => (
522                        outline[piece].point(u),
523                        if end { self.motion.to } else { self.motion.from },
524                    ),
525                    _ => unreachable!(),
526                };
527                let x = origin
528                    .add(xa.scale(q[0]))
529                    .add(ya.scale(q[1]))
530                    .add(n.scale(self.prism_height));
531                self.motion_apply(t, x)
532            }
533        }
534    }
535
536    fn source_reduced(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
537        self.reduce(self.frame.local(self.source_point(source, u, v)))
538    }
539
540    fn is_face(source: Source) -> bool {
541        matches!(source, Source::RevolvedFace { .. } | Source::PrismFace { .. } | Source::ScrewPrismSide { .. })
542    }
543
544    /// Parameter domain of a source: (u range, v range, v periodic).
545    fn domain(&self, source: Source) -> ([f64; 2], [f64; 2], bool) {
546        let time = [self.motion.from, self.motion.to];
547        match source {
548            Source::RevolvedFace { .. } => ([0.0, 1.0], [0.0, TAU], true),
549            Source::RevolvedCorner { .. } => ([0.0, TAU], [0.0, 0.0], false),
550            Source::PrismFace { .. } => ([0.0, 1.0], time, false),
551            Source::PrismVertex { .. } => (time, [0.0, 0.0], false),
552            Source::PrismEnd { .. } => ([0.0, 1.0], [0.0, 0.0], false),
553            Source::ScrewPrismSide { .. } => ([0.0, 1.0], self.prism_extent(), false),
554            Source::ScrewPrismEdge { .. } => (self.prism_extent(), [0.0, 0.0], false),
555            Source::ScrewPrismRim { .. } => ([0.0, 1.0], [0.0, 0.0], false),
556        }
557    }
558
559    fn prism_extent(&self) -> [f64; 2] {
560        match self.tool {
561            SweptTool::Prism { from, to, .. } => [*from, *to],
562            SweptTool::Revolved { .. } => [0.0, 0.0],
563        }
564    }
565
566    /// Is the face's first parameter periodic (a full circle in the meridian or
567    /// outline, which has no corner to bound it)?
568    fn u_periodic(&self, source: Source) -> bool {
569        match (self.tool, source) {
570            (SweptTool::Revolved { meridian, .. }, Source::RevolvedFace { piece }) => meridian[piece].is_full_circle(),
571            (SweptTool::Prism { outline, .. }, Source::PrismFace { piece } | Source::ScrewPrismSide { piece }) => outline[piece].is_full_circle(),
572            _ => false,
573        }
574    }
575
576    fn o_difference(&self, a: f64, b: f64) -> f64 {
577        if self.angular {
578            wrap_pi(a - b)
579        } else {
580            a - b
581        }
582    }
583
584    /// Newton on {ℓ = level, det(∇o, ∇ℓ) = 0} over a face source.
585    fn face_extremum(&self, source: Source, level: f64, mut u: f64, mut v: f64) -> Option<(f64, f64, f64)> {
586        let (ur, vr, periodic) = self.domain(source);
587        let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
588        let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
589        let residual = |u: f64, v: f64| -> [f64; 2] {
590            let (l, o) = self.source_reduced(source, u, v);
591            let (lu1, ou1) = self.source_reduced(source, u + hu, v);
592            let (lu0, ou0) = self.source_reduced(source, u - hu, v);
593            let (lv1, ov1) = self.source_reduced(source, u, v + hv);
594            let (lv0, ov0) = self.source_reduced(source, u, v - hv);
595            let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
596            let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
597            let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
598            // det scaled by the level's gradient so its size is that of ∇o.
599            let norm = lu.hypot(lv).max(1e-300);
600            let _ = o;
601            [l - level, (ou * lv - ov * lu) / norm]
602        };
603        let mut f = residual(u, v);
604        for _ in 0..60 {
605            let (du_step, dv_step) = (hu * 10.0, hv * 10.0);
606            let fu1 = residual(u + du_step, v);
607            let fu0 = residual(u - du_step, v);
608            let fv1 = residual(u, v + dv_step);
609            let fv0 = residual(u, v - dv_step);
610            let j = [
611                [(fu1[0] - fu0[0]) / (2.0 * du_step), (fv1[0] - fv0[0]) / (2.0 * dv_step)],
612                [(fu1[1] - fu0[1]) / (2.0 * du_step), (fv1[1] - fv0[1]) / (2.0 * dv_step)],
613            ];
614            let det = j[0][0] * j[1][1] - j[0][1] * j[1][0];
615            if !det.is_finite() || det.abs() < 1e-300 {
616                return None;
617            }
618            let du = -(f[0] * j[1][1] - f[1] * j[0][1]) / det;
619            let dv = -(j[0][0] * f[1] - j[1][0] * f[0]) / det;
620            let norm0 = f[0].hypot(f[1]);
621            let mut step = 1.0;
622            let mut accepted = false;
623            while step > 1e-4 {
624                let (nu, nv) = (u + step * du, v + step * dv);
625                let candidate = residual(nu, nv);
626                if candidate[0].hypot(candidate[1]) < norm0 || norm0 < 1e-14 * self.scale {
627                    u = nu;
628                    v = nv;
629                    f = candidate;
630                    accepted = true;
631                    break;
632                }
633                step *= 0.5;
634            }
635            if !accepted {
636                break;
637            }
638            if (du * step).abs() < 1e-15 * (ur[1] - ur[0]) && (dv * step).abs() < 1e-15 * (vr[1] - vr[0]).max(1.0) {
639                break;
640            }
641        }
642        if periodic {
643            v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
644        }
645        if self.u_periodic(source) {
646            u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
647        }
648        let tolerance = 1e-9;
649        if u < ur[0] - tolerance || u > ur[1] + tolerance || (!periodic && (v < vr[0] - tolerance || v > vr[1] + tolerance)) {
650            return None;
651        }
652        if f[0].abs() > 1e-10 * self.scale || f[1].abs() > 1e-6 {
653            return None;
654        }
655        let (l, o) = self.source_reduced(source, u.clamp(ur[0], ur[1]), v);
656        Some((l, o, u))
657    }
658
659    fn build_grid(&self, source: Source) -> Grid {
660        let (u, v, v_periodic) = self.domain(source);
661        let (nu, nv) = match source {
662            Source::RevolvedFace { .. } => (64, 256),
663            Source::ScrewPrismSide { .. } => (64, 64),
664            _ => {
665                let turn = self
666                    .motion
667                    .steps
668                    .iter()
669                    .map(|step| step.turn.abs())
670                    .fold(0.0, f64::max)
671                    * (self.motion.to - self.motion.from).abs();
672                (48, ((turn * 128.0) as usize).clamp(256, 4096))
673            }
674        };
675        let mut values = Vec::with_capacity(nu * nv);
676        for i in 0..nu {
677            let uu = u[0] + (u[1] - u[0]) * i as f64 / (nu - 1) as f64;
678            for j in 0..nv {
679                let vv = if v_periodic {
680                    v[0] + (v[1] - v[0]) * j as f64 / nv as f64
681                } else {
682                    v[0] + (v[1] - v[0]) * j as f64 / (nv - 1) as f64
683                };
684                values.push(self.source_reduced(source, uu, vv));
685            }
686        }
687        Grid { nu, nv, u, v, v_periodic, values }
688    }
689
690    fn grid_param(grid: &Grid, i: usize, j: usize) -> (f64, f64) {
691        let u = grid.u[0] + (grid.u[1] - grid.u[0]) * i as f64 / (grid.nu - 1) as f64;
692        let v = if grid.v_periodic {
693            grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / grid.nv as f64
694        } else {
695            grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / (grid.nv - 1) as f64
696        };
697        (u, v)
698    }
699
700    /// Candidates of one source at a level: (o, source index).
701    fn source_candidates(&self, index: usize, level: f64, out: &mut Vec<(f64, usize)>) {
702        let source = self.sources[index];
703        if Self::is_face(source) {
704            let grid = self.grids[index].as_ref().expect("face grid");
705            // Crossings of the level along grid edges, then seeds where o is a
706            // local extremum among the crossings of neighbouring cells.
707            let at = |i: usize, j: usize| grid.values[i * grid.nv + j % grid.nv];
708            let jmax = if grid.v_periodic { grid.nv } else { grid.nv - 1 };
709            let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
710            for i in 0..grid.nu {
711                for j in 0..jmax {
712                    let a = at(i, j);
713                    let b = at(i, j + 1);
714                    if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
715                        let w = (level - a.0) / (b.0 - a.0);
716                        let o = a.1 + self.o_difference(b.1, a.1) * w;
717                        let (u, v0) = Self::grid_param(grid, i, j);
718                        let (_, v1) = Self::grid_param(grid, i, j + 1);
719                        let v1 = if grid.v_periodic && j + 1 == grid.nv { grid.v[1] } else { v1 };
720                        crossings.push((i, j, o, u, v0 + (v1 - v0) * w));
721                    }
722                }
723            }
724            for i in 0..grid.nu.saturating_sub(1) {
725                for j in 0..grid.nv {
726                    let a = at(i, j);
727                    let b = at(i + 1, j);
728                    if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
729                        let w = (level - a.0) / (b.0 - a.0);
730                        let o = a.1 + self.o_difference(b.1, a.1) * w;
731                        let (u0, v) = Self::grid_param(grid, i, j);
732                        let (u1, _) = Self::grid_param(grid, i + 1, j);
733                        crossings.push((i, j, o, u0 + (u1 - u0) * w, v));
734                    }
735                }
736            }
737            let mut found: Vec<(f64, f64, f64)> = Vec::new();
738            let push = |found: &mut Vec<(f64, f64, f64)>, result: Option<(f64, f64, f64)>| {
739                if let Some((_, o_new, u_new)) = result {
740                    if !found
741                        .iter()
742                        .any(|&(o_old, u_old, _)| self.o_difference(o_old, o_new).abs() < 1e-11 && (u_old - u_new).abs() < 1e-6)
743                    {
744                        found.push((o_new, u_new, 0.0));
745                    }
746                }
747            };
748            // Near a tip the contour can be smaller than a cell: seed from the
749            // tip itself, pushed off it in four directions.
750            if let Some(params) = self.critical_params.get(index) {
751                let (ur, vr, _) = self.domain(source);
752                for &(u, v) in params {
753                    for (du, dv) in [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)] {
754                        let (su, sv) = (u + du * 1e-3 * (ur[1] - ur[0]), v + dv * 1e-3 * (vr[1] - vr[0]));
755                        push(&mut found, self.face_extremum(source, level, su, sv));
756                    }
757                }
758            }
759            let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
760            for (k, &(i, j, _, _, _)) in crossings.iter().enumerate() {
761                buckets.entry((i, j)).or_default().push(k);
762            }
763            for (k, &(i, j, o, u, v)) in crossings.iter().enumerate() {
764                let mut is_max = true;
765                let mut is_min = true;
766                let mut neighbours = Vec::new();
767                for di in [-1i64, 0, 1] {
768                    for dj in [-1i64, 0, 1] {
769                        let i2 = i as i64 + di;
770                        if i2 < 0 {
771                            continue;
772                        }
773                        let j2 = j as i64 + dj;
774                        let j2 = if grid.v_periodic { j2.rem_euclid(grid.nv as i64) } else { j2 };
775                        if j2 < 0 {
776                            continue;
777                        }
778                        if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
779                            neighbours.extend(list.iter().copied());
780                        }
781                    }
782                }
783                for m in neighbours {
784                    if m == k {
785                        continue;
786                    }
787                    let o2 = crossings[m].2;
788                    {
789                        let d = self.o_difference(o2, o);
790                        if d > 0.0 {
791                            is_max = false;
792                        }
793                        if d < 0.0 {
794                            is_min = false;
795                        }
796                    }
797                }
798                if !(is_max || is_min) {
799                    continue;
800                }
801                push(&mut found, self.face_extremum(source, level, u, v));
802            }
803            out.extend(found.into_iter().map(|(o, _, _)| (o, index)));
804        } else {
805            let (range, _, _) = self.domain(source);
806            let periodic = matches!(source, Source::RevolvedCorner { .. });
807            let n = match source {
808                Source::RevolvedCorner { .. } | Source::PrismEnd { .. } | Source::ScrewPrismEdge { .. } | Source::ScrewPrismRim { .. } => 1024,
809                _ => self.grids_len_for_time(),
810            };
811            let _ = periodic;
812            // The uniform samples, with the curve's own level extrema merged in:
813            // near a tip the two roots of `ℓ = ℓ₀` fall either side of the extremum,
814            // closer than any fixed step, and would be missed together.
815            let mut samples: Vec<f64> = (0..=n).map(|k| range[0] + (range[1] - range[0]) * k as f64 / n as f64).collect();
816            if let Some(extra) = self.critical_params.get(index) {
817                samples.extend(extra.iter().map(|p| p.0).filter(|t| *t > range[0] && *t < range[1]));
818                samples.sort_by(|a, b| a.total_cmp(b));
819            }
820            let mut previous = self.source_reduced(source, samples[0], 0.0).0 - level;
821            for k in 1..samples.len() {
822                let p = samples[k];
823                let value = self.source_reduced(source, p, 0.0).0 - level;
824                if previous == 0.0 {
825                    // A root exactly on the sample (a band's end level on the
826                    // curve's own end): bisecting from a zero walks to the far
827                    // end of the bracket, a whole sample step away.
828                    out.push((self.source_reduced(source, samples[k - 1], 0.0).1, index));
829                } else if previous * value < 0.0 {
830                    let (mut a, mut b) = (samples[k - 1], p);
831                    let fa0 = previous;
832                    for _ in 0..200 {
833                        let m = 0.5 * (a + b);
834                        let fm = self.source_reduced(source, m, 0.0).0 - level;
835                        if (fm < 0.0) == (fa0 < 0.0) && fm != 0.0 {
836                            a = m;
837                        } else {
838                            b = m;
839                        }
840                        if (b - a).abs() < 1e-15 * (range[1] - range[0]).abs().max(1.0) {
841                            break;
842                        }
843                    }
844                    let (_, o) = self.source_reduced(source, 0.5 * (a + b), 0.0);
845                    out.push((o, index));
846                }
847                previous = value;
848            }
849            if previous == 0.0 {
850                let last = *samples.last().expect("nonempty");
851                out.push((self.source_reduced(source, last, 0.0).1, index));
852            }
853        }
854    }
855
856    fn grids_len_for_time(&self) -> usize {
857        let turn = self
858            .motion
859            .steps
860            .iter()
861            .map(|step| step.turn.abs())
862            .fold(0.0, f64::max)
863            * (self.motion.to - self.motion.from).abs();
864        ((turn * 256.0) as usize).clamp(1024, 8192)
865    }
866
867    /// The clip-frame point a reduced coordinate is tested at.
868    fn lift(&self, level: f64, o: f64) -> [f64; 3] {
869        let axial = 0.5 * (self.clip.axial[0] + self.clip.axial[1]);
870        match self.class {
871            Class::Screw(c) => {
872                let alpha = o + axial / c;
873                [level * alpha.cos(), level * alpha.sin(), axial]
874            }
875            Class::Turn => {
876                let theta = self.clip.angle.map(|a| 0.5 * (a[0] + a[1])).unwrap_or(0.0);
877                [level * theta.cos(), level * theta.sin(), o]
878            }
879            Class::Planar => [level * o.cos(), level * o.sin(), axial],
880        }
881    }
882
883    /// Is the reduced point inside the sweep: does the point's preimage under
884    /// the motion meet the tool for some time in the range?
885    ///
886    /// Walked in time by the tool's signed distance: it bounds how far the
887    /// preimage is from the tool, and the preimage moves no faster than
888    /// `speed_bound`, so no time within `distance / speed` of the current one can
889    /// be inside. The walk is exact for the tool's own distance (a lower bound for
890    /// a prism, which is conservative), and the floor on the step only stops a
891    /// grazing walk from creeping.
892    fn removed(&self, level: f64, o: f64) -> bool {
893        let x = self.frame.world(self.lift(level, o));
894        let (t0, t1) = (self.motion.from, self.motion.to);
895        let speed = self.speed_bound(x);
896        // The floor only keeps a grazing walk from creeping; a coarse floor
897        // jumps over a shallow pass of the tool (1e-5 of the plus wheel's range
898        // is 0.02 mm of travel, and read a 4e-3 mm sliver of its tooth space as
899        // material). A walk still grazing after `cap` steps is on the boundary.
900        let floor = (t1 - t0) * 1e-8;
901        let cap = 200_000usize;
902        let touching = 1e-10 * self.scale;
903        let clock = web_time::Instant::now();
904        let mut t = t0;
905        let mut steps = 0usize;
906        let inside = loop {
907            steps += 1;
908            let distance = self.tool_signed_distance(self.motion_invert(t, x));
909            if distance <= touching || steps > cap {
910                break true;
911            }
912            if t >= t1 {
913                break false;
914            }
915            t = (t + (distance / speed).max(floor)).min(t1);
916        };
917        let mut c = self.counters.get();
918        c.1 += 1;
919        c.2 += steps;
920        c.4 += clock.elapsed().as_secs_f64();
921        self.counters.set(c);
922        inside
923    }
924
925    /// An upper bound on how fast the preimage `M(t)⁻¹ x` moves per unit `t`.
926    fn speed_bound(&self, x: Vec3) -> f64 {
927        let centre = self.frame.origin;
928        let span = self.motion.from.abs().max(self.motion.to.abs());
929        let mut reach = x.sub(centre).length();
930        for step in &self.motion.steps {
931            reach += 2.0 * step.axis_origin.sub(centre).length() + step.advance.abs() * span;
932        }
933        let speed: f64 = self
934            .motion
935            .steps
936            .iter()
937            .map(|step| step.turn.abs() * (reach + step.axis_origin.sub(centre).length()) + step.advance.abs())
938            .sum();
939        speed.max(1e-300)
940    }
941
942    /// The region's intervals at a level, sorted by `lo` (angular: `lo` in
943    /// [0, 2π), `hi` may exceed 2π).
944    fn intervals_at(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
945        if let Some(hit) = self.cache.borrow().get(&level.to_bits()) {
946            return Ok(hit.clone());
947        }
948        let clock = web_time::Instant::now();
949        let before = self.counters.get();
950        let intervals = self.intervals_uncached(level)?;
951        if clock.elapsed().as_secs_f64() > 0.5 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
952            let after = self.counters.get();
953            eprintln!(
954                "  slow level {level:.12}: {:.2}s, candidates {:.2}s, oracle calls {} steps {} ({:.2}s), {} intervals",
955                clock.elapsed().as_secs_f64(),
956                after.3 - before.3,
957                after.1 - before.1,
958                after.2 - before.2,
959                after.4 - before.4,
960                intervals.len()
961            );
962        }
963        self.cache.borrow_mut().insert(level.to_bits(), intervals.clone());
964        Ok(intervals)
965    }
966
967    fn intervals_uncached(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
968        let clock = web_time::Instant::now();
969        let mut candidates = Vec::new();
970        for index in 0..self.sources.len() {
971            self.source_candidates(index, level, &mut candidates);
972        }
973        let mut c = self.counters.get();
974        c.0 += 1;
975        c.3 += clock.elapsed().as_secs_f64();
976        self.counters.set(c);
977        if self.angular {
978            for candidate in &mut candidates {
979                candidate.0 = candidate.0.rem_euclid(TAU);
980            }
981        }
982        candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
983        candidates.dedup_by(|b, a| (b.0 - a.0).abs() < 1e-12);
984        if candidates.is_empty() {
985            let probes = 64;
986            let (base, span) = if self.angular { (0.0, TAU) } else { (-self.scale * 4.0, self.scale * 8.0) };
987            if (0..probes).all(|k| self.removed(level, base + span * (k as f64 + 0.5) / probes as f64)) {
988                return Err(refuse(
989                    "whole_level",
990                    format!("{WHOLE_LEVEL} {level}, which this construction does not build"),
991                ));
992            }
993            return Ok(Vec::new());
994        }
995        let n = candidates.len();
996        let gaps = if self.angular { n } else { n - 1 };
997        let mut removed = Vec::with_capacity(gaps);
998        for k in 0..gaps {
999            let a = candidates[k].0;
1000            let b = if k + 1 < n { candidates[k + 1].0 } else { candidates[0].0 + TAU };
1001            // A gap far below any feature is the two ends of one boundary
1002            // point; its middle is on the boundary, where the oracle can only
1003            // graze. It joins whatever is on either side.
1004            if (b - a) * level.max(1.0) < 1e-9 * self.scale {
1005                removed.push(true);
1006                continue;
1007            }
1008            removed.push(self.removed(level, 0.5 * (a + b)));
1009        }
1010        let mut intervals = Vec::new();
1011        // Below a crest some material is left, however narrow: the widest gap,
1012        // which the oracle can no longer see inside.
1013        if self.crest_gap && self.angular && removed.iter().all(|r| *r) {
1014            let width = |k: usize| if k + 1 < n { candidates[k + 1].0 - candidates[k].0 } else { candidates[0].0 + TAU - candidates[k].0 };
1015            if let Some(widest) = (0..gaps).max_by(|&a, &b| width(a).total_cmp(&width(b))) {
1016                removed[widest] = false;
1017            }
1018        }
1019        if self.angular && removed.iter().all(|r| *r) {
1020            return Err(refuse(
1021                "whole_level",
1022                format!("{WHOLE_LEVEL} {level}, which this construction does not build"),
1023            ));
1024        }
1025        // Start the walk after a material gap so a run never straddles the start.
1026        let start = if self.angular {
1027            removed.iter().position(|r| !r).map(|k| (k + 1) % n).unwrap_or(0)
1028        } else {
1029            0
1030        };
1031        let mut k = 0;
1032        while k < gaps {
1033            let g = (start + k) % n.max(1);
1034            if !removed[g] {
1035                k += 1;
1036                continue;
1037            }
1038            let first = g;
1039            let mut last = g;
1040            while k + 1 < gaps && removed[(start + k + 1) % n] {
1041                k += 1;
1042                last = (start + k) % n;
1043            }
1044            let lo = candidates[first];
1045            let hi_index = (last + 1) % n;
1046            let hi = candidates[hi_index];
1047            let mut hi_value = hi.0;
1048            if self.angular && hi_value <= lo.0 {
1049                hi_value += TAU;
1050            }
1051            intervals.push(Interval {
1052                lo: lo.0,
1053                hi: hi_value,
1054                lo_source: lo.1,
1055                hi_source: hi.1,
1056            });
1057            k += 1;
1058        }
1059        intervals.sort_by(|a, b| a.lo.total_cmp(&b.lo));
1060        Ok(intervals)
1061    }
1062
1063    /// Critical points of the level over a source (tips and plateaus).
1064    /// The face's characteristic function: `det(∇o, ∇ℓ) / |∇ℓ|`, zero where
1065    /// the face's image turns back (its envelope), and the level there.
1066    fn characteristic(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
1067        let (ur, vr, _) = self.domain(source);
1068        let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
1069        let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
1070        let (l, _) = self.source_reduced(source, u, v);
1071        let (lu1, ou1) = self.source_reduced(source, u + hu, v);
1072        let (lu0, ou0) = self.source_reduced(source, u - hu, v);
1073        let (lv1, ov1) = self.source_reduced(source, u, v + hv);
1074        let (lv0, ov0) = self.source_reduced(source, u, v - hv);
1075        let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
1076        let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
1077        let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
1078        ((ou * lv - ov * lu) / lu.hypot(lv).max(1e-300), l)
1079    }
1080
1081    /// FOLDS and KINKS of a face, recorded as its critical points so events snap
1082    /// to them and band ends read them exactly:
1083    /// - a FOLD is where the level is extremal along the face's own envelope
1084    ///   (`D = 0`): two extrema of `o` on one level contour annihilate there
1085    ///   (the plus wheel's lobes merging at ρ = 20, the tip of its arm's flat
1086    ///   end). Newton on {D = 0, ℓ_u·D_v − ℓ_v·D_u = 0}, seeded where the level
1087    ///   is locally extremal among the grid's crossings of D = 0.
1088    /// - a KINK is where the envelope runs into the face's own boundary and the
1089    ///   boundary's edge takes over: roots of D along each non-periodic side.
1090    fn folds_and_kinks(&self, source: Source, grid: &Grid, out: &mut Vec<(f64, f64)>, params: &mut Vec<(f64, f64)>) {
1091        let (ur, vr, v_periodic) = self.domain(source);
1092        let u_periodic = self.u_periodic(source);
1093        let (nu, nv) = (grid.nu, grid.nv);
1094        let mut d = vec![0.0; nu * nv];
1095        for i in 0..nu {
1096            for j in 0..nv {
1097                let (u, v) = Self::grid_param(grid, i, j);
1098                d[i * nv + j] = self.characteristic(source, u, v).0;
1099            }
1100        }
1101        // Crossings of D = 0 along grid edges: (i, j, u, v, level).
1102        let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
1103        let jmax = if v_periodic { nv } else { nv - 1 };
1104        for i in 0..nu {
1105            for j in 0..jmax {
1106                let (a, b) = (d[i * nv + j], d[i * nv + (j + 1) % nv]);
1107                if a * b < 0.0 {
1108                    let w = a / (a - b);
1109                    let (u, v0) = Self::grid_param(grid, i, j);
1110                    let (_, v1) = Self::grid_param(grid, i, j + 1);
1111                    let v1 = if v_periodic && j + 1 == nv { vr[1] } else { v1 };
1112                    let v = v0 + (v1 - v0) * w;
1113                    crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1114                }
1115            }
1116        }
1117        for i in 0..nu - 1 {
1118            for j in 0..nv {
1119                let (a, b) = (d[i * nv + j], d[(i + 1) * nv + j]);
1120                if a * b < 0.0 {
1121                    let w = a / (a - b);
1122                    let (u0, v) = Self::grid_param(grid, i, j);
1123                    let (u1, _) = Self::grid_param(grid, i + 1, j);
1124                    let u = u0 + (u1 - u0) * w;
1125                    crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1126                }
1127            }
1128        }
1129        let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
1130        for (k, c) in crossings.iter().enumerate() {
1131            buckets.entry((c.0, c.1)).or_default().push(k);
1132        }
1133        let ku = 1e-4 * (ur[1] - ur[0]);
1134        let kv = 1e-4 * (vr[1] - vr[0]);
1135        let fold = |u: f64, v: f64| -> [f64; 2] {
1136            let (dd, _) = self.characteristic(source, u, v);
1137            let du = (self.characteristic(source, u + ku, v).0 - self.characteristic(source, u - ku, v).0) / (2.0 * ku);
1138            let dv = (self.characteristic(source, u, v + kv).0 - self.characteristic(source, u, v - kv).0) / (2.0 * kv);
1139            let lu = (self.source_reduced(source, u + ku, v).0 - self.source_reduced(source, u - ku, v).0) / (2.0 * ku);
1140            let lv = (self.source_reduced(source, u, v + kv).0 - self.source_reduced(source, u, v - kv).0) / (2.0 * kv);
1141            let norm = (lu.hypot(lv) * du.hypot(dv)).max(1e-300);
1142            [dd, (lu * dv - lv * du) / norm]
1143        };
1144        for (k, &(i, j, u0, v0, level)) in crossings.iter().enumerate() {
1145            let mut is_min = true;
1146            let mut is_max = true;
1147            for di in [-1i64, 0, 1] {
1148                for dj in [-1i64, 0, 1] {
1149                    let i2 = i as i64 + di;
1150                    let j2 = if v_periodic { (j as i64 + dj).rem_euclid(nv as i64) } else { j as i64 + dj };
1151                    if i2 < 0 || j2 < 0 {
1152                        continue;
1153                    }
1154                    if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
1155                        for &m in list {
1156                            if m == k {
1157                                continue;
1158                            }
1159                            if crossings[m].4 < level {
1160                                is_min = false;
1161                            }
1162                            if crossings[m].4 > level {
1163                                is_max = false;
1164                            }
1165                        }
1166                    }
1167                }
1168            }
1169            if !(is_min || is_max) {
1170                continue;
1171            }
1172            let (mut u, mut v) = (u0, v0);
1173            let mut converged = false;
1174            for _ in 0..40 {
1175                let f = fold(u, v);
1176                let (su, sv) = (ku * 10.0, kv * 10.0);
1177                let fu1 = fold(u + su, v);
1178                let fu0 = fold(u - su, v);
1179                let fv1 = fold(u, v + sv);
1180                let fv0 = fold(u, v - sv);
1181                let j00 = (fu1[0] - fu0[0]) / (2.0 * su);
1182                let j01 = (fv1[0] - fv0[0]) / (2.0 * sv);
1183                let j10 = (fu1[1] - fu0[1]) / (2.0 * su);
1184                let j11 = (fv1[1] - fv0[1]) / (2.0 * sv);
1185                let det = j00 * j11 - j01 * j10;
1186                if !det.is_finite() || det.abs() < 1e-300 {
1187                    break;
1188                }
1189                let du = -(f[0] * j11 - f[1] * j01) / det;
1190                let dv = -(j00 * f[1] - j10 * f[0]) / det;
1191                // Stay near the seed: a fold is local to its crossing.
1192                let cap = 4.0 * ((ur[1] - ur[0]) / (nu as f64)).max((vr[1] - vr[0]) / (nv as f64));
1193                let scale = (du.hypot(dv) / cap).max(1.0);
1194                u += du / scale;
1195                v += dv / scale;
1196                if du.hypot(dv) < 1e-12 * (ur[1] - ur[0]).max(vr[1] - vr[0]) {
1197                    converged = true;
1198                    break;
1199                }
1200            }
1201            let f = fold(u, v);
1202            if !(converged || (f[0].abs() < 1e-9 && f[1].abs() < 1e-5)) {
1203                continue;
1204            }
1205            if u_periodic {
1206                u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1207            }
1208            if v_periodic {
1209                v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1210            }
1211            if u > ur[0] && u < ur[1] && (v_periodic || (v > vr[0] && v < vr[1])) {
1212                out.push(self.source_reduced(source, u, v));
1213                params.push((u, v));
1214            }
1215        }
1216        // Kinks: roots of D along each non-periodic side of the face.
1217        let mut sides: Vec<(bool, f64)> = Vec::new(); // (along v at fixed u, the fixed value)
1218        if !u_periodic {
1219            sides.push((true, ur[0]));
1220            sides.push((true, ur[1]));
1221        }
1222        if !v_periodic {
1223            sides.push((false, vr[0]));
1224            sides.push((false, vr[1]));
1225        }
1226        for (along_v, fixed) in sides {
1227            let (range, n) = if along_v { (vr, nv * 4) } else { (ur, nu * 4) };
1228            let at = |t: f64| if along_v { (fixed, t) } else { (t, fixed) };
1229            let value = |t: f64| {
1230                let (u, v) = at(t);
1231                self.characteristic(source, u, v).0
1232            };
1233            let mut previous = value(range[0]);
1234            for k in 1..=n {
1235                let t = range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1236                let current = value(t);
1237                if previous * current < 0.0 {
1238                    let (mut a, mut b) = (t - (range[1] - range[0]) / n as f64, t);
1239                    let fa = previous;
1240                    for _ in 0..100 {
1241                        let m = 0.5 * (a + b);
1242                        if (value(m) < 0.0) == (fa < 0.0) {
1243                            a = m;
1244                        } else {
1245                            b = m;
1246                        }
1247                    }
1248                    let (u, v) = at(0.5 * (a + b));
1249                    out.push(self.source_reduced(source, u, v));
1250                    params.push((u, v));
1251                }
1252                previous = current;
1253            }
1254        }
1255    }
1256
1257    fn source_critical(&self, index: usize) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
1258        let source = self.sources[index];
1259        let mut out = Vec::new();
1260        let mut params = Vec::new();
1261        if Self::is_face(source) {
1262            let grid = self.grids[index].as_ref().expect("face grid");
1263            let (ur, vr, periodic) = self.domain(source);
1264            let at = |i: usize, j: usize| grid.values[i * grid.nv + j].0;
1265            let u_periodic = self.u_periodic(source);
1266            // A periodic first parameter has its last row equal to its first.
1267            let rows = if u_periodic { grid.nu - 1 } else { grid.nu };
1268            for i in 0..rows {
1269                if !u_periodic && (i == 0 || i + 1 == grid.nu) {
1270                    continue;
1271                }
1272                for j in 0..grid.nv {
1273                    if !periodic && (j == 0 || j + 1 == grid.nv) {
1274                        continue;
1275                    }
1276                    let here = at(i, j);
1277                    let mut is_min = true;
1278                    let mut is_max = true;
1279                    for di in [-1i64, 0, 1] {
1280                        for dj in [-1i64, 0, 1] {
1281                            if di == 0 && dj == 0 {
1282                                continue;
1283                            }
1284                            let i2 = ((i as i64 + di).rem_euclid(rows as i64)) as usize;
1285                            let j2 = ((j as i64 + dj).rem_euclid(grid.nv as i64)) as usize;
1286                            let there = at(i2, j2);
1287                            if there < here {
1288                                is_min = false;
1289                            }
1290                            if there > here {
1291                                is_max = false;
1292                            }
1293                        }
1294                    }
1295                    if !(is_min || is_max) {
1296                        continue;
1297                    }
1298                    // Newton on ∇ℓ = 0.
1299                    let (mut u, mut v) = Self::grid_param(grid, i, j);
1300                    let hu = 1e-5 * (ur[1] - ur[0]);
1301                    let hv = 1e-5 * (vr[1] - vr[0]);
1302                    let level = |u: f64, v: f64| self.source_reduced(source, u, v).0;
1303                    for _ in 0..50 {
1304                        let f = level(u, v);
1305                        let fu = (level(u + hu, v) - level(u - hu, v)) / (2.0 * hu);
1306                        let fv = (level(u, v + hv) - level(u, v - hv)) / (2.0 * hv);
1307                        let fuu = (level(u + hu, v) - 2.0 * f + level(u - hu, v)) / (hu * hu);
1308                        let fvv = (level(u, v + hv) - 2.0 * f + level(u, v - hv)) / (hv * hv);
1309                        let fuv = (level(u + hu, v + hv) - level(u + hu, v - hv) - level(u - hu, v + hv)
1310                            + level(u - hu, v - hv))
1311                            / (4.0 * hu * hv);
1312                        let det = fuu * fvv - fuv * fuv;
1313                        if det.abs() < 1e-300 {
1314                            break;
1315                        }
1316                        let du = -(fu * fvv - fv * fuv) / det;
1317                        let dv = -(fuu * fv - fuv * fu) / det;
1318                        u += du;
1319                        v += dv;
1320                        if du.abs() < 1e-14 && dv.abs() < 1e-14 {
1321                            break;
1322                        }
1323                    }
1324                    if u_periodic {
1325                        u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1326                    }
1327                    if periodic {
1328                        v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1329                    }
1330                    if u >= ur[0] && u <= ur[1] && (periodic || (v >= vr[0] && v <= vr[1])) {
1331                        out.push(self.source_reduced(source, u, v));
1332                        params.push((u, v));
1333                    }
1334                }
1335            }
1336            self.folds_and_kinks(source, grid, &mut out, &mut params);
1337        } else {
1338            let (range, _, _) = self.domain(source);
1339            let n = 2048;
1340            let param = |k: usize| range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1341            let values: Vec<f64> = (0..=n).map(|k| self.source_reduced(source, param(k), 0.0).0).collect();
1342            for k in 1..n {
1343                let (a, b, c) = (values[k - 1], values[k], values[k + 1]);
1344                if (b <= a && b <= c) || (b >= a && b >= c) {
1345                    let sign = if b <= a && b <= c { 1.0 } else { -1.0 };
1346                    let (mut lo, mut hi) = (param(k - 1), param(k + 1));
1347                    let ratio = 0.618_033_988_749_894_9;
1348                    for _ in 0..100 {
1349                        let c1 = hi - ratio * (hi - lo);
1350                        let d1 = lo + ratio * (hi - lo);
1351                        if sign * self.source_reduced(source, c1, 0.0).0 < sign * self.source_reduced(source, d1, 0.0).0 {
1352                            hi = d1;
1353                        } else {
1354                            lo = c1;
1355                        }
1356                    }
1357                    out.push(self.source_reduced(source, 0.5 * (lo + hi), 0.0));
1358                    params.push((0.5 * (lo + hi), 0.0));
1359                }
1360            }
1361        }
1362        (out, params)
1363    }
1364}
1365
1366/// Are two sources the same boundary piece of the sweep: the same source, or
1367/// a piece and the one that continues it across a set-periodic motion's wrap?
1368/// A face handing over to the edge that bounds it is NOT the same: the V
1369/// wheel's flank meets its tip circle's image at a kink, and fitting one curve
1370/// through it read the groove 1.5e-6 off (split: 1.1e-8).
1371fn same_or_bounding(engine: &Engine, a: usize, b: usize) -> bool {
1372    if a == b {
1373        return true;
1374    }
1375    let pieces = match engine.tool {
1376        SweptTool::Revolved { meridian, .. } => meridian.len(),
1377        SweptTool::Prism { outline, .. } => outline.len(),
1378    };
1379    // (piece of the face, piece whose start the edge is)
1380    let face = |s: Source| match s {
1381        Source::RevolvedFace { piece } | Source::PrismFace { piece } | Source::ScrewPrismSide { piece } => Some(piece),
1382        _ => None,
1383    };
1384    let edge = |s: Source| match s {
1385        Source::RevolvedCorner { piece } | Source::PrismVertex { piece } | Source::ScrewPrismEdge { piece } => Some(piece),
1386        _ => None,
1387    };
1388    let (sa, sb) = (engine.sources[a], engine.sources[b]);
1389    // Across the wrap, piece `k` continues as `wrap[k]`.
1390    let same_piece = |x: usize, y: usize| {
1391        x == y || engine.wrap.as_ref().is_some_and(|map| map[x] == y || map[y] == x)
1392    };
1393    let bounds = |f: usize, e: usize| {
1394        same_piece(e, f) || same_piece(e, (f + 1) % pieces)
1395    };
1396    let _ = bounds;
1397    match (face(sa), edge(sa), face(sb), edge(sb)) {
1398        (Some(f), _, Some(g), _) => same_piece(f, g),
1399        (_, Some(e), _, Some(g)) => same_piece(e, g),
1400        _ => false,
1401    }
1402}
1403
1404/// Do two interval lists describe the same structure (same count, each end on
1405/// the same source or one that hands over to it smoothly, matched in order)?
1406fn compatible(engine: &Engine, a: &[Interval], b: &[Interval]) -> bool {
1407    let angular = engine.angular;
1408    if a.len() != b.len() {
1409        return false;
1410    }
1411    // Match each interval of `a` to the one of `b` nearest its middle.
1412    let mut used = vec![false; b.len()];
1413    for x in a {
1414        let mid = 0.5 * (x.lo + x.hi);
1415        let mut best = None;
1416        let mut best_distance = f64::INFINITY;
1417        for (k, y) in b.iter().enumerate() {
1418            let other = 0.5 * (y.lo + y.hi);
1419            let d = if angular { wrap_pi(mid - other).abs() } else { (mid - other).abs() };
1420            if d < best_distance {
1421                best_distance = d;
1422                best = Some(k);
1423            }
1424        }
1425        let Some(k) = best else { return false };
1426        let y = &b[k];
1427        let overlap = if angular {
1428            let shift = (wrap_pi(y.lo - x.lo)) - (y.lo - x.lo);
1429            let (ylo, yhi) = (y.lo + shift, y.hi + shift);
1430            x.hi.min(yhi) - x.lo.max(ylo)
1431        } else {
1432            x.hi.min(y.hi) - x.lo.max(y.lo)
1433        };
1434        if used[k]
1435            || overlap < 0.0
1436            || !same_or_bounding(engine, x.lo_source, y.lo_source)
1437            || !same_or_bounding(engine, x.hi_source, y.hi_source)
1438        {
1439            return false;
1440        }
1441        used[k] = true;
1442    }
1443    true
1444}
1445
1446/// A boundary piece in reduced coordinates, as a polyline of exact samples
1447/// (the first and last are the piece's ends) with their `σ`.
1448struct Piece {
1449    points: Vec<(f64, f64)>,
1450    sigma: Vec<f64>,
1451    /// A straight level segment (built exactly, not fitted).
1452    level_segment: bool,
1453}
1454
1455/// Build the sweep of `tool` along `motion` inside `clip`, as one body per
1456/// connected region of the reduced section.
1457pub fn sweep_solid_within(
1458    tool: &SweptTool,
1459    motion: &SolidMotion,
1460    clip: &SweepClip,
1461) -> Result<Vec<BrepSolid>, KernelRefusal> {
1462    let narrowed: SweepClip;
1463    // --- Frame and classification.
1464    let ez = clip
1465        .axis_direction
1466        .normalized()
1467        .or_refuse(KernelStage::Collect, "clip_axis")?;
1468    let ex = clip
1469        .reference
1470        .sub(ez.scale(clip.reference.dot(ez)))
1471        .normalized()
1472        .or_refuse(KernelStage::Collect, "clip_reference")?;
1473    let frame = Frame { origin: clip.axis_origin, ex, ey: ez.cross(ex), ez };
1474    if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
1475        return Err(KernelRefusal::input(
1476            KernelStage::Collect,
1477            "motion_range",
1478            format!("{SOLID_SWEEP_REFUSAL}: the motion's range must run forwards"),
1479        ));
1480    }
1481    if motion.steps.is_empty() {
1482        return Err(KernelRefusal::input(KernelStage::Collect, "motion_empty", format!("{SOLID_SWEEP_REFUSAL}: no motion")));
1483    }
1484    let mut axes = Vec::with_capacity(motion.steps.len());
1485    for step in &motion.steps {
1486        axes.push(step.axis_direction.normalized().or_refuse(KernelStage::Collect, "step_axis")?);
1487    }
1488    if !(clip.radius[0] >= 0.0 && clip.radius[1] > clip.radius[0] && clip.axial[1] > clip.axial[0]) {
1489        return Err(KernelRefusal::input(
1490            KernelStage::Collect,
1491            "clip_range",
1492            format!("{SOLID_SWEEP_REFUSAL}: the clip's ranges must be increasing"),
1493        ));
1494    }
1495    let on_axis = |origin: Vec3, axis: Vec3| -> bool {
1496        let parallel = axis.cross(ez).length() < 1e-12;
1497        let offset = origin.sub(clip.axis_origin);
1498        parallel && offset.sub(ez.scale(offset.dot(ez))).length() < 1e-9 * (1.0 + clip.radius[1])
1499    };
1500    let class = 'class: { match tool {
1501        SweptTool::Revolved { meridian, .. } => {
1502            validate_outline(meridian, "meridian")?;
1503            if motion.steps.len() != 1 || !on_axis(motion.steps[0].axis_origin, axes[0]) {
1504                return Err(refuse(
1505                    "motion_class",
1506                    "a solid of revolution is swept here only by one screw or turn about the clip's axis".into(),
1507                ));
1508            }
1509            let step = motion.steps[0];
1510            let sense = axes[0].dot(ez).signum();
1511            if step.turn == 0.0 {
1512                return Err(refuse("motion_class", "a slide of a solid of revolution is not built here".into()));
1513            }
1514            if step.advance == 0.0 {
1515                Class::Turn
1516            } else {
1517                // axial advance per radian about +ez
1518                Class::Screw((step.advance * sense) / (step.turn * sense))
1519            }
1520        }
1521        SweptTool::Prism { normal, outline, from, to, origin, .. } => {
1522            validate_outline(outline, "prism")?;
1523            let n = normal.normalized().or_refuse(KernelStage::Collect, "prism_normal")?;
1524            let screw = motion.steps.len() == 1
1525                && motion.steps[0].turn != 0.0
1526                && motion.steps[0].advance != 0.0
1527                && on_axis(motion.steps[0].axis_origin, axes[0])
1528                && n.cross(ez).length() <= 1e-12;
1529            if screw {
1530                // A screw carries the prism's ends along, so the clip may run past them.
1531                let step = motion.steps[0];
1532                let sense = axes[0].dot(ez).signum();
1533                break 'class Class::Screw((step.advance * sense) / (step.turn * sense));
1534            }
1535            if n.cross(ez).length() > 1e-12 {
1536                return Err(refuse("motion_class", "a prism is swept here only along its own direction's clip axis".into()));
1537            }
1538            for (step, axis) in motion.steps.iter().zip(&axes) {
1539                let turn_ok = step.turn == 0.0 || axis.cross(n).length() < 1e-12;
1540                let advance_ok = step.advance == 0.0 || (step.turn == 0.0 && axis.dot(n).abs() < 1e-12);
1541                if !(turn_ok && advance_ok) {
1542                    return Err(refuse(
1543                        "motion_class",
1544                        "a prism is swept here only by turns about axes along it and slides across it".into(),
1545                    ));
1546                }
1547            }
1548            // The clip's axial range must lie within the prism's extent.
1549            let base = clip.axis_origin.sub(*origin).dot(n);
1550            let sense = ez.dot(n);
1551            let h0 = base + sense * clip.axial[0];
1552            let h1 = base + sense * clip.axial[1];
1553            if h0.min(h1) < *from || h0.max(h1) > *to {
1554                return Err(refuse(
1555                    "clip_past_prism",
1556                    "the clip reaches past the prism's ends, where the sweep is not a prism".into(),
1557                ));
1558            }
1559            Class::Planar
1560        }
1561    } };
1562    let prism_height = match tool {
1563        SweptTool::Prism { from, to, .. } => 0.5 * (from + to),
1564        _ => 0.0,
1565    };
1566
1567    // Does the motion end where it began, as a set? For a prism, every outline
1568    // vertex at the last position must sit on some vertex at the first (the plus
1569    // turns 540°, which its half-turn symmetry makes a whole period). The
1570    // permutation says which piece continues which across the wrap.
1571    let wrap: Option<Vec<usize>> = match tool {
1572        SweptTool::Prism { .. } if matches!(class, Class::Screw(_)) => None,
1573        SweptTool::Prism { origin, normal, x_axis, outline, .. } => {
1574            let probe = Engine {
1575                tool,
1576                motion,
1577                axes: axes.clone(),
1578                frame,
1579                class,
1580                clip,
1581                crest_gap: false,
1582                sources: Vec::new(),
1583                grids: Vec::new(),
1584                critical: Vec::new(),
1585                critical_params: Vec::new(),
1586                angular: true,
1587                prism_height,
1588                scale: 1.0,
1589                cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1590                wrap: None,
1591                counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1592            };
1593            let n = normal.normalized_or(*normal);
1594            let xa = x_axis.normalized_or(*x_axis);
1595            let ya = n.cross(xa);
1596            let vertex = |k: usize, t: f64| {
1597                let q = outline[k].point(0.0);
1598                probe.motion_apply(t, origin.add(xa.scale(q[0])).add(ya.scale(q[1])).add(n.scale(prism_height)))
1599            };
1600            let size = scale_of(outline);
1601            let mut map = Vec::with_capacity(outline.len());
1602            for k in 0..outline.len() {
1603                let end = vertex(k, motion.to);
1604                match (0..outline.len()).find(|&j| vertex(j, motion.from).sub(end).length() < 1e-9 * size) {
1605                    Some(j) => map.push(j),
1606                    None => break,
1607                }
1608            }
1609            let consistent = map.len() == outline.len()
1610                && (0..outline.len()).all(|k| map[(k + 1) % outline.len()] == (map[k] + 1) % outline.len());
1611            consistent.then_some(map)
1612        }
1613        SweptTool::Revolved { .. } => None,
1614    };
1615    let periodic_motion = wrap.is_some();
1616    let mut sources = Vec::new();
1617    match tool {
1618        SweptTool::Revolved { meridian, .. } => {
1619            // A piece lying on the axis revolves into nothing, and a corner on the
1620            // axis into a point: neither bounds the tool.
1621            let on_axis = |p: [f64; 2]| p[0].abs() <= 1e-12 * scale_of(meridian);
1622            for piece in 0..meridian.len() {
1623                let lies_on_axis = matches!(meridian[piece], OutlinePiece::Line { from, to } if on_axis(from) && on_axis(to));
1624                if !lies_on_axis {
1625                    sources.push(Source::RevolvedFace { piece });
1626                }
1627                if !meridian[piece].is_full_circle() && !on_axis(meridian[piece].point(0.0)) {
1628                    sources.push(Source::RevolvedCorner { piece });
1629                }
1630            }
1631        }
1632        SweptTool::Prism { outline, .. } if matches!(class, Class::Screw(_)) => {
1633            for piece in 0..outline.len() {
1634                sources.push(Source::ScrewPrismSide { piece });
1635                if !outline[piece].is_full_circle() {
1636                    sources.push(Source::ScrewPrismEdge { piece });
1637                }
1638                sources.push(Source::ScrewPrismRim { piece, end: false });
1639                sources.push(Source::ScrewPrismRim { piece, end: true });
1640            }
1641        }
1642        SweptTool::Prism { outline, .. } => {
1643            for piece in 0..outline.len() {
1644                sources.push(Source::PrismFace { piece });
1645                if !outline[piece].is_full_circle() {
1646                    sources.push(Source::PrismVertex { piece });
1647                }
1648                if !periodic_motion {
1649                    sources.push(Source::PrismEnd { piece, end: false });
1650                    sources.push(Source::PrismEnd { piece, end: true });
1651                }
1652            }
1653        }
1654    }
1655    let scale = clip.radius[1].max(1.0);
1656    let mut engine = Engine {
1657        tool,
1658        motion,
1659        axes,
1660        frame,
1661        class,
1662        clip,
1663        crest_gap: false,
1664        sources,
1665        grids: Vec::new(),
1666        critical: Vec::new(),
1667        critical_params: Vec::new(),
1668        angular: class != Class::Turn,
1669        prism_height,
1670        scale,
1671        cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1672        wrap,
1673        counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1674    };
1675    engine.grids = engine
1676        .sources
1677        .iter()
1678        .map(|source| if Engine::is_face(*source) { Some(engine.build_grid(*source)) } else { None })
1679        .collect();
1680    let critical: Vec<_> = (0..engine.sources.len()).map(|k| engine.source_critical(k)).collect();
1681    engine.critical_params = critical.iter().map(|c| c.1.clone()).collect();
1682    engine.critical = critical.into_iter().map(|c| c.0).collect();
1683
1684    // --- Ends: a screw or turn must not reach the clip at either end.
1685    if matches!(class, Class::Screw(_) | Class::Turn) {
1686        check_ends_clear(&engine)?;
1687    }
1688
1689    // --- A screw whose grooves meet: above a crest level nothing stays. Build
1690    //     what stays below it, and take that from the clip at the end.
1691    let mut crest = None;
1692    if matches!(class, Class::Screw(_)) && clip.radius[0] == 0.0 && clip.angle.is_none() && whole_level_on() {
1693        crest = crest_level(&engine)?;
1694        if let Some(level) = crest {
1695            narrowed = SweepClip { radius: [0.0, level], ..clip.clone() };
1696            engine.clip = &narrowed;
1697            engine.crest_gap = true;
1698        }
1699    }
1700
1701    // --- Scan the levels for events.
1702    let (l0, l1) = (engine.clip.radius[0], engine.clip.radius[1]);
1703    let scan = 192;
1704    let mut levels: Vec<f64> = (0..=scan).map(|k| l0 + (l1 - l0) * k as f64 / scan as f64).collect();
1705    // Never sample exactly on a critical level: nudge the scan off them.
1706    for level in &mut levels {
1707        for critical in engine.critical.iter().flatten() {
1708            if (critical.0 - *level).abs() < 1e-9 * scale {
1709                *level += 1e-7 * (l1 - l0);
1710            }
1711        }
1712    }
1713    let mut structure = Vec::with_capacity(levels.len());
1714    for &level in &levels {
1715        structure.push(engine.intervals_at(level)?);
1716    }
1717    let debug = std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok();
1718    let mut breaks = vec![l0];
1719    for k in 0..scan {
1720        find_events(&engine, levels[k], &structure[k], levels[k + 1], &structure[k + 1], &mut breaks, 0)?;
1721    }
1722    breaks.push(l1);
1723    // Snap each event to a critical level of a source when one is that close.
1724    // A birth or death is found by bisection only to where the seeding first
1725    // sees the level's contour, which near a tip is a √ away from it: the
1726    // critical level is the exact one.
1727    let last = breaks.len() - 1;
1728    for level in breaks[1..last].iter_mut() {
1729        let mut nearest = *level;
1730        let mut distance = 1e-5 * scale;
1731        for critical in engine.critical.iter().flatten() {
1732            if (critical.0 - *level).abs() < distance {
1733                distance = (critical.0 - *level).abs();
1734                nearest = critical.0;
1735            }
1736        }
1737        *level = nearest;
1738    }
1739    breaks.sort_by(|a, b| a.total_cmp(b));
1740    // A source handing over to the next one at an edge can flicker over a few
1741    // bisection steps (the face's extremum sits a hair inside its domain); the
1742    // bands that leaves are far below any feature. Keep the first break of each
1743    // cluster, and the clip's own ends.
1744    let mut kept: Vec<f64> = Vec::with_capacity(breaks.len());
1745    for (index, &level) in breaks.iter().enumerate() {
1746        let is_end = index == 0 || index + 1 == breaks.len();
1747        match kept.last() {
1748            Some(&previous) if level - previous < 1e-5 * scale => {
1749                if is_end {
1750                    kept.pop();
1751                    kept.push(level);
1752                }
1753            }
1754            _ => kept.push(level),
1755        }
1756    }
1757    let mut breaks = kept;
1758
1759    // --- Merges no critical point explains: a FOLD the face's own Newton cannot
1760    //     reach (the plus wheel's arm tip: its level varies by 1e-4 along the
1761    //     envelope there). The gap between the two merging ends closes as
1762    //     √(ℓf − ℓ), so gap² is smooth in ℓ: fit it as a quadratic through three
1763    //     levels below the event, outside the band where the extremum Newton
1764    //     flickers, and take its root as the merge level; the gap's middle,
1765    //     extrapolated the same way, is the merge point. Both ends' sources
1766    //     record it as a critical point, and the bands keep their samples away
1767    //     from it (`fold_ends`).
1768    let mut fold_ends: Vec<f64> = Vec::new();
1769    for index in 1..breaks.len().saturating_sub(1) {
1770        let level = breaks[index];
1771        let snapped = engine.critical.iter().flatten().any(|c| (c.0 - level).abs() < 1e-9 * scale);
1772        if snapped {
1773            continue;
1774        }
1775        let room = (level - breaks[index - 1]).min(breaks[index + 1] - level);
1776        // Close enough that the √ law holds to the fit's order, far enough to be
1777        // clear of the flicker (the plus wheel: 2e-4 out, flicker inside 1e-5).
1778        let delta = (5e-6 * scale).min(room / 8.0);
1779        let below = engine.intervals_at(level - delta)?;
1780        let above = engine.intervals_at(level + delta)?;
1781        if below.len() <= above.len() {
1782            continue;
1783        }
1784        // Adjacent intervals below whose facing ends close into one above.
1785        let mut refined: Vec<(f64, f64, usize, usize)> = Vec::new();
1786        for k in 0..below.len() {
1787            let (x, y) = (below[k], below[(k + 1) % below.len()]);
1788            if !engine.angular && k + 1 == below.len() {
1789                break;
1790            }
1791            let gap_at = |intervals: &[Interval]| -> Option<(f64, f64)> {
1792                // the pair nearest (x.hi, y.lo) at this level
1793                let mut best: Option<(f64, f64, f64)> = None;
1794                for i in 0..intervals.len() {
1795                    let (p, q) = (intervals[i], intervals[(i + 1) % intervals.len()]);
1796                    let d = engine.o_difference(p.hi, x.hi).abs() + engine.o_difference(q.lo, y.lo).abs();
1797                    if best.map_or(true, |b| d < b.2) {
1798                        let gap = engine.o_difference(q.lo, p.hi);
1799                        best = Some((gap, p.hi + 0.5 * gap, d));
1800                    }
1801                }
1802                best.map(|b| (b.0, b.1))
1803            };
1804            let mut samples = Vec::new();
1805            for factor in [1.0, 2.0, 4.0] {
1806                let l = level - delta * factor;
1807                let intervals = engine.intervals_at(l)?;
1808                if intervals.len() != below.len() {
1809                    break;
1810                }
1811                if let Some((gap, middle)) = gap_at(&intervals) {
1812                    samples.push((l, gap, middle));
1813                }
1814            }
1815            if samples.len() < 3 || samples.iter().any(|s| s.1 <= 0.0 || s.1 > 0.5) {
1816                continue;
1817            }
1818            // Quadratic through (l, gap²); its root above the samples.
1819            let quad = |values: [f64; 3]| -> (f64, f64, f64) {
1820                let (x0, x1, x2) = (samples[0].0, samples[1].0, samples[2].0);
1821                let (y0, y1, y2) = (values[0], values[1], values[2]);
1822                let d01 = (y0 - y1) / (x0 - x1);
1823                let d12 = (y1 - y2) / (x1 - x2);
1824                let a2 = (d01 - d12) / (x0 - x2);
1825                let a1 = d01 - a2 * (x0 + x1);
1826                let a0 = y0 - a1 * x0 - a2 * x0 * x0;
1827                (a0, a1, a2)
1828            };
1829            let (c0, c1, c2) = quad([samples[0].1.powi(2), samples[1].1.powi(2), samples[2].1.powi(2)]);
1830            let root = if c2.abs() < 1e-300 {
1831                -c0 / c1
1832            } else {
1833                let disc = c1 * c1 - 4.0 * c2 * c0;
1834                if disc < 0.0 {
1835                    continue;
1836                }
1837                let r1 = (-c1 + disc.sqrt()) / (2.0 * c2);
1838                let r2 = (-c1 - disc.sqrt()) / (2.0 * c2);
1839                if (r1 - level).abs() < (r2 - level).abs() { r1 } else { r2 }
1840            };
1841            if !(root > samples[0].0 && (root - level).abs() < delta) {
1842                continue;
1843            }
1844            let (m0, m1, m2) = quad([samples[0].2, samples[1].2, samples[2].2]);
1845            let middle = m0 + m1 * root + m2 * root * root;
1846            refined.push((root, middle, x.hi_source, y.lo_source));
1847        }
1848        if !refined.is_empty() {
1849            // Every merging pair records its own point; the break is their mean
1850            // level (equal to round-off where the spaces are copies of one).
1851            let root = refined.iter().map(|r| r.0).sum::<f64>() / refined.len() as f64;
1852            for &(own, middle, a_source, b_source) in &refined {
1853                if debug {
1854                    eprintln!("  fold: break {level:.12} -> {own:.12} at o {middle:.12}");
1855                }
1856                engine.critical[a_source].push((root, middle));
1857                engine.critical[b_source].push((root, middle));
1858            }
1859            breaks[index] = root;
1860            fold_ends.push(root);
1861        }
1862    }
1863
1864    if debug {
1865        eprintln!("solid sweep: after events {:?} breaks {}", engine.counters.get(), breaks.len());
1866    }
1867    // --- Sample each band's interval ends.
1868    let mut pieces: Vec<Piece> = Vec::new();
1869    // Per band: the intervals at its bottom and top (for the level segments).
1870    let mut band_ends: Vec<(f64, f64, Vec<(usize, usize)>)> = Vec::new();
1871    for window in breaks.windows(2) {
1872        let (a, b) = (window[0], window[1]);
1873        if b - a < 1e-12 * scale {
1874            continue;
1875        }
1876        let mid = engine.intervals_at(0.5 * (a + b))?;
1877        if mid.is_empty() {
1878            continue;
1879        }
1880        let (band_pieces, _, _) = sample_band(&engine, a, b, &mid, &fold_ends)?;
1881        let first = pieces.len();
1882        let pairs = (0..band_pieces.len() / 2).map(|i| (first + 2 * i, first + 2 * i + 1)).collect();
1883        pieces.extend(band_pieces);
1884        band_ends.push((a, b, pairs));
1885    }
1886    if pieces.is_empty() {
1887        return Ok(Vec::new());
1888    }
1889    if debug {
1890        eprintln!("solid sweep: after bands {:?}", engine.counters.get());
1891        for (k, critical) in engine.critical.iter().enumerate() {
1892            eprintln!("  source {k} {:?}: critical {:?}", engine.sources[k], critical);
1893        }
1894    }
1895
1896    // --- One value per interval end at each break. The band below and the band
1897    //     above read an end that continues across a break from their own
1898    //     sources, at a level found only to the flicker cluster; the two agree to
1899    //     far below any feature but not to the bit, and a sliver of level segment
1900    //     between them is an edge no solid should carry. Snap every end at a level
1901    //     to its cluster's first value (ends of one interval meeting at a tip
1902    //     included).
1903    let snap_length = 1e-5 * scale;
1904    let mut break_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1905    break_levels.sort_by(|a, b| a.total_cmp(b));
1906    break_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1907    for &level in &break_levels {
1908        let tolerance = if engine.angular { snap_length / level.max(1e-12) } else { snap_length };
1909        let mut ends: Vec<(usize, bool)> = Vec::new();
1910        for (index, piece) in pieces.iter().enumerate() {
1911            if (piece.points[0].0 - level).abs() < 1e-12 * scale {
1912                ends.push((index, false));
1913            }
1914            if (piece.points.last().expect("nonempty").0 - level).abs() < 1e-12 * scale {
1915                ends.push((index, true));
1916            }
1917        }
1918        let value = |pieces: &Vec<Piece>, (index, last): (usize, bool)| {
1919            if last { pieces[index].points.last().expect("nonempty").1 } else { pieces[index].points[0].1 }
1920        };
1921        for i in 0..ends.len() {
1922            let anchor = value(&pieces, ends[i]);
1923            for j in i + 1..ends.len() {
1924                let other = value(&pieces, ends[j]);
1925                let d = engine.o_difference(other, anchor);
1926                if d.abs() < tolerance && d != 0.0 {
1927                    let (index, last) = ends[j];
1928                    let target = other - d;
1929                    let points = &mut pieces[index].points;
1930                    if last {
1931                        points.last_mut().expect("nonempty").1 = target;
1932                    } else {
1933                        points[0].1 = target;
1934                    }
1935                }
1936            }
1937        }
1938    }
1939
1940    // --- Level segments: where the intervals above and below a level differ.
1941    let mut all_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1942    all_levels.sort_by(|a, b| a.total_cmp(b));
1943    all_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1944    for &level in &all_levels {
1945        let mut below: Vec<(f64, f64)> = Vec::new();
1946        let mut above: Vec<(f64, f64)> = Vec::new();
1947        for (a, b, pairs) in &band_ends {
1948            for &(lo, hi) in pairs {
1949                let (lo, hi) = (&pieces[lo].points, &pieces[hi].points);
1950                if (b - level).abs() < 1e-12 * scale {
1951                    below.push((lo.last().expect("nonempty").1, hi.last().expect("nonempty").1));
1952                }
1953                if (a - level).abs() < 1e-12 * scale {
1954                    above.push((lo[0].1, hi[0].1));
1955                }
1956            }
1957        }
1958        let tolerance = if engine.angular { 1e-9 * scale / level.max(1e-12) } else { 1e-9 * scale };
1959        for (lo, hi) in symmetric_difference(&below, &above, engine.angular, tolerance) {
1960            let steps = 2;
1961            pieces.push(Piece {
1962                points: (0..=steps)
1963                    .map(|k| (level, lo + (hi - lo) * k as f64 / steps as f64))
1964                    .collect(),
1965                sigma: (0..=steps).map(|k| k as f64 / steps as f64).collect(),
1966                level_segment: true,
1967            });
1968        }
1969    }
1970
1971    if std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
1972        eprintln!("solid sweep: breaks {breaks:?}");
1973        for piece in &pieces {
1974            let a = piece.points[0];
1975            let b = *piece.points.last().expect("nonempty");
1976            eprintln!(
1977                "  piece {} ({:.9}, {:.9}) -> ({:.9}, {:.9}) n={}",
1978                if piece.level_segment { "LEVEL" } else { "edge " },
1979                a.0, a.1, b.0, b.1, piece.points.len()
1980            );
1981            if std::env::var("BREP_DEBUG_SOLID_SWEEP").as_deref() == Ok("2") {
1982                for p in piece.points.iter().take(6) {
1983                    eprintln!("      {:.12} {:.12}", p.0, p.1);
1984                }
1985            }
1986        }
1987    }
1988    // --- At a crest the walls have met: the top arc is no boundary of what stays.
1989    if let Some(level) = crest {
1990        pieces.retain(|piece| !(piece.level_segment && (piece.points[0].0 - level).abs() < 1e-9 * scale));
1991    }
1992    // --- Assemble loops and build the bodies.
1993    let loops = assemble_loops(&engine, pieces)?;
1994    let mut bodies = Vec::new();
1995    for pieces in loops {
1996        bodies.push(build_body(&engine, &pieces)?);
1997    }
1998    if crest.is_some() {
1999        let [kept] = bodies.as_slice() else {
2000            return Err(refuse("whole_level", "what stays below the crest is not one body".into()));
2001        };
2002        return clip_less(clip, frame_base(&engine), kept);
2003    }
2004    Ok(bodies)
2005}
2006
2007/// Replace a screw loft's side surfaces and helical edges by quintic splines
2008/// through the exact screw and its first two derivatives. A cubic interpolant
2009/// through `segments + 1` sections misses a turn by h⁴; these by h⁶, which is
2010/// what lets a long fine thread be built with few spans. Sections, caps and
2011/// parameters are the loft's, so the trims stay as they are.
2012fn carry_along_screw(body: &mut BrepSolid, engine: &Engine, c: f64, span: f64, segments: usize) -> Result<(), KernelRefusal> {
2013    let origin = engine.clip.axis_origin;
2014    let ez = engine.frame.ez;
2015    // The six Bézier points of each span collapse to three a span, C² at the joints.
2016    let carry = |start: Vec3| -> Vec<Vec3> {
2017        let h = span / segments as f64;
2018        let axial = ez.scale(start.sub(origin).dot(ez));
2019        let radial = start.sub(origin).sub(axial);
2020        let at = |k: usize| {
2021            let angle = h * k as f64;
2022            let r = radial.rotate_about(ez, angle);
2023            let value = origin.add(axial).add(r).add(ez.scale(c * angle));
2024            (value, ez.cross(r).add(ez.scale(c)), r.scale(-1.0))
2025        };
2026        let piece = |k: usize| {
2027            let ((f0, d0, a0), (f1, d1, a1)) = (at(k), at(k + 1));
2028            [
2029                f0,
2030                f0.add(d0.scale(h / 5.0)),
2031                f0.add(d0.scale(2.0 * h / 5.0)).add(a0.scale(h * h / 20.0)),
2032                f1.sub(d1.scale(2.0 * h / 5.0)).add(a1.scale(h * h / 20.0)),
2033                f1.sub(d1.scale(h / 5.0)),
2034                f1,
2035            ]
2036        };
2037        let mut points = Vec::with_capacity(3 * segments + 3);
2038        let first = piece(0);
2039        points.extend_from_slice(&first[..3]);
2040        for k in 0..segments - 1 {
2041            let (here, next) = (piece(k), piece(k + 1));
2042            points.push(here[3]);
2043            points.push(here[4].scale(2.0).sub(here[3]));
2044            points.push(next[2]);
2045        }
2046        let last = piece(segments - 1);
2047        points.extend_from_slice(&last[3..]);
2048        points
2049    };
2050    let knots = |lo: f64, hi: f64| -> Vec<f64> {
2051        let mut knots = vec![lo; 6];
2052        for k in 1..segments {
2053            knots.extend([lo + (hi - lo) * k as f64 / segments as f64; 3]);
2054        }
2055        knots.extend([hi; 6]);
2056        knots
2057    };
2058    let point = |p: &crate::Vec4| Vec3::new(p.x / p.w, p.y / p.w, p.z / p.w);
2059    let moved = |p: &crate::Vec4, angle: f64| -> Vec3 {
2060        rotate(point(p), origin, ez, angle).add(ez.scale(c * angle))
2061    };
2062    let tolerance = 1e-7 * engine.scale;
2063    for face in body.shells.iter_mut().flat_map(|shell| &mut shell.faces) {
2064        let rows = &face.surface.control_points;
2065        if rows[0].len() != segments + 1 {
2066            continue; // a cap
2067        }
2068        // The loft's columns must already be this screw, from section to section.
2069        if rows.iter().any(|row| moved(&row[0], span).sub(point(&row[segments])).length() > tolerance) {
2070            return Err(refuse("screw_loft", "a lofted wall does not follow the screw".into()));
2071        }
2072        let lifted: Vec<Vec<crate::Vec4>> = rows
2073            .iter()
2074            .map(|row| {
2075                let w = row[0].w;
2076                carry(point(&row[0])).into_iter().map(|q| crate::Vec4::from_point(q, w)).collect()
2077            })
2078            .collect();
2079        let v = &face.surface.knots_v;
2080        let (lo, hi) = (v[0], *v.last().expect("knots"));
2081        face.surface = crate::NurbsSurface::new(face.surface.degree_u, 5, face.surface.knots_u.clone(), knots(lo, hi), lifted)
2082            .or_refuse(KernelStage::Fragment, "screw_surface")?;
2083    }
2084    let height = c * span;
2085    let points: rustc_hash::FxHashMap<u64, Vec3> = body.vertices.iter().map(|vertex| (vertex.id, vertex.point)).collect();
2086    for edge in &mut body.edges {
2087        let (from, to) = (points[&edge.start_vertex_id], points[&edge.end_vertex_id]);
2088        let rise = to.sub(from).dot(ez);
2089        if (rise.abs() - height.abs()).abs() > tolerance {
2090            continue; // lies in a cap
2091        }
2092        let domain = edge.curve.domain().or_refuse(KernelStage::Fragment, "domain")?;
2093        let forward = rise * height > 0.0;
2094        let mut path = carry(if forward { from } else { to });
2095        if !forward {
2096            path.reverse();
2097        }
2098        edge.curve = NurbsCurve::new(5, knots(domain[0], domain[1]), path.into_iter().map(|q| crate::Vec4::from_point(q, 1.0)).collect())
2099            .or_refuse(KernelStage::Fragment, "screw_edge")?;
2100    }
2101    Ok(())
2102}
2103
2104/// `rows` copies of `row` along the screw, each the image of the last, sharing
2105/// the section edges and vertices between neighbours; only the first bottom cap
2106/// and the last top cap stay.
2107fn stack_screw_rows(
2108    row: &BrepSolid,
2109    engine: &Engine,
2110    c: f64,
2111    span: f64,
2112    segments: usize,
2113    rows: usize,
2114) -> Result<BrepSolid, KernelRefusal> {
2115    let origin = engine.clip.axis_origin;
2116    let ez = engine.frame.ez;
2117    let moved = |x: Vec3, copy: usize| -> Vec3 {
2118        let angle = span * copy as f64;
2119        rotate(x, origin, ez, angle).add(ez.scale(c * angle))
2120    };
2121    let tolerance = 1e-7 * engine.scale;
2122    let height = |x: Vec3| x.sub(origin).dot(ez);
2123    let points: rustc_hash::FxHashMap<u64, Vec3> = row.vertices.iter().map(|vertex| (vertex.id, vertex.point)).collect();
2124    let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
2125    for vertex in &row.vertices {
2126        low = low.min(height(vertex.point));
2127        high = high.max(height(vertex.point));
2128    }
2129    let at = |id: u64, level: f64| (height(points[&id]) - level).abs() <= tolerance;
2130    let in_level = |edge: &crate::topology::EdgeRecord, level: f64| at(edge.start_vertex_id, level) && at(edge.end_vertex_id, level);
2131    let (bottom, top) = if c * span > 0.0 { (low, high) } else { (high, low) };
2132    // Each bottom vertex and edge of a copy is a top one of the copy before.
2133    let mut vertex_below: rustc_hash::FxHashMap<u64, u64> = Default::default();
2134    for vertex in row.vertices.iter().filter(|vertex| at(vertex.id, bottom)) {
2135        let up = moved(vertex.point, 1);
2136        let below = row.vertices.iter().find(|other| at(other.id, top) && other.point.sub(up).length() <= tolerance);
2137        let Some(below) = below else { return Err(refuse("screw_rows", "a row's section does not meet the next".into())) };
2138        vertex_below.insert(vertex.id, below.id);
2139    }
2140    let mut edge_below: rustc_hash::FxHashMap<u64, u64> = Default::default();
2141    for edge in row.edges.iter().filter(|edge| in_level(edge, bottom)) {
2142        let ends = (vertex_below[&edge.start_vertex_id], vertex_below[&edge.end_vertex_id]);
2143        let below = row.edges.iter().find(|other| in_level(other, top) && (other.start_vertex_id, other.end_vertex_id) == ends);
2144        let Some(below) = below else { return Err(refuse("screw_rows", "a row's section edge does not meet the next".into())) };
2145        edge_below.insert(edge.id, below.id);
2146    }
2147    let face_ids = row.shells.iter().flat_map(|shell| &shell.faces).flat_map(|face| {
2148        std::iter::once(face.id).chain(face.loops.iter().flat_map(|l| std::iter::once(l.id).chain(l.coedges.iter().map(|c| c.id))))
2149    });
2150    let stride = row
2151        .vertices
2152        .iter()
2153        .map(|v| v.id)
2154        .chain(row.edges.iter().map(|e| e.id))
2155        .chain(face_ids)
2156        .max()
2157        .unwrap_or(0)
2158        + 1;
2159    let vertex_id = |id: u64, copy: usize| match vertex_below.get(&id) {
2160        Some(&below) if copy > 0 => below + stride * (copy as u64 - 1),
2161        _ => id + stride * copy as u64,
2162    };
2163    let edge_id = |id: u64, copy: usize| match edge_below.get(&id) {
2164        Some(&below) if copy > 0 => below + stride * (copy as u64 - 1),
2165        _ => id + stride * copy as u64,
2166    };
2167    let is_cap = |face: &crate::FaceRecord| face.surface.control_points[0].len() != 3 * segments + 3;
2168    let cap_level = |face: &crate::FaceRecord, level: f64| {
2169        face.loops.iter().flat_map(|l| &l.coedges).all(|coedge| {
2170            row.edges.iter().find(|edge| edge.id == coedge.edge_id).is_some_and(|edge| in_level(edge, level))
2171        })
2172    };
2173    let mut stacked = row.clone();
2174    stacked.mass_properties_cache = Default::default();
2175    stacked.vertices.clear();
2176    stacked.edges.clear();
2177    for shell in &mut stacked.shells {
2178        shell.faces.clear();
2179    }
2180    for copy in 0..rows {
2181        for vertex in &row.vertices {
2182            if copy == 0 || !vertex_below.contains_key(&vertex.id) {
2183                stacked.vertices.push(crate::topology::VertexRecord { id: vertex_id(vertex.id, copy), point: moved(vertex.point, copy) });
2184            }
2185        }
2186        for edge in &row.edges {
2187            if copy == 0 || !edge_below.contains_key(&edge.id) {
2188                stacked.edges.push(crate::topology::EdgeRecord {
2189                    id: edge_id(edge.id, copy),
2190                    curve: transform_curve(&edge.curve, |x| moved(x, copy))?,
2191                    t0: edge.t0,
2192                    t1: edge.t1,
2193                    start_vertex_id: vertex_id(edge.start_vertex_id, copy),
2194                    end_vertex_id: vertex_id(edge.end_vertex_id, copy),
2195                    degenerate: edge.degenerate,
2196                    name: edge.name.clone(),
2197                });
2198            }
2199        }
2200        for (target, shell) in stacked.shells.iter_mut().zip(&row.shells) {
2201            for face in &shell.faces {
2202                if is_cap(face) && !((copy == 0 && cap_level(face, bottom)) || (copy + 1 == rows && cap_level(face, top))) {
2203                    continue;
2204                }
2205                let control_points = face
2206                    .surface
2207                    .control_points
2208                    .iter()
2209                    .map(|line| {
2210                        line.iter()
2211                            .map(|p| {
2212                                let w = p.w;
2213                                crate::Vec4::from_point(moved(Vec3::new(p.x / w, p.y / w, p.z / w), copy), w)
2214                            })
2215                            .collect()
2216                    })
2217                    .collect();
2218                let surface = crate::NurbsSurface::new(
2219                    face.surface.degree_u,
2220                    face.surface.degree_v,
2221                    face.surface.knots_u.clone(),
2222                    face.surface.knots_v.clone(),
2223                    control_points,
2224                )
2225                .or_refuse(KernelStage::Fragment, "screw_rows")?;
2226                let mut copied = face.clone();
2227                copied.surface = surface;
2228                copied.id += stride * copy as u64;
2229                for loop_record in &mut copied.loops {
2230                    loop_record.id += stride * copy as u64;
2231                    for coedge in &mut loop_record.coedges {
2232                        coedge.id += stride * copy as u64;
2233                        coedge.edge_id = edge_id(coedge.edge_id, copy);
2234                    }
2235                }
2236                target.faces.push(copied);
2237            }
2238        }
2239    }
2240    let issues = stacked.validate();
2241    if issues.iter().any(|issue| issue.severity == "error") {
2242        return Err(refuse("screw_rows", format!("the stacked thread is invalid: {issues:?}")));
2243    }
2244    Ok(stacked)
2245}
2246
2247/// Where the clip's solid starts, on its axis.
2248fn frame_base(engine: &Engine) -> Vec3 {
2249    engine.frame.world([0.0, 0.0, engine.clip.axial[0]])
2250}
2251
2252/// The whole clip cylinder with `kept` taken out of it.
2253fn clip_less(clip: &SweepClip, base: Vec3, kept: &BrepSolid) -> Result<Vec<BrepSolid>, KernelRefusal> {
2254    let direction = clip.axis_direction.normalized().or_refuse(KernelStage::Collect, "clip_axis")?;
2255    let cylinder = crate::make_cylinder_brep(base, direction, clip.radius[1], clip.axial[1] - clip.axial[0])
2256        .or_refuse(KernelStage::Fragment, "clip_cylinder")?;
2257    if thread_rows_on() {
2258        if let Some(direct) = clip_less_direct(&cylinder, base, direction, kept)? {
2259            return Ok(vec![direct]);
2260        }
2261    }
2262    let r = crate::boolean_operation(&cylinder, kept, crate::BooleanOperation::Subtract, &Default::default())?;
2263    Ok(vec![r])
2264}
2265
2266/// `cylinder` with `kept` taken out, where `kept` runs from cap to cap inside
2267/// the wall: nothing crosses, so the result is written down: `kept`'s end loops
2268/// become holes in the caps and its sides turn inside out. `None` when the
2269/// bodies are not that shape; the caller takes the boolean then.
2270fn clip_less_direct(
2271    cylinder: &BrepSolid,
2272    base: Vec3,
2273    direction: Vec3,
2274    kept: &BrepSolid,
2275) -> Result<Option<BrepSolid>, KernelRefusal> {
2276    if kept.shells.len() != 1 || cylinder.shells.len() != 1 {
2277        return Ok(None);
2278    }
2279    let mut stride = 0;
2280    for shell in &cylinder.shells {
2281        for face in &shell.faces {
2282            stride = stride.max(face.id);
2283            for loop_record in &face.loops {
2284                stride = stride.max(loop_record.id);
2285                stride = loop_record.coedges.iter().fold(stride, |m, c| m.max(c.id));
2286            }
2287        }
2288    }
2289    stride = cylinder.vertices.iter().fold(stride, |m, v| m.max(v.id));
2290    stride = cylinder.edges.iter().fold(stride, |m, e| m.max(e.id)) + 1;
2291    let corners = |surface: &crate::NurbsSurface| -> Result<([f64; 4], [Vec3; 3]), String> {
2292        let (u0, u1) = (surface.knots_u[0], *surface.knots_u.last().ok_or("knots")?);
2293        let (v0, v1) = (surface.knots_v[0], *surface.knots_v.last().ok_or("knots")?);
2294        Ok(([u0, u1, v0, v1], [surface.evaluate(u0, v0)?, surface.evaluate(u1, v0)?, surface.evaluate(u0, v1)?]))
2295    };
2296    let mut result = cylinder.clone();
2297    result.mass_properties_cache = Default::default();
2298    result.genus = 1;
2299    result.vertices.extend(kept.vertices.iter().map(|v| crate::topology::VertexRecord { id: v.id + stride, point: v.point }));
2300    for edge in &kept.edges {
2301        let mut edge = edge.clone();
2302        edge.id += stride;
2303        edge.start_vertex_id += stride;
2304        edge.end_vertex_id += stride;
2305        result.edges.push(edge);
2306    }
2307    let mut holes = 0;
2308    let turned = |loop_record: &crate::topology::LoopRecord| -> Result<crate::topology::LoopRecord, KernelRefusal> {
2309        let mut coedges = Vec::with_capacity(loop_record.coedges.len());
2310        for coedge in loop_record.coedges.iter().rev() {
2311            coedges.push(crate::topology::CoedgeRecord {
2312                id: coedge.id + stride,
2313                edge_id: coedge.edge_id + stride,
2314                forward: !coedge.forward,
2315                pcurve: coedge.pcurve.reversed().or_refuse(KernelStage::Fragment, "clip_less_pcurve")?,
2316            });
2317        }
2318        Ok(crate::topology::LoopRecord { id: loop_record.id + stride, coedges })
2319    };
2320    for face in &kept.shells[0].faces {
2321        let mut loops = Vec::with_capacity(face.loops.len());
2322        for loop_record in &face.loops {
2323            loops.push(turned(loop_record)?);
2324        }
2325        if !face.surface.is_affine().unwrap_or(false) {
2326            result.shells[0].faces.push(crate::FaceRecord {
2327                id: face.id + stride,
2328                surface: face.surface.clone(),
2329                same_sense: !face.same_sense,
2330                loops,
2331                name: face.name.clone(),
2332            });
2333            continue;
2334        }
2335        // An end: its loops are holes in the cap at its height, read in that cap's chart.
2336        let (from_box, from) = corners(&face.surface).or_refuse(KernelStage::Fragment, "clip_less_chart")?;
2337        let height = from[0].sub(base).dot(direction);
2338        let mut caps = result.shells[0].faces.iter_mut().filter(|cap| {
2339            cap.surface.is_affine().unwrap_or(false)
2340                && cap.surface.evaluate(cap.surface.knots_u[0], cap.surface.knots_v[0]).is_ok_and(|p| (p.sub(base).dot(direction) - height).abs() < 1e-9 * (1.0 + height.abs()))
2341        });
2342        let (Some(cap), None) = (caps.next(), caps.next()) else { return Ok(None) };
2343        let (to_box, to) = corners(&cap.surface).or_refuse(KernelStage::Fragment, "clip_less_chart")?;
2344        let (eu, ev) = (to[1].sub(to[0]), to[2].sub(to[0]));
2345        if eu.dot(ev).abs() > 1e-9 * eu.length() * ev.length() {
2346            return Ok(None);
2347        }
2348        let chart = |p: Vec3| {
2349            let d = p.sub(to[0]);
2350            [
2351                to_box[0] + (to_box[1] - to_box[0]) * d.dot(eu) / eu.dot(eu),
2352                to_box[2] + (to_box[3] - to_box[2]) * d.dot(ev) / ev.dot(ev),
2353            ]
2354        };
2355        let source = |a: f64, b: f64| from[0].add(from[1].sub(from[0]).scale(a)).add(from[2].sub(from[0]).scale(b));
2356        let (origin, across, up) = (chart(source(0.0, 0.0)), chart(source(1.0, 0.0)), chart(source(0.0, 1.0)));
2357        for loop_record in &mut loops {
2358            for coedge in &mut loop_record.coedges {
2359                let curve = &coedge.pcurve;
2360                let control_points = curve
2361                    .control_points
2362                    .iter()
2363                    .map(|p| {
2364                        let a = (p.x / p.w - from_box[0]) / (from_box[1] - from_box[0]);
2365                        let b = (p.y / p.w - from_box[2]) / (from_box[3] - from_box[2]);
2366                        let x = origin[0] + a * (across[0] - origin[0]) + b * (up[0] - origin[0]);
2367                        let y = origin[1] + a * (across[1] - origin[1]) + b * (up[1] - origin[1]);
2368                        crate::Vec4 { x: x * p.w, y: y * p.w, z: p.z, w: p.w }
2369                    })
2370                    .collect();
2371                coedge.pcurve = NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2372                    .or_refuse(KernelStage::Fragment, "clip_less_pcurve")?;
2373            }
2374        }
2375        cap.loops.extend(loops);
2376        holes += 1;
2377    }
2378    if holes != 2 || result.validate().iter().any(|issue| issue.severity == "error") {
2379        return Ok(None);
2380    }
2381    Ok(Some(result))
2382}
2383
2384/// The level above which a screw's sweep covers every angle, up to the clip,
2385/// when the material between its turns narrows to nothing there; `None` when
2386/// the sweep leaves something at the clip's edge.
2387fn crest_level(engine: &Engine) -> Result<Option<f64>, KernelRefusal> {
2388    let whole = |level: f64| -> Result<bool, KernelRefusal> {
2389        match engine.intervals_at(level) {
2390            Ok(_) => Ok(false),
2391            Err(error) if error.message.contains(WHOLE_LEVEL) => Ok(true),
2392            Err(error) => Err(error),
2393        }
2394    };
2395    let [l0, l1] = engine.clip.radius;
2396    let scan = 64;
2397    let level_at = |k: usize| l0 + (l1 - l0) * k as f64 / scan as f64;
2398    if !whole(l1)? {
2399        return Ok(None);
2400    }
2401    let mut first = scan;
2402    while first > 0 && whole(level_at(first - 1))? {
2403        first -= 1;
2404    }
2405    if first == 0 {
2406        return Ok(None);
2407    }
2408    let (mut below, mut above) = (level_at(first - 1), level_at(first));
2409    for _ in 0..60 {
2410        let middle = 0.5 * (below + above);
2411        if whole(middle)? {
2412            above = middle;
2413        } else {
2414            below = middle;
2415        }
2416    }
2417    // The material must narrow to nothing there, not stop at some width. Near
2418    // the end the walls have already crossed unseen (the oracle stops seeing the
2419    // gap a little past the crest), so the crest is where the gap, measured
2420    // well below, extrapolates to zero.
2421    let gap = |level: f64| -> Result<f64, KernelRefusal> {
2422        Ok(TAU - engine.intervals_at(level)?.iter().map(|interval| interval.hi - interval.lo).sum::<f64>())
2423    };
2424    let step = 4e-5 * (l1 - l0);
2425    let (x0, x1, x2) = (below - 3.0 * step, below - 2.0 * step, below - step);
2426    let (g0, g1, g2) = (gap(x0)?, gap(x1)?, gap(x2)?);
2427    if !(g2 > 0.0 && g1 > g2 && g0 > g1) || g2 > 16.0 * (g1 - g2) {
2428        return Ok(None);
2429    }
2430    let curve = |x: f64| {
2431        g0 * (x - x1) * (x - x2) / ((x0 - x1) * (x0 - x2))
2432            + g1 * (x - x0) * (x - x2) / ((x1 - x0) * (x1 - x2))
2433            + g2 * (x - x0) * (x - x1) / ((x2 - x0) * (x2 - x1))
2434    };
2435    let (mut lo, mut hi) = (x2, below + step);
2436    if curve(hi) > 0.0 {
2437        return Ok(None);
2438    }
2439    for _ in 0..80 {
2440        let middle = 0.5 * (lo + hi);
2441        if curve(middle) > 0.0 {
2442            lo = middle;
2443        } else {
2444            hi = middle;
2445        }
2446    }
2447    Ok(Some(0.5 * (lo + hi)))
2448}
2449
2450fn find_events(
2451    engine: &Engine,
2452    a: f64,
2453    sa: &[Interval],
2454    b: f64,
2455    sb: &[Interval],
2456    breaks: &mut Vec<f64>,
2457    depth: usize,
2458) -> Result<(), KernelRefusal> {
2459    if compatible(engine, sa, sb) {
2460        return Ok(());
2461    }
2462    if depth <= 2 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
2463        let summary = |s: &[Interval]| -> Vec<(usize, usize, f64, f64)> {
2464            s.iter().map(|x| (x.lo_source, x.hi_source, (x.lo * 1e4).round() / 1e4, (x.hi * 1e4).round() / 1e4)).collect()
2465        };
2466        eprintln!("  find_events depth {depth} [{a:.9}, {b:.9}] {:?}\n      -> {:?} counters {:?}", summary(sa), summary(sb), engine.counters.get());
2467    }
2468    if b - a < 1e-13 * engine.scale || depth > 60 {
2469        breaks.push(0.5 * (a + b));
2470        return Ok(());
2471    }
2472    let m = 0.5 * (a + b);
2473    let sm = engine.intervals_at(m)?;
2474    // Both halves stay incompatible only where the reading flickers; follow the
2475    // half that changes first and let the other be found from the scan above.
2476    if !compatible(engine, sa, &sm) && !compatible(engine, &sm, sb) && depth > 8 {
2477        breaks.push(m);
2478        return Ok(());
2479    }
2480    find_events(engine, a, sa, m, &sm, breaks, depth + 1)?;
2481    find_events(engine, m, &sm, b, sb, breaks, depth + 1)
2482}
2483
2484/// The candidate of `source` at `level` nearest `guess` (or its critical point
2485/// at that level); `fallback` when the source has none there.
2486fn end_value(engine: &Engine, source: usize, level: f64, guess: f64, fallback: f64) -> f64 {
2487    let mut candidates = Vec::new();
2488    engine.source_candidates(source, level, &mut candidates);
2489    let mut options: Vec<f64> = candidates.iter().map(|c| c.0).collect();
2490    for critical in &engine.critical[source] {
2491        if (critical.0 - level).abs() < 1e-7 * engine.scale {
2492            options.push(critical.1);
2493        }
2494    }
2495    let mut best = fallback;
2496    let mut distance = f64::INFINITY;
2497    for option in options {
2498        let d = engine.o_difference(option, guess).abs();
2499        if d < distance {
2500            distance = d;
2501            best = guess + engine.o_difference(option, guess);
2502        }
2503    }
2504    best
2505}
2506
2507/// Sample a band (a, b) of constant structure; return its end pieces and the
2508/// intervals at its bottom and top.
2509#[allow(clippy::type_complexity)]
2510fn sample_band(
2511    engine: &Engine,
2512    a: f64,
2513    b: f64,
2514    mid: &[Interval],
2515    fold_ends: &[f64],
2516) -> Result<(Vec<Piece>, Vec<(f64, f64)>, Vec<(f64, f64)>), KernelRefusal> {
2517    let count = mid.len();
2518    let n = 24usize;
2519    // Next to a fold the extremum Newton flickers; keep every sample at least
2520    // this far from a fold end (the end itself reads the fold's critical point).
2521    let near_fold = fold_ends.iter().any(|f| (f - a).abs() < 1e-12 * engine.scale || (f - b).abs() < 1e-12 * engine.scale);
2522    let n_cap = if near_fold {
2523        let keep = (1e-4 * engine.scale).min((b - a) / 50.0);
2524        // smallest σ step: (b − a)(1 − cos(π/n))/2 ≥ keep
2525        let ratio = (1.0 - 2.0 * keep / (b - a)).clamp(-1.0, 1.0);
2526        ((PI / ratio.acos()).floor() as usize).max(8)
2527    } else {
2528        192
2529    };
2530    let mut n = n.min(n_cap);
2531    loop {
2532        let sigma: Vec<f64> = (0..=n).map(|k| k as f64 / n as f64).collect();
2533        let level_at = |s: f64| a + (b - a) * (1.0 - (PI * s).cos()) / 2.0;
2534        // Track every interval from the middle sample outward.
2535        let mut lo = vec![vec![f64::NAN; n + 1]; count];
2536        let mut hi = vec![vec![f64::NAN; n + 1]; count];
2537        let mut lo_src = vec![vec![0usize; n + 1]; count];
2538        let mut hi_src = vec![vec![0usize; n + 1]; count];
2539        let mid_index = n / 2;
2540        let mid_intervals = engine.intervals_at(level_at(sigma[mid_index]))?;
2541        if mid_intervals.len() != count {
2542            return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
2543        }
2544        for (i, interval) in mid_intervals.iter().enumerate() {
2545            lo[i][mid_index] = interval.lo;
2546            hi[i][mid_index] = interval.hi;
2547            lo_src[i][mid_index] = interval.lo_source;
2548            hi_src[i][mid_index] = interval.hi_source;
2549        }
2550        for direction in [-1i64, 1] {
2551            let mut k = mid_index as i64 + direction;
2552            while k > 0 && (k as usize) < n {
2553                let ku = k as usize;
2554                let previous = (ku as i64 - direction) as usize;
2555                let intervals = engine.intervals_at(level_at(sigma[ku]))?;
2556                if intervals.len() != count {
2557                    return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
2558                }
2559                for i in 0..count {
2560                    let centre = 0.5 * (lo[i][previous] + hi[i][previous]);
2561                    let nearest = intervals
2562                        .iter()
2563                        .min_by(|x, y| {
2564                            let dx = engine.o_difference(0.5 * (x.lo + x.hi), centre).abs();
2565                            let dy = engine.o_difference(0.5 * (y.lo + y.hi), centre).abs();
2566                            dx.total_cmp(&dy)
2567                        })
2568                        .expect("nonempty");
2569                    let shift = engine.o_difference(nearest.lo, lo[i][previous]) - (nearest.lo - lo[i][previous]);
2570                    lo[i][ku] = nearest.lo + shift;
2571                    hi[i][ku] = nearest.hi + shift;
2572                    lo_src[i][ku] = nearest.lo_source;
2573                    hi_src[i][ku] = nearest.hi_source;
2574                }
2575                k += direction;
2576            }
2577        }
2578        // Ends: the same source's value at exactly the band's end levels,
2579        // nearest the linear extrapolation of the two nearest samples.
2580        for i in 0..count {
2581            for (end, inner, inner2, inner3, level) in [(0usize, 1usize, 2usize, 3usize, a), (n, n - 1, n - 2, n - 3, b)] {
2582                // A quadratic in the sample index through the three nearest samples.
2583                // The levels cluster as σ² at the ends, so a regular end is quadratic
2584                // in σ and a √ end (a tip) linear: this is exact for both, where
2585                // `2v₁ − v₂` misses the first by the slope times a sample spacing. A
2586                // source whose domain ends at this level has no candidate there, so
2587                // the estimate IS the end value (1e-3 on the bar swung on its edge).
2588                let near = |values: &Vec<f64>| 2.0 * values[inner] - values[inner2];
2589                let quadratic = |values: &Vec<f64>| 3.0 * values[inner] - 3.0 * values[inner2] + values[inner3];
2590                lo[i][end] = end_value(engine, lo_src[i][inner], level, near(&lo[i]), quadratic(&lo[i]));
2591                hi[i][end] = end_value(engine, hi_src[i][inner], level, near(&hi[i]), quadratic(&hi[i]));
2592            }
2593        }
2594        // Accuracy: compare the fitted ends at the σ mid-points with exact values.
2595        let mut worst = 0.0f64;
2596        let mut worst_at = (0usize, 0usize, 0usize, 0.0f64, 0.0f64);
2597        for i in 0..count {
2598            for (side, values) in [&lo[i], &hi[i]].into_iter().enumerate() {
2599                let points: Vec<Vec3> = (0..=n)
2600                    .map(|k| reduced_to_plane(engine, level_at(sigma[k]), values[k]))
2601                    .collect();
2602                let curve = fit_piece(&points)?;
2603                let parameters = chord_parameters(&points);
2604                // The end spans too: an end value is read differently from the samples.
2605                for k in (1..n - 1).step_by((n / 8).max(1)).chain([0, n - 1]) {
2606                    let s = 0.5 * (sigma[k] + sigma[k + 1]);
2607                    let level = level_at(s);
2608                    let intervals = engine.intervals_at(level)?;
2609                    let guess = 0.5 * (values[k] + values[k + 1]);
2610                    let exact = intervals
2611                        .iter()
2612                        .flat_map(|x| [x.lo, x.hi])
2613                        .map(|o| guess + engine.o_difference(o, guess))
2614                        .min_by(|x, y| (x - guess).abs().total_cmp(&(y - guess).abs()))
2615                        .unwrap_or(guess);
2616                    let truth = reduced_to_plane(engine, level, exact);
2617                    let bracket = (parameters[k.saturating_sub(1)], parameters[(k + 2).min(n)]);
2618                    let off = distance_in_bracket(&curve, truth, bracket.0, bracket.1)?;
2619                    if off > worst {
2620                        worst = off;
2621                        worst_at = (i, side, k, level, exact);
2622                    }
2623                }
2624            }
2625        }
2626        if n >= n_cap && worst > 1e-6 * engine.scale {
2627            return Err(refuse(
2628                "fit_tolerance",
2629                format!(
2630                    "a boundary piece of the band {a}..{b} stays {worst:.3e} off its envelope at {n} samples \
2631                     (interval {}, {}, sample {}, level {}, o {})",
2632                    worst_at.0,
2633                    if worst_at.1 == 0 { "lo" } else { "hi" },
2634                    worst_at.2,
2635                    worst_at.3,
2636                    worst_at.4
2637                ),
2638            ));
2639        }
2640        if worst <= 1e-8 * engine.scale || n >= n_cap {
2641            let mut pieces = Vec::new();
2642            for i in 0..count {
2643                for values in [&lo[i], &hi[i]] {
2644                    pieces.push(Piece {
2645                        points: (0..=n).map(|k| (level_at(sigma[k]), values[k])).collect(),
2646                        sigma: sigma.clone(),
2647                        level_segment: false,
2648                    });
2649                }
2650            }
2651            let bottoms = (0..count).map(|i| (lo[i][0], hi[i][0])).collect();
2652            let tops = (0..count).map(|i| (lo[i][n], hi[i][n])).collect();
2653            return Ok((pieces, bottoms, tops));
2654        }
2655        n = (n * 2).min(n_cap);
2656    }
2657}
2658
2659/// Chord-length parameters for interpolating `points`, normalised to [0, 1].
2660/// Not the sampling's own `σ`: the levels cluster at both ends of a band, which
2661/// near a tip is what keeps a √ end smooth, but at a regular end it makes the
2662/// curve's speed vanish there, and an interpolant through a zero-speed end
2663/// cusps (the screw groove's wall folded where it met the closing arc).
2664fn chord_parameters(points: &[Vec3]) -> Vec<f64> {
2665    let mut parameters = vec![0.0; points.len()];
2666    for k in 1..points.len() {
2667        parameters[k] = parameters[k - 1] + points[k].sub(points[k - 1]).length();
2668    }
2669    let total = parameters[points.len() - 1].max(1e-300);
2670    for parameter in &mut parameters {
2671        *parameter /= total;
2672    }
2673    let last = points.len() - 1;
2674    parameters[last] = 1.0;
2675    parameters
2676}
2677
2678fn fit_piece(points: &[Vec3]) -> Result<NurbsCurve, KernelRefusal> {
2679    interpolate_curve(points, 3, &chord_parameters(points)).or_refuse(KernelStage::Fragment, "piece_fit")
2680}
2681
2682/// Distance from `target` to `curve` over the parameter bracket [a, b].
2683fn distance_in_bracket(curve: &NurbsCurve, target: Vec3, a: f64, b: f64) -> Result<f64, KernelRefusal> {
2684    let distance = |t: f64| -> Result<f64, KernelRefusal> {
2685        Ok(curve.evaluate(t).or_refuse(KernelStage::Fragment, "piece_eval")?.sub(target).length())
2686    };
2687    let ratio = 0.618_033_988_749_894_9;
2688    let (mut lo, mut hi) = (a, b);
2689    for _ in 0..80 {
2690        let c = hi - ratio * (hi - lo);
2691        let d = lo + ratio * (hi - lo);
2692        if distance(c)? < distance(d)? {
2693            hi = d;
2694        } else {
2695            lo = c;
2696        }
2697    }
2698    distance(0.5 * (lo + hi))
2699}
2700
2701/// `below Δ above` as intervals of `o` (both lists at one level).
2702fn symmetric_difference(below: &[(f64, f64)], above: &[(f64, f64)], angular: bool, tolerance: f64) -> Vec<(f64, f64)> {
2703    // Normalise to [base, base + 2π) for angular values, splitting wrapped ones.
2704    let normalise = |list: &[(f64, f64)]| -> Vec<(f64, f64)> {
2705        let mut out = Vec::new();
2706        for &(lo, hi) in list {
2707            if angular {
2708                let shift = lo.rem_euclid(TAU) - lo;
2709                let (lo, hi) = (lo + shift, hi + shift);
2710                if hi > TAU {
2711                    out.push((lo, TAU));
2712                    out.push((0.0, hi - TAU));
2713                } else {
2714                    out.push((lo, hi));
2715                }
2716            } else {
2717                out.push((lo, hi));
2718            }
2719        }
2720        out
2721    };
2722    let a = normalise(below);
2723    let b = normalise(above);
2724    let mut cuts: Vec<f64> = a.iter().chain(&b).flat_map(|&(lo, hi)| [lo, hi]).collect();
2725    cuts.sort_by(|x, y| x.total_cmp(y));
2726    cuts.dedup_by(|y, x| (*y - *x).abs() < tolerance);
2727    let inside = |list: &Vec<(f64, f64)>, x: f64| list.iter().any(|&(lo, hi)| x > lo && x < hi);
2728    let mut out: Vec<(f64, f64)> = Vec::new();
2729    for w in cuts.windows(2) {
2730        let m = 0.5 * (w[0] + w[1]);
2731        if inside(&a, m) != inside(&b, m) && w[1] - w[0] > tolerance {
2732            if let Some(last) = out.last_mut() {
2733                if (last.1 - w[0]).abs() < tolerance {
2734                    last.1 = w[1];
2735                    continue;
2736                }
2737            }
2738            out.push((w[0], w[1]));
2739        }
2740    }
2741    if angular && out.len() > 1 {
2742        let first = out[0];
2743        let last = *out.last().expect("nonempty");
2744        if first.0.abs() < tolerance && (last.1 - TAU).abs() < tolerance {
2745            out.pop();
2746            out[0] = (last.0 - TAU, first.1);
2747        }
2748    }
2749    out
2750}
2751
2752/// A reduced point on the section plane the body is built from.
2753fn reduced_to_plane(engine: &Engine, level: f64, o: f64) -> Vec3 {
2754    let clip = engine.clip;
2755    match engine.class {
2756        Class::Screw(c) => {
2757            let z = clip.axial[0];
2758            let alpha = o + z / c;
2759            engine.frame.world([level * alpha.cos(), level * alpha.sin(), z])
2760        }
2761        Class::Turn => {
2762            let theta = clip.angle.map(|a| a[0]).unwrap_or(0.0);
2763            engine.frame.world([level * theta.cos(), level * theta.sin(), o])
2764        }
2765        Class::Planar => engine.frame.world([level * o.cos(), level * o.sin(), clip.axial[0]]),
2766    }
2767}
2768
2769/// Join pieces end to end into closed loops (in the section plane).
2770fn assemble_loops(engine: &Engine, pieces: Vec<Piece>) -> Result<Vec<Vec<Piece>>, KernelRefusal> {
2771    let tolerance = 1e-6 * engine.scale;
2772    let ends: Vec<(Vec3, Vec3)> = pieces
2773        .iter()
2774        .map(|piece| {
2775            let first = piece.points[0];
2776            let last = *piece.points.last().expect("nonempty");
2777            (reduced_to_plane(engine, first.0, first.1), reduced_to_plane(engine, last.0, last.1))
2778        })
2779        .collect();
2780    let mut used = vec![false; pieces.len()];
2781    let mut orders: Vec<Vec<(usize, bool)>> = Vec::new();
2782    for start in 0..pieces.len() {
2783        if used[start] {
2784            continue;
2785        }
2786        used[start] = true;
2787        let mut order = vec![(start, false)];
2788        let origin = ends[start].0;
2789        let mut cursor = ends[start].1;
2790        let mut guard = 0;
2791        while cursor.sub(origin).length() > tolerance {
2792            guard += 1;
2793            if guard > pieces.len() {
2794                return Err(refuse("loop_open", "the section's boundary does not close".into()));
2795            }
2796            let mut next = None;
2797            let mut best = tolerance;
2798            for k in 0..pieces.len() {
2799                if used[k] {
2800                    continue;
2801                }
2802                for (reversed, end) in [(false, ends[k].0), (true, ends[k].1)] {
2803                    let d = end.sub(cursor).length();
2804                    if d < best {
2805                        best = d;
2806                        next = Some((k, reversed));
2807                    }
2808                }
2809            }
2810            let Some((k, reversed)) = next else {
2811                return Err(refuse(
2812                    "loop_open",
2813                    format!("the section's boundary does not close (a gap at {:?})", cursor),
2814                ));
2815            };
2816            used[k] = true;
2817            order.push((k, reversed));
2818            cursor = if reversed { ends[k].0 } else { ends[k].1 };
2819        }
2820        orders.push(order);
2821    }
2822    if orders.len() > 1 {
2823        // A loop inside another is a hole; not built here.
2824        let polygon = |order: &Vec<(usize, bool)>| -> Vec<Vec3> {
2825            order
2826                .iter()
2827                .flat_map(|&(k, _)| pieces[k].points.iter().map(|p| reduced_to_plane(engine, p.0, p.1)))
2828                .collect()
2829        };
2830        let polygons: Vec<Vec<Vec3>> = orders.iter().map(polygon).collect();
2831        for (i, a) in polygons.iter().enumerate() {
2832            for (j, b) in polygons.iter().enumerate() {
2833                if i != j && point_in_polygon(engine, a[0], b) {
2834                    return Err(refuse("section_hole", "the section has a hole, which is not built here".into()));
2835                }
2836            }
2837        }
2838    }
2839    let mut slots: Vec<Option<Piece>> = pieces.into_iter().map(Some).collect();
2840    Ok(orders
2841        .into_iter()
2842        .map(|order| {
2843            order
2844                .into_iter()
2845                .map(|(k, reversed)| {
2846                    let mut piece = slots[k].take().expect("each piece once");
2847                    if reversed {
2848                        piece.points.reverse();
2849                        piece.sigma = piece.sigma.iter().rev().map(|s| 1.0 - s).collect();
2850                    }
2851                    piece
2852                })
2853                .collect()
2854        })
2855        .collect())
2856}
2857
2858fn point_in_polygon(engine: &Engine, p: Vec3, polygon: &[Vec3]) -> bool {
2859    let local = |x: Vec3| -> [f64; 2] {
2860        let l = engine.frame.local(x);
2861        match engine.class {
2862            Class::Turn => [l[0].hypot(l[1]), l[2]],
2863            _ => [l[0], l[1]],
2864        }
2865    };
2866    let q = local(p);
2867    let mut inside = false;
2868    for k in 0..polygon.len() {
2869        let a = local(polygon[k]);
2870        let b = local(polygon[(k + 1) % polygon.len()]);
2871        if (a[1] > q[1]) != (b[1] > q[1]) {
2872            let x = a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
2873            if x > q[0] {
2874                inside = !inside;
2875            }
2876        }
2877    }
2878    inside
2879}
2880
2881/// Fit each piece and build the body for one loop.
2882fn build_body(engine: &Engine, pieces: &[Piece]) -> Result<BrepSolid, KernelRefusal> {
2883    let clip = engine.clip;
2884    let mut profile: Vec<NurbsCurve> = Vec::new();
2885    // Snap shared ends so consecutive curves meet exactly.
2886    let count = pieces.len();
2887    let mut start_points: Vec<Vec3> = pieces
2888        .iter()
2889        .map(|p| reduced_to_plane(engine, p.points[0].0, p.points[0].1))
2890        .collect();
2891    // A level segment is built exactly from its own ends, so the piece after
2892    // it starts where it ends.
2893    for (index, piece) in pieces.iter().enumerate() {
2894        if piece.level_segment && engine.class != Class::Turn {
2895            let (level, o1) = *piece.points.last().expect("nonempty");
2896            start_points[(index + 1) % count] = reduced_to_plane(engine, level, o1);
2897        }
2898    }
2899    for (index, piece) in pieces.iter().enumerate() {
2900        let start = start_points[index];
2901        let end = start_points[(index + 1) % count];
2902        if piece.level_segment {
2903            let (level, o0) = piece.points[0];
2904            let o1 = piece.points.last().expect("nonempty").1;
2905            let curve = match engine.class {
2906                Class::Turn => make_line(start, end),
2907                Class::Screw(c) => {
2908                    let shift = clip.axial[0] / c;
2909                    level_arc(engine, level, o0 + shift, o1 + shift)
2910                }
2911                Class::Planar => level_arc(engine, level, o0, o1),
2912            }
2913            .or_refuse(KernelStage::Fragment, "level_segment")?;
2914            profile.push(curve);
2915            continue;
2916        }
2917        let mut points: Vec<Vec3> = piece
2918            .points
2919            .iter()
2920            .map(|p| reduced_to_plane(engine, p.0, p.1))
2921            .collect();
2922        points[0] = start;
2923        let last = points.len() - 1;
2924        points[last] = end;
2925        profile.push(fit_piece(&points)?);
2926    }
2927    match engine.class {
2928        Class::Screw(c) => {
2929            let span = (clip.axial[1] - clip.axial[0]) / c; // radians of screw
2930            // Over a crest the walls are re-carried below to high order, so 12 a turn is enough.
2931            let per_turn = if engine.crest_gap { 12.0 } else { 64.0 };
2932            // Every turn is the screw image of the first: build one and stack
2933            // copies, so no surface or edge runs the thread's whole length.
2934            let rows = if engine.crest_gap && thread_rows_on() { (span.abs() / TAU / TURNS_PER_ROW).round().max(1.0) as usize } else { 1 };
2935            let span = span / rows as f64;
2936            let sections = ((span.abs() / TAU * per_turn).ceil() as usize).max(8) + 1;
2937            let mut lofted = Vec::with_capacity(sections);
2938            for k in 0..sections {
2939                let angle = span * k as f64 / (sections - 1) as f64;
2940                let lift = engine.frame.ez.scale(c * angle);
2941                let moved: Vec<NurbsCurve> = profile
2942                    .iter()
2943                    .map(|curve| transform_curve(curve, |x| rotate(x, clip.axis_origin, engine.frame.ez, angle).add(lift)))
2944                    .collect::<Result<_, _>>()?;
2945                lofted.push(moved);
2946            }
2947            let mut body = loft_profile_brep(&lofted)?;
2948            if engine.crest_gap {
2949                carry_along_screw(&mut body, engine, c, span, sections - 1)?;
2950            }
2951            if rows > 1 {
2952                body = stack_screw_rows(&body, engine, c, span, sections - 1, rows)?;
2953            }
2954            Ok(body)
2955        }
2956        Class::Turn => {
2957            let angle = clip.angle.map(|a| a[1] - a[0]).unwrap_or(TAU);
2958            revolve_profile_brep(&profile, clip.axis_origin, engine.frame.ez, angle)
2959                .or_refuse(KernelStage::Fragment, "revolve")
2960        }
2961        Class::Planar => extrude_profile_brep(&profile, engine.frame.ez, clip.axial[1] - clip.axial[0]),
2962    }
2963}
2964
2965fn level_arc(engine: &Engine, level: f64, a0: f64, a1: f64) -> Result<NurbsCurve, String> {
2966    let centre = engine.frame.world([0.0, 0.0, match engine.class {
2967        Class::Screw(_) | Class::Planar => engine.clip.axial[0],
2968        Class::Turn => 0.0,
2969    }]);
2970    let (start, end) = if a1 >= a0 { (a0, a1) } else { (a1, a0) };
2971    let arc = make_arc(centre, engine.frame.ex, engine.frame.ey, level, start, end)?;
2972    if a1 >= a0 {
2973        Ok(arc)
2974    } else {
2975        arc.reversed()
2976    }
2977}
2978
2979fn transform_curve(curve: &NurbsCurve, map: impl Fn(Vec3) -> Vec3) -> Result<NurbsCurve, KernelRefusal> {
2980    let control_points = curve
2981        .control_points
2982        .iter()
2983        .map(|p| {
2984            let w = p.w;
2985            let x = map(Vec3::new(p.x / w, p.y / w, p.z / w));
2986            crate::Vec4 { x: x.x * w, y: x.y * w, z: x.z * w, w }
2987        })
2988        .collect();
2989    NurbsCurve::new(curve.degree, curve.knots.clone(), control_points).or_refuse(KernelStage::Fragment, "curve_new")
2990}
2991
2992/// Refuse a screw or turn whose first or last position reaches the clip.
2993fn check_ends_clear(engine: &Engine) -> Result<(), KernelRefusal> {
2994    let clip = engine.clip;
2995    // Tool boundary samples at its own place (clip frame).
2996    let mut samples: Vec<[f64; 3]> = Vec::new();
2997    for (index, source) in engine.sources.iter().enumerate() {
2998        if let Some(grid) = &engine.grids[index] {
2999            for i in 0..grid.nu {
3000                for j in 0..grid.nv {
3001                    let (u, v) = Engine::grid_param(grid, i, j);
3002                    samples.push(engine.frame.local(engine.source_point(*source, u, v)));
3003                }
3004            }
3005        }
3006    }
3007    let margin = 1e-6 * engine.scale;
3008    for t in [engine.motion.from, engine.motion.to] {
3009        for p in &samples {
3010            let x = engine.frame.local(engine.motion_apply(t, engine.frame.world(*p)));
3011            let level = x[0].hypot(x[1]);
3012            if level > clip.radius[1] + margin || level < clip.radius[0] - margin {
3013                continue;
3014            }
3015            if x[2] < clip.axial[0] - margin || x[2] > clip.axial[1] + margin {
3016                continue;
3017            }
3018            if let Some([a0, a1]) = clip.angle {
3019                let angle = x[1].atan2(x[0]);
3020                let mid = 0.5 * (a0 + a1);
3021                let rel = wrap_pi(angle - mid);
3022                if rel.abs() > 0.5 * (a1 - a0) + 1e-9 {
3023                    continue;
3024                }
3025            }
3026            return Err(refuse(
3027                "ends_reach_clip",
3028                format!(
3029                    "the tool reaches the clip region at the motion's {} (t = {t}); the sweep is \
3030                     not invariant there, so the region would be wrong",
3031                    if t == engine.motion.from { "start" } else { "end" }
3032                ),
3033            ));
3034        }
3035    }
3036    Ok(())
3037}
3038