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
288fn refuse(what: &'static str, message: String) -> KernelRefusal {
289    KernelRefusal::unsupported(KernelStage::Classify, what, format!("{SOLID_SWEEP_REFUSAL}: {message}"))
290}
291
292fn rotate(x: Vec3, origin: Vec3, axis: Vec3, angle: f64) -> Vec3 {
293    origin.add(x.sub(origin).rotate_about(axis, angle))
294}
295
296impl ScrewStep {
297    fn apply(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
298        rotate(x, self.axis_origin, axis, self.turn * t).add(axis.scale(self.advance * t))
299    }
300    fn invert(&self, t: f64, x: Vec3, axis: Vec3) -> Vec3 {
301        rotate(x.sub(axis.scale(self.advance * t)), self.axis_origin, axis, -self.turn * t)
302    }
303}
304
305/// Orthonormal frame of the clip: (ex, ey, ez).
306#[derive(Clone, Copy, Debug)]
307struct Frame {
308    origin: Vec3,
309    ex: Vec3,
310    ey: Vec3,
311    ez: Vec3,
312}
313
314impl Frame {
315    fn local(&self, x: Vec3) -> [f64; 3] {
316        let d = x.sub(self.origin);
317        [d.dot(self.ex), d.dot(self.ey), d.dot(self.ez)]
318    }
319    fn world(&self, p: [f64; 3]) -> Vec3 {
320        self.origin
321            .add(self.ex.scale(p[0]))
322            .add(self.ey.scale(p[1]))
323            .add(self.ez.scale(p[2]))
324    }
325}
326
327#[derive(Clone, Copy, Debug, PartialEq)]
328enum Class {
329    /// Axial advance per radian of the screw, in the clip frame (nonzero).
330    Screw(f64),
331    Turn,
332    Planar,
333}
334
335/// A tool-boundary source: a face patch over a 2D parameter box, or an edge
336/// over a 1D parameter interval. `periodic` marks a parameter that wraps.
337#[derive(Clone, Copy, Debug)]
338enum Source {
339    /// Revolved meridian piece `piece`, parameters (s ∈ [0,1], φ ∈ [0,2π)).
340    RevolvedFace { piece: usize },
341    /// Revolved meridian corner at the start of piece `piece`, parameter φ.
342    RevolvedCorner { piece: usize },
343    /// Prism outline piece `piece` carried through time: (s ∈ [0,1], t).
344    PrismFace { piece: usize },
345    /// Prism outline vertex at the start of piece `piece`, parameter t.
346    PrismVertex { piece: usize },
347    /// Prism outline piece `piece` at the motion's first (`end = false`) or last
348    /// position, parameter s.
349    PrismEnd { piece: usize, end: bool },
350}
351
352struct Engine<'a> {
353    tool: &'a SweptTool,
354    motion: &'a SolidMotion,
355    axes: Vec<Vec3>,
356    frame: Frame,
357    class: Class,
358    clip: &'a SweepClip,
359    sources: Vec<Source>,
360    /// Per face source: its parameter grid (nu × nv) of reduced coordinates.
361    grids: Vec<Option<Grid>>,
362    /// Per source: its critical points of the level, as (level, o).
363    critical: Vec<Vec<(f64, f64)>>,
364    /// Per face source: the parameters of its critical points (tips), which
365    /// seed the extremum Newton where the level's contour is smaller than a cell.
366    critical_params: Vec<Vec<(f64, f64)>>,
367    /// Angular `o` (wraps at 2π) or not.
368    angular: bool,
369    /// Prism height (along its normal) the planar sources are evaluated at.
370    prism_height: f64,
371    scale: f64,
372    /// `intervals_at` by level bits: the sampling revisits levels (a refined
373    /// band keeps every level of the coarser one).
374    cache: std::cell::RefCell<std::collections::HashMap<u64, Vec<Interval>>>,
375    /// The motion ends where it began as a set (the plus wheel: one turn of the
376    /// wheel, one and a half of the tool), so there are no end positions; piece
377    /// `k` at the end continues as piece `wrap[k]` at the start.
378    wrap: Option<Vec<usize>>,
379    /// Debug counters: (levels evaluated, oracle calls, oracle steps, seconds in
380    /// candidates, seconds in the oracle).
381    counters: std::cell::Cell<(usize, usize, usize, f64, f64)>,
382}
383
384struct Grid {
385    nu: usize,
386    nv: usize,
387    u: [f64; 2],
388    v: [f64; 2],
389    v_periodic: bool,
390    values: Vec<(f64, f64)>,
391}
392
393/// One interval of the region at a level, with the sources of its ends.
394#[derive(Clone, Copy, Debug)]
395struct Interval {
396    lo: f64,
397    hi: f64,
398    lo_source: usize,
399    hi_source: usize,
400}
401
402fn wrap_pi(angle: f64) -> f64 {
403    let wrapped = (angle + PI).rem_euclid(TAU) - PI;
404    wrapped
405}
406
407impl<'a> Engine<'a> {
408    fn motion_apply(&self, t: f64, x: Vec3) -> Vec3 {
409        self.motion
410            .steps
411            .iter()
412            .zip(&self.axes)
413            .fold(x, |x, (step, axis)| step.apply(t, x, *axis))
414    }
415
416    fn motion_invert(&self, t: f64, x: Vec3) -> Vec3 {
417        self.motion
418            .steps
419            .iter()
420            .zip(&self.axes)
421            .rev()
422            .fold(x, |x, (step, axis)| step.invert(t, x, *axis))
423    }
424
425    /// Reduced coordinates of a clip-frame point.
426    fn reduce(&self, p: [f64; 3]) -> (f64, f64) {
427        let level = p[0].hypot(p[1]);
428        match self.class {
429            Class::Screw(c) => (level, p[1].atan2(p[0]) - p[2] / c),
430            Class::Turn => (level, p[2]),
431            Class::Planar => (level, p[1].atan2(p[0])),
432        }
433    }
434
435    fn tool_signed_distance(&self, x: Vec3) -> f64 {
436        match self.tool {
437            SweptTool::Revolved { axis_origin, axis_direction, meridian, .. } => {
438                let axis = axis_direction.normalized_or(*axis_direction);
439                let d = x.sub(*axis_origin);
440                let e = d.dot(axis);
441                let radial = d.sub(axis.scale(e)).length();
442                outline_signed_distance(meridian, [radial, e])
443            }
444            SweptTool::Prism { origin, normal, x_axis, outline, from, to } => {
445                let n = normal.normalized_or(*normal);
446                let xa = x_axis.normalized_or(*x_axis);
447                let ya = n.cross(xa);
448                let d = x.sub(*origin);
449                let h = d.dot(n);
450                let planar = outline_signed_distance(outline, [d.dot(xa), d.dot(ya)]);
451                planar.max(from - h).max(h - to)
452            }
453        }
454    }
455
456    /// The world point of a source at its parameters (at the tool's own place
457    /// for revolved tools; carried by the motion for prisms).
458    fn source_point(&self, source: Source, u: f64, v: f64) -> Vec3 {
459        match (self.tool, source) {
460            (SweptTool::Revolved { axis_origin, axis_direction, radial, meridian }, _) => {
461                let axis = axis_direction.normalized_or(*axis_direction);
462                let r0 = radial.sub(axis.scale(radial.dot(axis))).normalized_or(*radial);
463                let r1 = axis.cross(r0);
464                let (m, phi) = match source {
465                    Source::RevolvedFace { piece } => (meridian[piece].point(u), v),
466                    Source::RevolvedCorner { piece } => (meridian[piece].point(0.0), u),
467                    _ => unreachable!(),
468                };
469                axis_origin
470                    .add(r0.scale(m[0] * phi.cos()))
471                    .add(r1.scale(m[0] * phi.sin()))
472                    .add(axis.scale(m[1]))
473            }
474            (SweptTool::Prism { origin, normal, x_axis, outline, .. }, _) => {
475                let n = normal.normalized_or(*normal);
476                let xa = x_axis.normalized_or(*x_axis);
477                let ya = n.cross(xa);
478                let (q, t) = match source {
479                    Source::PrismFace { piece } => (outline[piece].point(u), v),
480                    Source::PrismVertex { piece } => (outline[piece].point(0.0), u),
481                    Source::PrismEnd { piece, end } => (
482                        outline[piece].point(u),
483                        if end { self.motion.to } else { self.motion.from },
484                    ),
485                    _ => unreachable!(),
486                };
487                let x = origin
488                    .add(xa.scale(q[0]))
489                    .add(ya.scale(q[1]))
490                    .add(n.scale(self.prism_height));
491                self.motion_apply(t, x)
492            }
493        }
494    }
495
496    fn source_reduced(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
497        self.reduce(self.frame.local(self.source_point(source, u, v)))
498    }
499
500    fn is_face(source: Source) -> bool {
501        matches!(source, Source::RevolvedFace { .. } | Source::PrismFace { .. })
502    }
503
504    /// Parameter domain of a source: (u range, v range, v periodic).
505    fn domain(&self, source: Source) -> ([f64; 2], [f64; 2], bool) {
506        let time = [self.motion.from, self.motion.to];
507        match source {
508            Source::RevolvedFace { .. } => ([0.0, 1.0], [0.0, TAU], true),
509            Source::RevolvedCorner { .. } => ([0.0, TAU], [0.0, 0.0], false),
510            Source::PrismFace { .. } => ([0.0, 1.0], time, false),
511            Source::PrismVertex { .. } => (time, [0.0, 0.0], false),
512            Source::PrismEnd { .. } => ([0.0, 1.0], [0.0, 0.0], false),
513        }
514    }
515
516    /// Is the face's first parameter periodic (a full circle in the meridian or
517    /// outline, which has no corner to bound it)?
518    fn u_periodic(&self, source: Source) -> bool {
519        match (self.tool, source) {
520            (SweptTool::Revolved { meridian, .. }, Source::RevolvedFace { piece }) => meridian[piece].is_full_circle(),
521            (SweptTool::Prism { outline, .. }, Source::PrismFace { piece }) => outline[piece].is_full_circle(),
522            _ => false,
523        }
524    }
525
526    fn o_difference(&self, a: f64, b: f64) -> f64 {
527        if self.angular {
528            wrap_pi(a - b)
529        } else {
530            a - b
531        }
532    }
533
534    /// Newton on {ℓ = level, det(∇o, ∇ℓ) = 0} over a face source.
535    fn face_extremum(&self, source: Source, level: f64, mut u: f64, mut v: f64) -> Option<(f64, f64, f64)> {
536        let (ur, vr, periodic) = self.domain(source);
537        let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
538        let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
539        let residual = |u: f64, v: f64| -> [f64; 2] {
540            let (l, o) = self.source_reduced(source, u, v);
541            let (lu1, ou1) = self.source_reduced(source, u + hu, v);
542            let (lu0, ou0) = self.source_reduced(source, u - hu, v);
543            let (lv1, ov1) = self.source_reduced(source, u, v + hv);
544            let (lv0, ov0) = self.source_reduced(source, u, v - hv);
545            let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
546            let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
547            let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
548            // det scaled by the level's gradient so its size is that of ∇o.
549            let norm = lu.hypot(lv).max(1e-300);
550            let _ = o;
551            [l - level, (ou * lv - ov * lu) / norm]
552        };
553        let mut f = residual(u, v);
554        for _ in 0..60 {
555            let (du_step, dv_step) = (hu * 10.0, hv * 10.0);
556            let fu1 = residual(u + du_step, v);
557            let fu0 = residual(u - du_step, v);
558            let fv1 = residual(u, v + dv_step);
559            let fv0 = residual(u, v - dv_step);
560            let j = [
561                [(fu1[0] - fu0[0]) / (2.0 * du_step), (fv1[0] - fv0[0]) / (2.0 * dv_step)],
562                [(fu1[1] - fu0[1]) / (2.0 * du_step), (fv1[1] - fv0[1]) / (2.0 * dv_step)],
563            ];
564            let det = j[0][0] * j[1][1] - j[0][1] * j[1][0];
565            if !det.is_finite() || det.abs() < 1e-300 {
566                return None;
567            }
568            let du = -(f[0] * j[1][1] - f[1] * j[0][1]) / det;
569            let dv = -(j[0][0] * f[1] - j[1][0] * f[0]) / det;
570            let norm0 = f[0].hypot(f[1]);
571            let mut step = 1.0;
572            let mut accepted = false;
573            while step > 1e-4 {
574                let (nu, nv) = (u + step * du, v + step * dv);
575                let candidate = residual(nu, nv);
576                if candidate[0].hypot(candidate[1]) < norm0 || norm0 < 1e-14 * self.scale {
577                    u = nu;
578                    v = nv;
579                    f = candidate;
580                    accepted = true;
581                    break;
582                }
583                step *= 0.5;
584            }
585            if !accepted {
586                break;
587            }
588            if (du * step).abs() < 1e-15 * (ur[1] - ur[0]) && (dv * step).abs() < 1e-15 * (vr[1] - vr[0]).max(1.0) {
589                break;
590            }
591        }
592        if periodic {
593            v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
594        }
595        if self.u_periodic(source) {
596            u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
597        }
598        let tolerance = 1e-9;
599        if u < ur[0] - tolerance || u > ur[1] + tolerance || (!periodic && (v < vr[0] - tolerance || v > vr[1] + tolerance)) {
600            return None;
601        }
602        if f[0].abs() > 1e-10 * self.scale || f[1].abs() > 1e-6 {
603            return None;
604        }
605        let (l, o) = self.source_reduced(source, u.clamp(ur[0], ur[1]), v);
606        Some((l, o, u))
607    }
608
609    fn build_grid(&self, source: Source) -> Grid {
610        let (u, v, v_periodic) = self.domain(source);
611        let (nu, nv) = match source {
612            Source::RevolvedFace { .. } => (64, 256),
613            _ => {
614                let turn = self
615                    .motion
616                    .steps
617                    .iter()
618                    .map(|step| step.turn.abs())
619                    .fold(0.0, f64::max)
620                    * (self.motion.to - self.motion.from).abs();
621                (48, ((turn * 128.0) as usize).clamp(256, 4096))
622            }
623        };
624        let mut values = Vec::with_capacity(nu * nv);
625        for i in 0..nu {
626            let uu = u[0] + (u[1] - u[0]) * i as f64 / (nu - 1) as f64;
627            for j in 0..nv {
628                let vv = if v_periodic {
629                    v[0] + (v[1] - v[0]) * j as f64 / nv as f64
630                } else {
631                    v[0] + (v[1] - v[0]) * j as f64 / (nv - 1) as f64
632                };
633                values.push(self.source_reduced(source, uu, vv));
634            }
635        }
636        Grid { nu, nv, u, v, v_periodic, values }
637    }
638
639    fn grid_param(grid: &Grid, i: usize, j: usize) -> (f64, f64) {
640        let u = grid.u[0] + (grid.u[1] - grid.u[0]) * i as f64 / (grid.nu - 1) as f64;
641        let v = if grid.v_periodic {
642            grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / grid.nv as f64
643        } else {
644            grid.v[0] + (grid.v[1] - grid.v[0]) * j as f64 / (grid.nv - 1) as f64
645        };
646        (u, v)
647    }
648
649    /// Candidates of one source at a level: (o, source index).
650    fn source_candidates(&self, index: usize, level: f64, out: &mut Vec<(f64, usize)>) {
651        let source = self.sources[index];
652        if Self::is_face(source) {
653            let grid = self.grids[index].as_ref().expect("face grid");
654            // Crossings of the level along grid edges, then seeds where o is a
655            // local extremum among the crossings of neighbouring cells.
656            let at = |i: usize, j: usize| grid.values[i * grid.nv + j % grid.nv];
657            let jmax = if grid.v_periodic { grid.nv } else { grid.nv - 1 };
658            let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
659            for i in 0..grid.nu {
660                for j in 0..jmax {
661                    let a = at(i, j);
662                    let b = at(i, j + 1);
663                    if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
664                        let w = (level - a.0) / (b.0 - a.0);
665                        let o = a.1 + self.o_difference(b.1, a.1) * w;
666                        let (u, v0) = Self::grid_param(grid, i, j);
667                        let (_, v1) = Self::grid_param(grid, i, j + 1);
668                        let v1 = if grid.v_periodic && j + 1 == grid.nv { grid.v[1] } else { v1 };
669                        crossings.push((i, j, o, u, v0 + (v1 - v0) * w));
670                    }
671                }
672            }
673            for i in 0..grid.nu.saturating_sub(1) {
674                for j in 0..grid.nv {
675                    let a = at(i, j);
676                    let b = at(i + 1, j);
677                    if (a.0 - level) * (b.0 - level) <= 0.0 && a.0 != b.0 {
678                        let w = (level - a.0) / (b.0 - a.0);
679                        let o = a.1 + self.o_difference(b.1, a.1) * w;
680                        let (u0, v) = Self::grid_param(grid, i, j);
681                        let (u1, _) = Self::grid_param(grid, i + 1, j);
682                        crossings.push((i, j, o, u0 + (u1 - u0) * w, v));
683                    }
684                }
685            }
686            let mut found: Vec<(f64, f64, f64)> = Vec::new();
687            let push = |found: &mut Vec<(f64, f64, f64)>, result: Option<(f64, f64, f64)>| {
688                if let Some((_, o_new, u_new)) = result {
689                    if !found
690                        .iter()
691                        .any(|&(o_old, u_old, _)| self.o_difference(o_old, o_new).abs() < 1e-11 && (u_old - u_new).abs() < 1e-6)
692                    {
693                        found.push((o_new, u_new, 0.0));
694                    }
695                }
696            };
697            // Near a tip the contour can be smaller than a cell: seed from the
698            // tip itself, pushed off it in four directions.
699            if let Some(params) = self.critical_params.get(index) {
700                let (ur, vr, _) = self.domain(source);
701                for &(u, v) in params {
702                    for (du, dv) in [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)] {
703                        let (su, sv) = (u + du * 1e-3 * (ur[1] - ur[0]), v + dv * 1e-3 * (vr[1] - vr[0]));
704                        push(&mut found, self.face_extremum(source, level, su, sv));
705                    }
706                }
707            }
708            let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
709            for (k, &(i, j, _, _, _)) in crossings.iter().enumerate() {
710                buckets.entry((i, j)).or_default().push(k);
711            }
712            for (k, &(i, j, o, u, v)) in crossings.iter().enumerate() {
713                let mut is_max = true;
714                let mut is_min = true;
715                let mut neighbours = Vec::new();
716                for di in [-1i64, 0, 1] {
717                    for dj in [-1i64, 0, 1] {
718                        let i2 = i as i64 + di;
719                        if i2 < 0 {
720                            continue;
721                        }
722                        let j2 = j as i64 + dj;
723                        let j2 = if grid.v_periodic { j2.rem_euclid(grid.nv as i64) } else { j2 };
724                        if j2 < 0 {
725                            continue;
726                        }
727                        if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
728                            neighbours.extend(list.iter().copied());
729                        }
730                    }
731                }
732                for m in neighbours {
733                    if m == k {
734                        continue;
735                    }
736                    let o2 = crossings[m].2;
737                    {
738                        let d = self.o_difference(o2, o);
739                        if d > 0.0 {
740                            is_max = false;
741                        }
742                        if d < 0.0 {
743                            is_min = false;
744                        }
745                    }
746                }
747                if !(is_max || is_min) {
748                    continue;
749                }
750                push(&mut found, self.face_extremum(source, level, u, v));
751            }
752            out.extend(found.into_iter().map(|(o, _, _)| (o, index)));
753        } else {
754            let (range, _, _) = self.domain(source);
755            let periodic = matches!(source, Source::RevolvedCorner { .. });
756            let n = match source {
757                Source::RevolvedCorner { .. } | Source::PrismEnd { .. } => 1024,
758                _ => self.grids_len_for_time(),
759            };
760            let _ = periodic;
761            // The uniform samples, with the curve's own level extrema merged in:
762            // near a tip the two roots of `ℓ = ℓ₀` fall either side of the extremum,
763            // closer than any fixed step, and would be missed together.
764            let mut samples: Vec<f64> = (0..=n).map(|k| range[0] + (range[1] - range[0]) * k as f64 / n as f64).collect();
765            if let Some(extra) = self.critical_params.get(index) {
766                samples.extend(extra.iter().map(|p| p.0).filter(|t| *t > range[0] && *t < range[1]));
767                samples.sort_by(|a, b| a.total_cmp(b));
768            }
769            let mut previous = self.source_reduced(source, samples[0], 0.0).0 - level;
770            for k in 1..samples.len() {
771                let p = samples[k];
772                let value = self.source_reduced(source, p, 0.0).0 - level;
773                if previous == 0.0 || previous * value < 0.0 {
774                    let (mut a, mut b) = (samples[k - 1], p);
775                    let fa0 = previous;
776                    for _ in 0..200 {
777                        let m = 0.5 * (a + b);
778                        let fm = self.source_reduced(source, m, 0.0).0 - level;
779                        if (fm < 0.0) == (fa0 < 0.0) && fm != 0.0 {
780                            a = m;
781                        } else {
782                            b = m;
783                        }
784                        if (b - a).abs() < 1e-15 * (range[1] - range[0]).abs().max(1.0) {
785                            break;
786                        }
787                    }
788                    let (_, o) = self.source_reduced(source, 0.5 * (a + b), 0.0);
789                    out.push((o, index));
790                }
791                previous = value;
792            }
793        }
794    }
795
796    fn grids_len_for_time(&self) -> usize {
797        let turn = self
798            .motion
799            .steps
800            .iter()
801            .map(|step| step.turn.abs())
802            .fold(0.0, f64::max)
803            * (self.motion.to - self.motion.from).abs();
804        ((turn * 256.0) as usize).clamp(1024, 8192)
805    }
806
807    /// The clip-frame point a reduced coordinate is tested at.
808    fn lift(&self, level: f64, o: f64) -> [f64; 3] {
809        let axial = 0.5 * (self.clip.axial[0] + self.clip.axial[1]);
810        match self.class {
811            Class::Screw(c) => {
812                let alpha = o + axial / c;
813                [level * alpha.cos(), level * alpha.sin(), axial]
814            }
815            Class::Turn => {
816                let theta = self.clip.angle.map(|a| 0.5 * (a[0] + a[1])).unwrap_or(0.0);
817                [level * theta.cos(), level * theta.sin(), o]
818            }
819            Class::Planar => [level * o.cos(), level * o.sin(), axial],
820        }
821    }
822
823    /// Is the reduced point inside the sweep: does the point's preimage under
824    /// the motion meet the tool for some time in the range?
825    ///
826    /// Walked in time by the tool's signed distance: it bounds how far the
827    /// preimage is from the tool, and the preimage moves no faster than
828    /// `speed_bound`, so no time within `distance / speed` of the current one can
829    /// be inside. The walk is exact for the tool's own distance (a lower bound for
830    /// a prism, which is conservative), and the floor on the step only stops a
831    /// grazing walk from creeping.
832    fn removed(&self, level: f64, o: f64) -> bool {
833        let x = self.frame.world(self.lift(level, o));
834        let (t0, t1) = (self.motion.from, self.motion.to);
835        let speed = self.speed_bound(x);
836        // The floor only keeps a grazing walk from creeping; a coarse floor
837        // jumps over a shallow pass of the tool (1e-5 of the plus wheel's range
838        // is 0.02 mm of travel, and read a 4e-3 mm sliver of its tooth space as
839        // material). A walk still grazing after `cap` steps is on the boundary.
840        let floor = (t1 - t0) * 1e-8;
841        let cap = 200_000usize;
842        let touching = 1e-10 * self.scale;
843        let clock = web_time::Instant::now();
844        let mut t = t0;
845        let mut steps = 0usize;
846        let inside = loop {
847            steps += 1;
848            let distance = self.tool_signed_distance(self.motion_invert(t, x));
849            if distance <= touching || steps > cap {
850                break true;
851            }
852            if t >= t1 {
853                break false;
854            }
855            t = (t + (distance / speed).max(floor)).min(t1);
856        };
857        let mut c = self.counters.get();
858        c.1 += 1;
859        c.2 += steps;
860        c.4 += clock.elapsed().as_secs_f64();
861        self.counters.set(c);
862        inside
863    }
864
865    /// An upper bound on how fast the preimage `M(t)⁻¹ x` moves per unit `t`.
866    fn speed_bound(&self, x: Vec3) -> f64 {
867        let centre = self.frame.origin;
868        let span = self.motion.from.abs().max(self.motion.to.abs());
869        let mut reach = x.sub(centre).length();
870        for step in &self.motion.steps {
871            reach += 2.0 * step.axis_origin.sub(centre).length() + step.advance.abs() * span;
872        }
873        let speed: f64 = self
874            .motion
875            .steps
876            .iter()
877            .map(|step| step.turn.abs() * (reach + step.axis_origin.sub(centre).length()) + step.advance.abs())
878            .sum();
879        speed.max(1e-300)
880    }
881
882    /// The region's intervals at a level, sorted by `lo` (angular: `lo` in
883    /// [0, 2π), `hi` may exceed 2π).
884    fn intervals_at(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
885        if let Some(hit) = self.cache.borrow().get(&level.to_bits()) {
886            return Ok(hit.clone());
887        }
888        let clock = web_time::Instant::now();
889        let before = self.counters.get();
890        let intervals = self.intervals_uncached(level)?;
891        if clock.elapsed().as_secs_f64() > 0.5 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
892            let after = self.counters.get();
893            eprintln!(
894                "  slow level {level:.12}: {:.2}s, candidates {:.2}s, oracle calls {} steps {} ({:.2}s), {} intervals",
895                clock.elapsed().as_secs_f64(),
896                after.3 - before.3,
897                after.1 - before.1,
898                after.2 - before.2,
899                after.4 - before.4,
900                intervals.len()
901            );
902        }
903        self.cache.borrow_mut().insert(level.to_bits(), intervals.clone());
904        Ok(intervals)
905    }
906
907    fn intervals_uncached(&self, level: f64) -> Result<Vec<Interval>, KernelRefusal> {
908        let clock = web_time::Instant::now();
909        let mut candidates = Vec::new();
910        for index in 0..self.sources.len() {
911            self.source_candidates(index, level, &mut candidates);
912        }
913        let mut c = self.counters.get();
914        c.0 += 1;
915        c.3 += clock.elapsed().as_secs_f64();
916        self.counters.set(c);
917        if self.angular {
918            for candidate in &mut candidates {
919                candidate.0 = candidate.0.rem_euclid(TAU);
920            }
921        }
922        candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
923        candidates.dedup_by(|b, a| (b.0 - a.0).abs() < 1e-12);
924        if candidates.is_empty() {
925            let probes = 64;
926            let (base, span) = if self.angular { (0.0, TAU) } else { (-self.scale * 4.0, self.scale * 8.0) };
927            if (0..probes).all(|k| self.removed(level, base + span * (k as f64 + 0.5) / probes as f64)) {
928                return Err(refuse(
929                    "whole_level",
930                    format!("the sweep covers every angle at level {level}, which this construction does not build"),
931                ));
932            }
933            return Ok(Vec::new());
934        }
935        let n = candidates.len();
936        let gaps = if self.angular { n } else { n - 1 };
937        let mut removed = Vec::with_capacity(gaps);
938        for k in 0..gaps {
939            let a = candidates[k].0;
940            let b = if k + 1 < n { candidates[k + 1].0 } else { candidates[0].0 + TAU };
941            // A gap far below any feature is the two ends of one boundary
942            // point; its middle is on the boundary, where the oracle can only
943            // graze. It joins whatever is on either side.
944            if (b - a) * level.max(1.0) < 1e-9 * self.scale {
945                removed.push(true);
946                continue;
947            }
948            removed.push(self.removed(level, 0.5 * (a + b)));
949        }
950        let mut intervals = Vec::new();
951        if self.angular && removed.iter().all(|r| *r) {
952            return Err(refuse(
953                "whole_level",
954                format!("the sweep covers every angle at level {level}, which this construction does not build"),
955            ));
956        }
957        // Start the walk after a material gap so a run never straddles the start.
958        let start = if self.angular {
959            removed.iter().position(|r| !r).map(|k| (k + 1) % n).unwrap_or(0)
960        } else {
961            0
962        };
963        let mut k = 0;
964        while k < gaps {
965            let g = (start + k) % n.max(1);
966            if !removed[g] {
967                k += 1;
968                continue;
969            }
970            let first = g;
971            let mut last = g;
972            while k + 1 < gaps && removed[(start + k + 1) % n] {
973                k += 1;
974                last = (start + k) % n;
975            }
976            let lo = candidates[first];
977            let hi_index = (last + 1) % n;
978            let hi = candidates[hi_index];
979            let mut hi_value = hi.0;
980            if self.angular && hi_value <= lo.0 {
981                hi_value += TAU;
982            }
983            intervals.push(Interval {
984                lo: lo.0,
985                hi: hi_value,
986                lo_source: lo.1,
987                hi_source: hi.1,
988            });
989            k += 1;
990        }
991        intervals.sort_by(|a, b| a.lo.total_cmp(&b.lo));
992        Ok(intervals)
993    }
994
995    /// Critical points of the level over a source (tips and plateaus).
996    /// The face's characteristic function: `det(∇o, ∇ℓ) / |∇ℓ|`, zero where
997    /// the face's image turns back (its envelope), and the level there.
998    fn characteristic(&self, source: Source, u: f64, v: f64) -> (f64, f64) {
999        let (ur, vr, _) = self.domain(source);
1000        let hu = 1e-6 * (ur[1] - ur[0]).max(1e-3);
1001        let hv = 1e-6 * (vr[1] - vr[0]).max(1e-3);
1002        let (l, _) = self.source_reduced(source, u, v);
1003        let (lu1, ou1) = self.source_reduced(source, u + hu, v);
1004        let (lu0, ou0) = self.source_reduced(source, u - hu, v);
1005        let (lv1, ov1) = self.source_reduced(source, u, v + hv);
1006        let (lv0, ov0) = self.source_reduced(source, u, v - hv);
1007        let (lu, lv) = ((lu1 - lu0) / (2.0 * hu), (lv1 - lv0) / (2.0 * hv));
1008        let ou = self.o_difference(ou1, ou0) / (2.0 * hu);
1009        let ov = self.o_difference(ov1, ov0) / (2.0 * hv);
1010        ((ou * lv - ov * lu) / lu.hypot(lv).max(1e-300), l)
1011    }
1012
1013    /// FOLDS and KINKS of a face, recorded as its critical points so events snap
1014    /// to them and band ends read them exactly:
1015    /// - a FOLD is where the level is extremal along the face's own envelope
1016    ///   (`D = 0`): two extrema of `o` on one level contour annihilate there
1017    ///   (the plus wheel's lobes merging at ρ = 20, the tip of its arm's flat
1018    ///   end). Newton on {D = 0, ℓ_u·D_v − ℓ_v·D_u = 0}, seeded where the level
1019    ///   is locally extremal among the grid's crossings of D = 0.
1020    /// - a KINK is where the envelope runs into the face's own boundary and the
1021    ///   boundary's edge takes over: roots of D along each non-periodic side.
1022    fn folds_and_kinks(&self, source: Source, grid: &Grid, out: &mut Vec<(f64, f64)>, params: &mut Vec<(f64, f64)>) {
1023        let (ur, vr, v_periodic) = self.domain(source);
1024        let u_periodic = self.u_periodic(source);
1025        let (nu, nv) = (grid.nu, grid.nv);
1026        let mut d = vec![0.0; nu * nv];
1027        for i in 0..nu {
1028            for j in 0..nv {
1029                let (u, v) = Self::grid_param(grid, i, j);
1030                d[i * nv + j] = self.characteristic(source, u, v).0;
1031            }
1032        }
1033        // Crossings of D = 0 along grid edges: (i, j, u, v, level).
1034        let mut crossings: Vec<(usize, usize, f64, f64, f64)> = Vec::new();
1035        let jmax = if v_periodic { nv } else { nv - 1 };
1036        for i in 0..nu {
1037            for j in 0..jmax {
1038                let (a, b) = (d[i * nv + j], d[i * nv + (j + 1) % nv]);
1039                if a * b < 0.0 {
1040                    let w = a / (a - b);
1041                    let (u, v0) = Self::grid_param(grid, i, j);
1042                    let (_, v1) = Self::grid_param(grid, i, j + 1);
1043                    let v1 = if v_periodic && j + 1 == nv { vr[1] } else { v1 };
1044                    let v = v0 + (v1 - v0) * w;
1045                    crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1046                }
1047            }
1048        }
1049        for i in 0..nu - 1 {
1050            for j in 0..nv {
1051                let (a, b) = (d[i * nv + j], d[(i + 1) * nv + j]);
1052                if a * b < 0.0 {
1053                    let w = a / (a - b);
1054                    let (u0, v) = Self::grid_param(grid, i, j);
1055                    let (u1, _) = Self::grid_param(grid, i + 1, j);
1056                    let u = u0 + (u1 - u0) * w;
1057                    crossings.push((i, j, u, v, self.source_reduced(source, u, v).0));
1058                }
1059            }
1060        }
1061        let mut buckets: std::collections::HashMap<(usize, usize), Vec<usize>> = std::collections::HashMap::new();
1062        for (k, c) in crossings.iter().enumerate() {
1063            buckets.entry((c.0, c.1)).or_default().push(k);
1064        }
1065        let ku = 1e-4 * (ur[1] - ur[0]);
1066        let kv = 1e-4 * (vr[1] - vr[0]);
1067        let fold = |u: f64, v: f64| -> [f64; 2] {
1068            let (dd, _) = self.characteristic(source, u, v);
1069            let du = (self.characteristic(source, u + ku, v).0 - self.characteristic(source, u - ku, v).0) / (2.0 * ku);
1070            let dv = (self.characteristic(source, u, v + kv).0 - self.characteristic(source, u, v - kv).0) / (2.0 * kv);
1071            let lu = (self.source_reduced(source, u + ku, v).0 - self.source_reduced(source, u - ku, v).0) / (2.0 * ku);
1072            let lv = (self.source_reduced(source, u, v + kv).0 - self.source_reduced(source, u, v - kv).0) / (2.0 * kv);
1073            let norm = (lu.hypot(lv) * du.hypot(dv)).max(1e-300);
1074            [dd, (lu * dv - lv * du) / norm]
1075        };
1076        for (k, &(i, j, u0, v0, level)) in crossings.iter().enumerate() {
1077            let mut is_min = true;
1078            let mut is_max = true;
1079            for di in [-1i64, 0, 1] {
1080                for dj in [-1i64, 0, 1] {
1081                    let i2 = i as i64 + di;
1082                    let j2 = if v_periodic { (j as i64 + dj).rem_euclid(nv as i64) } else { j as i64 + dj };
1083                    if i2 < 0 || j2 < 0 {
1084                        continue;
1085                    }
1086                    if let Some(list) = buckets.get(&(i2 as usize, j2 as usize)) {
1087                        for &m in list {
1088                            if m == k {
1089                                continue;
1090                            }
1091                            if crossings[m].4 < level {
1092                                is_min = false;
1093                            }
1094                            if crossings[m].4 > level {
1095                                is_max = false;
1096                            }
1097                        }
1098                    }
1099                }
1100            }
1101            if !(is_min || is_max) {
1102                continue;
1103            }
1104            let (mut u, mut v) = (u0, v0);
1105            let mut converged = false;
1106            for _ in 0..40 {
1107                let f = fold(u, v);
1108                let (su, sv) = (ku * 10.0, kv * 10.0);
1109                let fu1 = fold(u + su, v);
1110                let fu0 = fold(u - su, v);
1111                let fv1 = fold(u, v + sv);
1112                let fv0 = fold(u, v - sv);
1113                let j00 = (fu1[0] - fu0[0]) / (2.0 * su);
1114                let j01 = (fv1[0] - fv0[0]) / (2.0 * sv);
1115                let j10 = (fu1[1] - fu0[1]) / (2.0 * su);
1116                let j11 = (fv1[1] - fv0[1]) / (2.0 * sv);
1117                let det = j00 * j11 - j01 * j10;
1118                if !det.is_finite() || det.abs() < 1e-300 {
1119                    break;
1120                }
1121                let du = -(f[0] * j11 - f[1] * j01) / det;
1122                let dv = -(j00 * f[1] - j10 * f[0]) / det;
1123                // Stay near the seed: a fold is local to its crossing.
1124                let cap = 4.0 * ((ur[1] - ur[0]) / (nu as f64)).max((vr[1] - vr[0]) / (nv as f64));
1125                let scale = (du.hypot(dv) / cap).max(1.0);
1126                u += du / scale;
1127                v += dv / scale;
1128                if du.hypot(dv) < 1e-12 * (ur[1] - ur[0]).max(vr[1] - vr[0]) {
1129                    converged = true;
1130                    break;
1131                }
1132            }
1133            let f = fold(u, v);
1134            if !(converged || (f[0].abs() < 1e-9 && f[1].abs() < 1e-5)) {
1135                continue;
1136            }
1137            if u_periodic {
1138                u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1139            }
1140            if v_periodic {
1141                v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1142            }
1143            if u > ur[0] && u < ur[1] && (v_periodic || (v > vr[0] && v < vr[1])) {
1144                out.push(self.source_reduced(source, u, v));
1145                params.push((u, v));
1146            }
1147        }
1148        // Kinks: roots of D along each non-periodic side of the face.
1149        let mut sides: Vec<(bool, f64)> = Vec::new(); // (along v at fixed u, the fixed value)
1150        if !u_periodic {
1151            sides.push((true, ur[0]));
1152            sides.push((true, ur[1]));
1153        }
1154        if !v_periodic {
1155            sides.push((false, vr[0]));
1156            sides.push((false, vr[1]));
1157        }
1158        for (along_v, fixed) in sides {
1159            let (range, n) = if along_v { (vr, nv * 4) } else { (ur, nu * 4) };
1160            let at = |t: f64| if along_v { (fixed, t) } else { (t, fixed) };
1161            let value = |t: f64| {
1162                let (u, v) = at(t);
1163                self.characteristic(source, u, v).0
1164            };
1165            let mut previous = value(range[0]);
1166            for k in 1..=n {
1167                let t = range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1168                let current = value(t);
1169                if previous * current < 0.0 {
1170                    let (mut a, mut b) = (t - (range[1] - range[0]) / n as f64, t);
1171                    let fa = previous;
1172                    for _ in 0..100 {
1173                        let m = 0.5 * (a + b);
1174                        if (value(m) < 0.0) == (fa < 0.0) {
1175                            a = m;
1176                        } else {
1177                            b = m;
1178                        }
1179                    }
1180                    let (u, v) = at(0.5 * (a + b));
1181                    out.push(self.source_reduced(source, u, v));
1182                    params.push((u, v));
1183                }
1184                previous = current;
1185            }
1186        }
1187    }
1188
1189    fn source_critical(&self, index: usize) -> (Vec<(f64, f64)>, Vec<(f64, f64)>) {
1190        let source = self.sources[index];
1191        let mut out = Vec::new();
1192        let mut params = Vec::new();
1193        if Self::is_face(source) {
1194            let grid = self.grids[index].as_ref().expect("face grid");
1195            let (ur, vr, periodic) = self.domain(source);
1196            let at = |i: usize, j: usize| grid.values[i * grid.nv + j].0;
1197            let u_periodic = self.u_periodic(source);
1198            // A periodic first parameter has its last row equal to its first.
1199            let rows = if u_periodic { grid.nu - 1 } else { grid.nu };
1200            for i in 0..rows {
1201                if !u_periodic && (i == 0 || i + 1 == grid.nu) {
1202                    continue;
1203                }
1204                for j in 0..grid.nv {
1205                    if !periodic && (j == 0 || j + 1 == grid.nv) {
1206                        continue;
1207                    }
1208                    let here = at(i, j);
1209                    let mut is_min = true;
1210                    let mut is_max = true;
1211                    for di in [-1i64, 0, 1] {
1212                        for dj in [-1i64, 0, 1] {
1213                            if di == 0 && dj == 0 {
1214                                continue;
1215                            }
1216                            let i2 = ((i as i64 + di).rem_euclid(rows as i64)) as usize;
1217                            let j2 = ((j as i64 + dj).rem_euclid(grid.nv as i64)) as usize;
1218                            let there = at(i2, j2);
1219                            if there < here {
1220                                is_min = false;
1221                            }
1222                            if there > here {
1223                                is_max = false;
1224                            }
1225                        }
1226                    }
1227                    if !(is_min || is_max) {
1228                        continue;
1229                    }
1230                    // Newton on ∇ℓ = 0.
1231                    let (mut u, mut v) = Self::grid_param(grid, i, j);
1232                    let hu = 1e-5 * (ur[1] - ur[0]);
1233                    let hv = 1e-5 * (vr[1] - vr[0]);
1234                    let level = |u: f64, v: f64| self.source_reduced(source, u, v).0;
1235                    for _ in 0..50 {
1236                        let f = level(u, v);
1237                        let fu = (level(u + hu, v) - level(u - hu, v)) / (2.0 * hu);
1238                        let fv = (level(u, v + hv) - level(u, v - hv)) / (2.0 * hv);
1239                        let fuu = (level(u + hu, v) - 2.0 * f + level(u - hu, v)) / (hu * hu);
1240                        let fvv = (level(u, v + hv) - 2.0 * f + level(u, v - hv)) / (hv * hv);
1241                        let fuv = (level(u + hu, v + hv) - level(u + hu, v - hv) - level(u - hu, v + hv)
1242                            + level(u - hu, v - hv))
1243                            / (4.0 * hu * hv);
1244                        let det = fuu * fvv - fuv * fuv;
1245                        if det.abs() < 1e-300 {
1246                            break;
1247                        }
1248                        let du = -(fu * fvv - fv * fuv) / det;
1249                        let dv = -(fuu * fv - fuv * fu) / det;
1250                        u += du;
1251                        v += dv;
1252                        if du.abs() < 1e-14 && dv.abs() < 1e-14 {
1253                            break;
1254                        }
1255                    }
1256                    if u_periodic {
1257                        u = ur[0] + (u - ur[0]).rem_euclid(ur[1] - ur[0]);
1258                    }
1259                    if periodic {
1260                        v = vr[0] + (v - vr[0]).rem_euclid(vr[1] - vr[0]);
1261                    }
1262                    if u >= ur[0] && u <= ur[1] && (periodic || (v >= vr[0] && v <= vr[1])) {
1263                        out.push(self.source_reduced(source, u, v));
1264                        params.push((u, v));
1265                    }
1266                }
1267            }
1268            self.folds_and_kinks(source, grid, &mut out, &mut params);
1269        } else {
1270            let (range, _, _) = self.domain(source);
1271            let n = 2048;
1272            let param = |k: usize| range[0] + (range[1] - range[0]) * k as f64 / n as f64;
1273            let values: Vec<f64> = (0..=n).map(|k| self.source_reduced(source, param(k), 0.0).0).collect();
1274            for k in 1..n {
1275                let (a, b, c) = (values[k - 1], values[k], values[k + 1]);
1276                if (b <= a && b <= c) || (b >= a && b >= c) {
1277                    let sign = if b <= a && b <= c { 1.0 } else { -1.0 };
1278                    let (mut lo, mut hi) = (param(k - 1), param(k + 1));
1279                    let ratio = 0.618_033_988_749_894_9;
1280                    for _ in 0..100 {
1281                        let c1 = hi - ratio * (hi - lo);
1282                        let d1 = lo + ratio * (hi - lo);
1283                        if sign * self.source_reduced(source, c1, 0.0).0 < sign * self.source_reduced(source, d1, 0.0).0 {
1284                            hi = d1;
1285                        } else {
1286                            lo = c1;
1287                        }
1288                    }
1289                    out.push(self.source_reduced(source, 0.5 * (lo + hi), 0.0));
1290                    params.push((0.5 * (lo + hi), 0.0));
1291                }
1292            }
1293        }
1294        (out, params)
1295    }
1296}
1297
1298/// Are two sources the same boundary piece of the sweep: the same source, or
1299/// a piece and the one that continues it across a set-periodic motion's wrap?
1300/// A face handing over to the edge that bounds it is NOT the same: the V
1301/// wheel's flank meets its tip circle's image at a kink, and fitting one curve
1302/// through it read the groove 1.5e-6 off (split: 1.1e-8).
1303fn same_or_bounding(engine: &Engine, a: usize, b: usize) -> bool {
1304    if a == b {
1305        return true;
1306    }
1307    let pieces = match engine.tool {
1308        SweptTool::Revolved { meridian, .. } => meridian.len(),
1309        SweptTool::Prism { outline, .. } => outline.len(),
1310    };
1311    // (piece of the face, piece whose start the edge is)
1312    let face = |s: Source| match s {
1313        Source::RevolvedFace { piece } | Source::PrismFace { piece } => Some(piece),
1314        _ => None,
1315    };
1316    let edge = |s: Source| match s {
1317        Source::RevolvedCorner { piece } | Source::PrismVertex { piece } => Some(piece),
1318        _ => None,
1319    };
1320    let (sa, sb) = (engine.sources[a], engine.sources[b]);
1321    // Across the wrap, piece `k` continues as `wrap[k]`.
1322    let same_piece = |x: usize, y: usize| {
1323        x == y || engine.wrap.as_ref().is_some_and(|map| map[x] == y || map[y] == x)
1324    };
1325    let bounds = |f: usize, e: usize| {
1326        same_piece(e, f) || same_piece(e, (f + 1) % pieces)
1327    };
1328    let _ = bounds;
1329    match (face(sa), edge(sa), face(sb), edge(sb)) {
1330        (Some(f), _, Some(g), _) => same_piece(f, g),
1331        (_, Some(e), _, Some(g)) => same_piece(e, g),
1332        _ => false,
1333    }
1334}
1335
1336/// Do two interval lists describe the same structure (same count, each end on
1337/// the same source or one that hands over to it smoothly, matched in order)?
1338fn compatible(engine: &Engine, a: &[Interval], b: &[Interval]) -> bool {
1339    let angular = engine.angular;
1340    if a.len() != b.len() {
1341        return false;
1342    }
1343    // Match each interval of `a` to the one of `b` nearest its middle.
1344    let mut used = vec![false; b.len()];
1345    for x in a {
1346        let mid = 0.5 * (x.lo + x.hi);
1347        let mut best = None;
1348        let mut best_distance = f64::INFINITY;
1349        for (k, y) in b.iter().enumerate() {
1350            let other = 0.5 * (y.lo + y.hi);
1351            let d = if angular { wrap_pi(mid - other).abs() } else { (mid - other).abs() };
1352            if d < best_distance {
1353                best_distance = d;
1354                best = Some(k);
1355            }
1356        }
1357        let Some(k) = best else { return false };
1358        let y = &b[k];
1359        let overlap = if angular {
1360            let shift = (wrap_pi(y.lo - x.lo)) - (y.lo - x.lo);
1361            let (ylo, yhi) = (y.lo + shift, y.hi + shift);
1362            x.hi.min(yhi) - x.lo.max(ylo)
1363        } else {
1364            x.hi.min(y.hi) - x.lo.max(y.lo)
1365        };
1366        if used[k]
1367            || overlap < 0.0
1368            || !same_or_bounding(engine, x.lo_source, y.lo_source)
1369            || !same_or_bounding(engine, x.hi_source, y.hi_source)
1370        {
1371            return false;
1372        }
1373        used[k] = true;
1374    }
1375    true
1376}
1377
1378/// A boundary piece in reduced coordinates, as a polyline of exact samples
1379/// (the first and last are the piece's ends) with their `σ`.
1380struct Piece {
1381    points: Vec<(f64, f64)>,
1382    sigma: Vec<f64>,
1383    /// A straight level segment (built exactly, not fitted).
1384    level_segment: bool,
1385}
1386
1387/// Build the sweep of `tool` along `motion` inside `clip`, as one body per
1388/// connected region of the reduced section.
1389pub fn sweep_solid_within(
1390    tool: &SweptTool,
1391    motion: &SolidMotion,
1392    clip: &SweepClip,
1393) -> Result<Vec<BrepSolid>, KernelRefusal> {
1394    // --- Frame and classification.
1395    let ez = clip
1396        .axis_direction
1397        .normalized()
1398        .or_refuse(KernelStage::Collect, "clip_axis")?;
1399    let ex = clip
1400        .reference
1401        .sub(ez.scale(clip.reference.dot(ez)))
1402        .normalized()
1403        .or_refuse(KernelStage::Collect, "clip_reference")?;
1404    let frame = Frame { origin: clip.axis_origin, ex, ey: ez.cross(ex), ez };
1405    if !(motion.from.is_finite() && motion.to.is_finite() && motion.to > motion.from) {
1406        return Err(KernelRefusal::input(
1407            KernelStage::Collect,
1408            "motion_range",
1409            format!("{SOLID_SWEEP_REFUSAL}: the motion's range must run forwards"),
1410        ));
1411    }
1412    if motion.steps.is_empty() {
1413        return Err(KernelRefusal::input(KernelStage::Collect, "motion_empty", format!("{SOLID_SWEEP_REFUSAL}: no motion")));
1414    }
1415    let mut axes = Vec::with_capacity(motion.steps.len());
1416    for step in &motion.steps {
1417        axes.push(step.axis_direction.normalized().or_refuse(KernelStage::Collect, "step_axis")?);
1418    }
1419    if !(clip.radius[0] >= 0.0 && clip.radius[1] > clip.radius[0] && clip.axial[1] > clip.axial[0]) {
1420        return Err(KernelRefusal::input(
1421            KernelStage::Collect,
1422            "clip_range",
1423            format!("{SOLID_SWEEP_REFUSAL}: the clip's ranges must be increasing"),
1424        ));
1425    }
1426    let on_axis = |origin: Vec3, axis: Vec3| -> bool {
1427        let parallel = axis.cross(ez).length() < 1e-12;
1428        let offset = origin.sub(clip.axis_origin);
1429        parallel && offset.sub(ez.scale(offset.dot(ez))).length() < 1e-9 * (1.0 + clip.radius[1])
1430    };
1431    let class = match tool {
1432        SweptTool::Revolved { meridian, .. } => {
1433            validate_outline(meridian, "meridian")?;
1434            if motion.steps.len() != 1 || !on_axis(motion.steps[0].axis_origin, axes[0]) {
1435                return Err(refuse(
1436                    "motion_class",
1437                    "a solid of revolution is swept here only by one screw or turn about the clip's axis".into(),
1438                ));
1439            }
1440            let step = motion.steps[0];
1441            let sense = axes[0].dot(ez).signum();
1442            if step.turn == 0.0 {
1443                return Err(refuse("motion_class", "a slide of a solid of revolution is not built here".into()));
1444            }
1445            if step.advance == 0.0 {
1446                Class::Turn
1447            } else {
1448                // axial advance per radian about +ez
1449                Class::Screw((step.advance * sense) / (step.turn * sense))
1450            }
1451        }
1452        SweptTool::Prism { normal, outline, from, to, origin, .. } => {
1453            validate_outline(outline, "prism")?;
1454            let n = normal.normalized().or_refuse(KernelStage::Collect, "prism_normal")?;
1455            if n.cross(ez).length() > 1e-12 {
1456                return Err(refuse("motion_class", "a prism is swept here only along its own direction's clip axis".into()));
1457            }
1458            for (step, axis) in motion.steps.iter().zip(&axes) {
1459                let turn_ok = step.turn == 0.0 || axis.cross(n).length() < 1e-12;
1460                let advance_ok = step.advance == 0.0 || (step.turn == 0.0 && axis.dot(n).abs() < 1e-12);
1461                if !(turn_ok && advance_ok) {
1462                    return Err(refuse(
1463                        "motion_class",
1464                        "a prism is swept here only by turns about axes along it and slides across it".into(),
1465                    ));
1466                }
1467            }
1468            // The clip's axial range must lie within the prism's extent.
1469            let base = clip.axis_origin.sub(*origin).dot(n);
1470            let sense = ez.dot(n);
1471            let h0 = base + sense * clip.axial[0];
1472            let h1 = base + sense * clip.axial[1];
1473            if h0.min(h1) < *from || h0.max(h1) > *to {
1474                return Err(refuse(
1475                    "clip_past_prism",
1476                    "the clip reaches past the prism's ends, where the sweep is not a prism".into(),
1477                ));
1478            }
1479            Class::Planar
1480        }
1481    };
1482    let prism_height = match tool {
1483        SweptTool::Prism { from, to, .. } => 0.5 * (from + to),
1484        _ => 0.0,
1485    };
1486
1487    // Does the motion end where it began, as a set? For a prism, every outline
1488    // vertex at the last position must sit on some vertex at the first (the plus
1489    // turns 540°, which its half-turn symmetry makes a whole period). The
1490    // permutation says which piece continues which across the wrap.
1491    let wrap: Option<Vec<usize>> = match tool {
1492        SweptTool::Prism { origin, normal, x_axis, outline, .. } => {
1493            let probe = Engine {
1494                tool,
1495                motion,
1496                axes: axes.clone(),
1497                frame,
1498                class,
1499                clip,
1500                sources: Vec::new(),
1501                grids: Vec::new(),
1502                critical: Vec::new(),
1503                critical_params: Vec::new(),
1504                angular: true,
1505                prism_height,
1506                scale: 1.0,
1507                cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1508                wrap: None,
1509                counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1510            };
1511            let n = normal.normalized_or(*normal);
1512            let xa = x_axis.normalized_or(*x_axis);
1513            let ya = n.cross(xa);
1514            let vertex = |k: usize, t: f64| {
1515                let q = outline[k].point(0.0);
1516                probe.motion_apply(t, origin.add(xa.scale(q[0])).add(ya.scale(q[1])).add(n.scale(prism_height)))
1517            };
1518            let size = scale_of(outline);
1519            let mut map = Vec::with_capacity(outline.len());
1520            for k in 0..outline.len() {
1521                let end = vertex(k, motion.to);
1522                match (0..outline.len()).find(|&j| vertex(j, motion.from).sub(end).length() < 1e-9 * size) {
1523                    Some(j) => map.push(j),
1524                    None => break,
1525                }
1526            }
1527            let consistent = map.len() == outline.len()
1528                && (0..outline.len()).all(|k| map[(k + 1) % outline.len()] == (map[k] + 1) % outline.len());
1529            consistent.then_some(map)
1530        }
1531        SweptTool::Revolved { .. } => None,
1532    };
1533    let periodic_motion = wrap.is_some();
1534    let mut sources = Vec::new();
1535    match tool {
1536        SweptTool::Revolved { meridian, .. } => {
1537            // A piece lying on the axis revolves into nothing, and a corner on the
1538            // axis into a point: neither bounds the tool.
1539            let on_axis = |p: [f64; 2]| p[0].abs() <= 1e-12 * scale_of(meridian);
1540            for piece in 0..meridian.len() {
1541                let lies_on_axis = matches!(meridian[piece], OutlinePiece::Line { from, to } if on_axis(from) && on_axis(to));
1542                if !lies_on_axis {
1543                    sources.push(Source::RevolvedFace { piece });
1544                }
1545                if !meridian[piece].is_full_circle() && !on_axis(meridian[piece].point(0.0)) {
1546                    sources.push(Source::RevolvedCorner { piece });
1547                }
1548            }
1549        }
1550        SweptTool::Prism { outline, .. } => {
1551            for piece in 0..outline.len() {
1552                sources.push(Source::PrismFace { piece });
1553                if !outline[piece].is_full_circle() {
1554                    sources.push(Source::PrismVertex { piece });
1555                }
1556                if !periodic_motion {
1557                    sources.push(Source::PrismEnd { piece, end: false });
1558                    sources.push(Source::PrismEnd { piece, end: true });
1559                }
1560            }
1561        }
1562    }
1563    let scale = clip.radius[1].max(1.0);
1564    let mut engine = Engine {
1565        tool,
1566        motion,
1567        axes,
1568        frame,
1569        class,
1570        clip,
1571        sources,
1572        grids: Vec::new(),
1573        critical: Vec::new(),
1574        critical_params: Vec::new(),
1575        angular: class != Class::Turn,
1576        prism_height,
1577        scale,
1578        cache: std::cell::RefCell::new(std::collections::HashMap::new()),
1579        wrap,
1580        counters: std::cell::Cell::new((0, 0, 0, 0.0, 0.0)),
1581    };
1582    engine.grids = engine
1583        .sources
1584        .iter()
1585        .map(|source| if Engine::is_face(*source) { Some(engine.build_grid(*source)) } else { None })
1586        .collect();
1587    let critical: Vec<_> = (0..engine.sources.len()).map(|k| engine.source_critical(k)).collect();
1588    engine.critical_params = critical.iter().map(|c| c.1.clone()).collect();
1589    engine.critical = critical.into_iter().map(|c| c.0).collect();
1590
1591    // --- Ends: a screw or turn must not reach the clip at either end.
1592    if matches!(class, Class::Screw(_) | Class::Turn) {
1593        check_ends_clear(&engine)?;
1594    }
1595
1596    // --- Scan the levels for events.
1597    let (l0, l1) = (clip.radius[0], clip.radius[1]);
1598    let scan = 192;
1599    let mut levels: Vec<f64> = (0..=scan).map(|k| l0 + (l1 - l0) * k as f64 / scan as f64).collect();
1600    // Never sample exactly on a critical level: nudge the scan off them.
1601    for level in &mut levels {
1602        for critical in engine.critical.iter().flatten() {
1603            if (critical.0 - *level).abs() < 1e-9 * scale {
1604                *level += 1e-7 * (l1 - l0);
1605            }
1606        }
1607    }
1608    let mut structure = Vec::with_capacity(levels.len());
1609    for &level in &levels {
1610        structure.push(engine.intervals_at(level)?);
1611    }
1612    let debug = std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok();
1613    let mut breaks = vec![l0];
1614    for k in 0..scan {
1615        find_events(&engine, levels[k], &structure[k], levels[k + 1], &structure[k + 1], &mut breaks, 0)?;
1616    }
1617    breaks.push(l1);
1618    // Snap each event to a critical level of a source when one is that close.
1619    // A birth or death is found by bisection only to where the seeding first
1620    // sees the level's contour, which near a tip is a √ away from it: the
1621    // critical level is the exact one.
1622    let last = breaks.len() - 1;
1623    for level in breaks[1..last].iter_mut() {
1624        let mut nearest = *level;
1625        let mut distance = 1e-5 * scale;
1626        for critical in engine.critical.iter().flatten() {
1627            if (critical.0 - *level).abs() < distance {
1628                distance = (critical.0 - *level).abs();
1629                nearest = critical.0;
1630            }
1631        }
1632        *level = nearest;
1633    }
1634    breaks.sort_by(|a, b| a.total_cmp(b));
1635    // A source handing over to the next one at an edge can flicker over a few
1636    // bisection steps (the face's extremum sits a hair inside its domain); the
1637    // bands that leaves are far below any feature. Keep the first break of each
1638    // cluster, and the clip's own ends.
1639    let mut kept: Vec<f64> = Vec::with_capacity(breaks.len());
1640    for (index, &level) in breaks.iter().enumerate() {
1641        let is_end = index == 0 || index + 1 == breaks.len();
1642        match kept.last() {
1643            Some(&previous) if level - previous < 1e-5 * scale => {
1644                if is_end {
1645                    kept.pop();
1646                    kept.push(level);
1647                }
1648            }
1649            _ => kept.push(level),
1650        }
1651    }
1652    let mut breaks = kept;
1653
1654    // --- Merges no critical point explains: a FOLD the face's own Newton cannot
1655    //     reach (the plus wheel's arm tip: its level varies by 1e-4 along the
1656    //     envelope there). The gap between the two merging ends closes as
1657    //     √(ℓf − ℓ), so gap² is smooth in ℓ: fit it as a quadratic through three
1658    //     levels below the event, outside the band where the extremum Newton
1659    //     flickers, and take its root as the merge level; the gap's middle,
1660    //     extrapolated the same way, is the merge point. Both ends' sources
1661    //     record it as a critical point, and the bands keep their samples away
1662    //     from it (`fold_ends`).
1663    let mut fold_ends: Vec<f64> = Vec::new();
1664    for index in 1..breaks.len().saturating_sub(1) {
1665        let level = breaks[index];
1666        let snapped = engine.critical.iter().flatten().any(|c| (c.0 - level).abs() < 1e-9 * scale);
1667        if snapped {
1668            continue;
1669        }
1670        let room = (level - breaks[index - 1]).min(breaks[index + 1] - level);
1671        // Close enough that the √ law holds to the fit's order, far enough to be
1672        // clear of the flicker (the plus wheel: 2e-4 out, flicker inside 1e-5).
1673        let delta = (5e-6 * scale).min(room / 8.0);
1674        let below = engine.intervals_at(level - delta)?;
1675        let above = engine.intervals_at(level + delta)?;
1676        if below.len() <= above.len() {
1677            continue;
1678        }
1679        // Adjacent intervals below whose facing ends close into one above.
1680        let mut refined: Vec<(f64, f64, usize, usize)> = Vec::new();
1681        for k in 0..below.len() {
1682            let (x, y) = (below[k], below[(k + 1) % below.len()]);
1683            if !engine.angular && k + 1 == below.len() {
1684                break;
1685            }
1686            let gap_at = |intervals: &[Interval]| -> Option<(f64, f64)> {
1687                // the pair nearest (x.hi, y.lo) at this level
1688                let mut best: Option<(f64, f64, f64)> = None;
1689                for i in 0..intervals.len() {
1690                    let (p, q) = (intervals[i], intervals[(i + 1) % intervals.len()]);
1691                    let d = engine.o_difference(p.hi, x.hi).abs() + engine.o_difference(q.lo, y.lo).abs();
1692                    if best.map_or(true, |b| d < b.2) {
1693                        let gap = engine.o_difference(q.lo, p.hi);
1694                        best = Some((gap, p.hi + 0.5 * gap, d));
1695                    }
1696                }
1697                best.map(|b| (b.0, b.1))
1698            };
1699            let mut samples = Vec::new();
1700            for factor in [1.0, 2.0, 4.0] {
1701                let l = level - delta * factor;
1702                let intervals = engine.intervals_at(l)?;
1703                if intervals.len() != below.len() {
1704                    break;
1705                }
1706                if let Some((gap, middle)) = gap_at(&intervals) {
1707                    samples.push((l, gap, middle));
1708                }
1709            }
1710            if samples.len() < 3 || samples.iter().any(|s| s.1 <= 0.0 || s.1 > 0.5) {
1711                continue;
1712            }
1713            // Quadratic through (l, gap²); its root above the samples.
1714            let quad = |values: [f64; 3]| -> (f64, f64, f64) {
1715                let (x0, x1, x2) = (samples[0].0, samples[1].0, samples[2].0);
1716                let (y0, y1, y2) = (values[0], values[1], values[2]);
1717                let d01 = (y0 - y1) / (x0 - x1);
1718                let d12 = (y1 - y2) / (x1 - x2);
1719                let a2 = (d01 - d12) / (x0 - x2);
1720                let a1 = d01 - a2 * (x0 + x1);
1721                let a0 = y0 - a1 * x0 - a2 * x0 * x0;
1722                (a0, a1, a2)
1723            };
1724            let (c0, c1, c2) = quad([samples[0].1.powi(2), samples[1].1.powi(2), samples[2].1.powi(2)]);
1725            let root = if c2.abs() < 1e-300 {
1726                -c0 / c1
1727            } else {
1728                let disc = c1 * c1 - 4.0 * c2 * c0;
1729                if disc < 0.0 {
1730                    continue;
1731                }
1732                let r1 = (-c1 + disc.sqrt()) / (2.0 * c2);
1733                let r2 = (-c1 - disc.sqrt()) / (2.0 * c2);
1734                if (r1 - level).abs() < (r2 - level).abs() { r1 } else { r2 }
1735            };
1736            if !(root > samples[0].0 && (root - level).abs() < delta) {
1737                continue;
1738            }
1739            let (m0, m1, m2) = quad([samples[0].2, samples[1].2, samples[2].2]);
1740            let middle = m0 + m1 * root + m2 * root * root;
1741            refined.push((root, middle, x.hi_source, y.lo_source));
1742        }
1743        if !refined.is_empty() {
1744            // Every merging pair records its own point; the break is their mean
1745            // level (equal to round-off where the spaces are copies of one).
1746            let root = refined.iter().map(|r| r.0).sum::<f64>() / refined.len() as f64;
1747            for &(own, middle, a_source, b_source) in &refined {
1748                if debug {
1749                    eprintln!("  fold: break {level:.12} -> {own:.12} at o {middle:.12}");
1750                }
1751                engine.critical[a_source].push((root, middle));
1752                engine.critical[b_source].push((root, middle));
1753            }
1754            breaks[index] = root;
1755            fold_ends.push(root);
1756        }
1757    }
1758
1759    if debug {
1760        eprintln!("solid sweep: after events {:?} breaks {}", engine.counters.get(), breaks.len());
1761    }
1762    // --- Sample each band's interval ends.
1763    let mut pieces: Vec<Piece> = Vec::new();
1764    // Per band: the intervals at its bottom and top (for the level segments).
1765    let mut band_ends: Vec<(f64, f64, Vec<(usize, usize)>)> = Vec::new();
1766    for window in breaks.windows(2) {
1767        let (a, b) = (window[0], window[1]);
1768        if b - a < 1e-12 * scale {
1769            continue;
1770        }
1771        let mid = engine.intervals_at(0.5 * (a + b))?;
1772        if mid.is_empty() {
1773            continue;
1774        }
1775        let (band_pieces, _, _) = sample_band(&engine, a, b, &mid, &fold_ends)?;
1776        let first = pieces.len();
1777        let pairs = (0..band_pieces.len() / 2).map(|i| (first + 2 * i, first + 2 * i + 1)).collect();
1778        pieces.extend(band_pieces);
1779        band_ends.push((a, b, pairs));
1780    }
1781    if pieces.is_empty() {
1782        return Ok(Vec::new());
1783    }
1784    if debug {
1785        eprintln!("solid sweep: after bands {:?}", engine.counters.get());
1786        for (k, critical) in engine.critical.iter().enumerate() {
1787            eprintln!("  source {k} {:?}: critical {:?}", engine.sources[k], critical);
1788        }
1789    }
1790
1791    // --- One value per interval end at each break. The band below and the band
1792    //     above read an end that continues across a break from their own
1793    //     sources, at a level found only to the flicker cluster; the two agree to
1794    //     far below any feature but not to the bit, and a sliver of level segment
1795    //     between them is an edge no solid should carry. Snap every end at a level
1796    //     to its cluster's first value (ends of one interval meeting at a tip
1797    //     included).
1798    let snap_length = 1e-5 * scale;
1799    let mut break_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1800    break_levels.sort_by(|a, b| a.total_cmp(b));
1801    break_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1802    for &level in &break_levels {
1803        let tolerance = if engine.angular { snap_length / level.max(1e-12) } else { snap_length };
1804        let mut ends: Vec<(usize, bool)> = Vec::new();
1805        for (index, piece) in pieces.iter().enumerate() {
1806            if (piece.points[0].0 - level).abs() < 1e-12 * scale {
1807                ends.push((index, false));
1808            }
1809            if (piece.points.last().expect("nonempty").0 - level).abs() < 1e-12 * scale {
1810                ends.push((index, true));
1811            }
1812        }
1813        let value = |pieces: &Vec<Piece>, (index, last): (usize, bool)| {
1814            if last { pieces[index].points.last().expect("nonempty").1 } else { pieces[index].points[0].1 }
1815        };
1816        for i in 0..ends.len() {
1817            let anchor = value(&pieces, ends[i]);
1818            for j in i + 1..ends.len() {
1819                let other = value(&pieces, ends[j]);
1820                let d = engine.o_difference(other, anchor);
1821                if d.abs() < tolerance && d != 0.0 {
1822                    let (index, last) = ends[j];
1823                    let target = other - d;
1824                    let points = &mut pieces[index].points;
1825                    if last {
1826                        points.last_mut().expect("nonempty").1 = target;
1827                    } else {
1828                        points[0].1 = target;
1829                    }
1830                }
1831            }
1832        }
1833    }
1834
1835    // --- Level segments: where the intervals above and below a level differ.
1836    let mut all_levels: Vec<f64> = band_ends.iter().flat_map(|e| [e.0, e.1]).collect();
1837    all_levels.sort_by(|a, b| a.total_cmp(b));
1838    all_levels.dedup_by(|b, a| (*b - *a).abs() < 1e-12 * scale);
1839    for &level in &all_levels {
1840        let mut below: Vec<(f64, f64)> = Vec::new();
1841        let mut above: Vec<(f64, f64)> = Vec::new();
1842        for (a, b, pairs) in &band_ends {
1843            for &(lo, hi) in pairs {
1844                let (lo, hi) = (&pieces[lo].points, &pieces[hi].points);
1845                if (b - level).abs() < 1e-12 * scale {
1846                    below.push((lo.last().expect("nonempty").1, hi.last().expect("nonempty").1));
1847                }
1848                if (a - level).abs() < 1e-12 * scale {
1849                    above.push((lo[0].1, hi[0].1));
1850                }
1851            }
1852        }
1853        let tolerance = if engine.angular { 1e-9 * scale / level.max(1e-12) } else { 1e-9 * scale };
1854        for (lo, hi) in symmetric_difference(&below, &above, engine.angular, tolerance) {
1855            let steps = 2;
1856            pieces.push(Piece {
1857                points: (0..=steps)
1858                    .map(|k| (level, lo + (hi - lo) * k as f64 / steps as f64))
1859                    .collect(),
1860                sigma: (0..=steps).map(|k| k as f64 / steps as f64).collect(),
1861                level_segment: true,
1862            });
1863        }
1864    }
1865
1866    if std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
1867        eprintln!("solid sweep: breaks {breaks:?}");
1868        for piece in &pieces {
1869            let a = piece.points[0];
1870            let b = *piece.points.last().expect("nonempty");
1871            eprintln!(
1872                "  piece {} ({:.9}, {:.9}) -> ({:.9}, {:.9}) n={}",
1873                if piece.level_segment { "LEVEL" } else { "edge " },
1874                a.0, a.1, b.0, b.1, piece.points.len()
1875            );
1876            if std::env::var("BREP_DEBUG_SOLID_SWEEP").as_deref() == Ok("2") {
1877                for p in piece.points.iter().take(6) {
1878                    eprintln!("      {:.12} {:.12}", p.0, p.1);
1879                }
1880            }
1881        }
1882    }
1883    // --- Assemble loops and build the bodies.
1884    let loops = assemble_loops(&engine, pieces)?;
1885    let mut bodies = Vec::new();
1886    for pieces in loops {
1887        bodies.push(build_body(&engine, &pieces)?);
1888    }
1889    Ok(bodies)
1890}
1891
1892fn find_events(
1893    engine: &Engine,
1894    a: f64,
1895    sa: &[Interval],
1896    b: f64,
1897    sb: &[Interval],
1898    breaks: &mut Vec<f64>,
1899    depth: usize,
1900) -> Result<(), KernelRefusal> {
1901    if compatible(engine, sa, sb) {
1902        return Ok(());
1903    }
1904    if depth <= 2 && std::env::var("BREP_DEBUG_SOLID_SWEEP").is_ok() {
1905        let summary = |s: &[Interval]| -> Vec<(usize, usize, f64, f64)> {
1906            s.iter().map(|x| (x.lo_source, x.hi_source, (x.lo * 1e4).round() / 1e4, (x.hi * 1e4).round() / 1e4)).collect()
1907        };
1908        eprintln!("  find_events depth {depth} [{a:.9}, {b:.9}] {:?}\n      -> {:?} counters {:?}", summary(sa), summary(sb), engine.counters.get());
1909    }
1910    if b - a < 1e-13 * engine.scale || depth > 60 {
1911        breaks.push(0.5 * (a + b));
1912        return Ok(());
1913    }
1914    let m = 0.5 * (a + b);
1915    let sm = engine.intervals_at(m)?;
1916    // Both halves stay incompatible only where the reading flickers; follow the
1917    // half that changes first and let the other be found from the scan above.
1918    if !compatible(engine, sa, &sm) && !compatible(engine, &sm, sb) && depth > 8 {
1919        breaks.push(m);
1920        return Ok(());
1921    }
1922    find_events(engine, a, sa, m, &sm, breaks, depth + 1)?;
1923    find_events(engine, m, &sm, b, sb, breaks, depth + 1)
1924}
1925
1926/// The candidate of `source` at `level` nearest `guess` (or its critical point
1927/// at that level).
1928fn end_value(engine: &Engine, source: usize, level: f64, guess: f64) -> f64 {
1929    let mut candidates = Vec::new();
1930    engine.source_candidates(source, level, &mut candidates);
1931    let mut options: Vec<f64> = candidates.iter().map(|c| c.0).collect();
1932    for critical in &engine.critical[source] {
1933        if (critical.0 - level).abs() < 1e-7 * engine.scale {
1934            options.push(critical.1);
1935        }
1936    }
1937    let mut best = guess;
1938    let mut distance = f64::INFINITY;
1939    for option in options {
1940        let d = engine.o_difference(option, guess).abs();
1941        if d < distance {
1942            distance = d;
1943            best = guess + engine.o_difference(option, guess);
1944        }
1945    }
1946    best
1947}
1948
1949/// Sample a band (a, b) of constant structure; return its end pieces and the
1950/// intervals at its bottom and top.
1951#[allow(clippy::type_complexity)]
1952fn sample_band(
1953    engine: &Engine,
1954    a: f64,
1955    b: f64,
1956    mid: &[Interval],
1957    fold_ends: &[f64],
1958) -> Result<(Vec<Piece>, Vec<(f64, f64)>, Vec<(f64, f64)>), KernelRefusal> {
1959    let count = mid.len();
1960    let n = 24usize;
1961    // Next to a fold the extremum Newton flickers; keep every sample at least
1962    // this far from a fold end (the end itself reads the fold's critical point).
1963    let near_fold = fold_ends.iter().any(|f| (f - a).abs() < 1e-12 * engine.scale || (f - b).abs() < 1e-12 * engine.scale);
1964    let n_cap = if near_fold {
1965        let keep = (1e-4 * engine.scale).min((b - a) / 50.0);
1966        // smallest σ step: (b − a)(1 − cos(π/n))/2 ≥ keep
1967        let ratio = (1.0 - 2.0 * keep / (b - a)).clamp(-1.0, 1.0);
1968        ((PI / ratio.acos()).floor() as usize).max(8)
1969    } else {
1970        192
1971    };
1972    let mut n = n.min(n_cap);
1973    loop {
1974        let sigma: Vec<f64> = (0..=n).map(|k| k as f64 / n as f64).collect();
1975        let level_at = |s: f64| a + (b - a) * (1.0 - (PI * s).cos()) / 2.0;
1976        // Track every interval from the middle sample outward.
1977        let mut lo = vec![vec![f64::NAN; n + 1]; count];
1978        let mut hi = vec![vec![f64::NAN; n + 1]; count];
1979        let mut lo_src = vec![vec![0usize; n + 1]; count];
1980        let mut hi_src = vec![vec![0usize; n + 1]; count];
1981        let mid_index = n / 2;
1982        let mid_intervals = engine.intervals_at(level_at(sigma[mid_index]))?;
1983        if mid_intervals.len() != count {
1984            return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
1985        }
1986        for (i, interval) in mid_intervals.iter().enumerate() {
1987            lo[i][mid_index] = interval.lo;
1988            hi[i][mid_index] = interval.hi;
1989            lo_src[i][mid_index] = interval.lo_source;
1990            hi_src[i][mid_index] = interval.hi_source;
1991        }
1992        for direction in [-1i64, 1] {
1993            let mut k = mid_index as i64 + direction;
1994            while k > 0 && (k as usize) < n {
1995                let ku = k as usize;
1996                let previous = (ku as i64 - direction) as usize;
1997                let intervals = engine.intervals_at(level_at(sigma[ku]))?;
1998                if intervals.len() != count {
1999                    return Err(refuse("band_structure", format!("the structure changes inside the band {a}..{b}")));
2000                }
2001                for i in 0..count {
2002                    let centre = 0.5 * (lo[i][previous] + hi[i][previous]);
2003                    let nearest = intervals
2004                        .iter()
2005                        .min_by(|x, y| {
2006                            let dx = engine.o_difference(0.5 * (x.lo + x.hi), centre).abs();
2007                            let dy = engine.o_difference(0.5 * (y.lo + y.hi), centre).abs();
2008                            dx.total_cmp(&dy)
2009                        })
2010                        .expect("nonempty");
2011                    let shift = engine.o_difference(nearest.lo, lo[i][previous]) - (nearest.lo - lo[i][previous]);
2012                    lo[i][ku] = nearest.lo + shift;
2013                    hi[i][ku] = nearest.hi + shift;
2014                    lo_src[i][ku] = nearest.lo_source;
2015                    hi_src[i][ku] = nearest.hi_source;
2016                }
2017                k += direction;
2018            }
2019        }
2020        // Ends: the same source's value at exactly the band's end levels,
2021        // nearest the linear extrapolation of the two nearest samples.
2022        for i in 0..count {
2023            for (end, inner, inner2, level) in [(0usize, 1usize, 2usize, a), (n, n - 1, n - 2, b)] {
2024                let extrapolate = |values: &Vec<f64>| 2.0 * values[inner] - values[inner2];
2025                lo[i][end] = end_value(engine, lo_src[i][inner], level, extrapolate(&lo[i]));
2026                hi[i][end] = end_value(engine, hi_src[i][inner], level, extrapolate(&hi[i]));
2027            }
2028        }
2029        // Accuracy: compare the fitted ends at the σ mid-points with exact values.
2030        let mut worst = 0.0f64;
2031        let mut worst_at = (0usize, 0usize, 0usize, 0.0f64, 0.0f64);
2032        for i in 0..count {
2033            for (side, values) in [&lo[i], &hi[i]].into_iter().enumerate() {
2034                let points: Vec<Vec3> = (0..=n)
2035                    .map(|k| reduced_to_plane(engine, level_at(sigma[k]), values[k]))
2036                    .collect();
2037                let curve = fit_piece(&points)?;
2038                let parameters = chord_parameters(&points);
2039                for k in (1..n - 1).step_by((n / 8).max(1)) {
2040                    let s = 0.5 * (sigma[k] + sigma[k + 1]);
2041                    let level = level_at(s);
2042                    let intervals = engine.intervals_at(level)?;
2043                    let guess = 0.5 * (values[k] + values[k + 1]);
2044                    let exact = intervals
2045                        .iter()
2046                        .flat_map(|x| [x.lo, x.hi])
2047                        .map(|o| guess + engine.o_difference(o, guess))
2048                        .min_by(|x, y| (x - guess).abs().total_cmp(&(y - guess).abs()))
2049                        .unwrap_or(guess);
2050                    let truth = reduced_to_plane(engine, level, exact);
2051                    let bracket = (parameters[k.saturating_sub(1)], parameters[(k + 2).min(n)]);
2052                    let off = distance_in_bracket(&curve, truth, bracket.0, bracket.1)?;
2053                    if off > worst {
2054                        worst = off;
2055                        worst_at = (i, side, k, level, exact);
2056                    }
2057                }
2058            }
2059        }
2060        if n >= n_cap && worst > 1e-6 * engine.scale {
2061            return Err(refuse(
2062                "fit_tolerance",
2063                format!(
2064                    "a boundary piece of the band {a}..{b} stays {worst:.3e} off its envelope at {n} samples \
2065                     (interval {}, {}, sample {}, level {}, o {})",
2066                    worst_at.0,
2067                    if worst_at.1 == 0 { "lo" } else { "hi" },
2068                    worst_at.2,
2069                    worst_at.3,
2070                    worst_at.4
2071                ),
2072            ));
2073        }
2074        if worst <= 1e-8 * engine.scale || n >= n_cap {
2075            let mut pieces = Vec::new();
2076            for i in 0..count {
2077                for values in [&lo[i], &hi[i]] {
2078                    pieces.push(Piece {
2079                        points: (0..=n).map(|k| (level_at(sigma[k]), values[k])).collect(),
2080                        sigma: sigma.clone(),
2081                        level_segment: false,
2082                    });
2083                }
2084            }
2085            let bottoms = (0..count).map(|i| (lo[i][0], hi[i][0])).collect();
2086            let tops = (0..count).map(|i| (lo[i][n], hi[i][n])).collect();
2087            return Ok((pieces, bottoms, tops));
2088        }
2089        n = (n * 2).min(n_cap);
2090    }
2091}
2092
2093/// Chord-length parameters for interpolating `points`, normalised to [0, 1].
2094/// Not the sampling's own `σ`: the levels cluster at both ends of a band, which
2095/// near a tip is what keeps a √ end smooth, but at a regular end it makes the
2096/// curve's speed vanish there, and an interpolant through a zero-speed end
2097/// cusps (the screw groove's wall folded where it met the closing arc).
2098fn chord_parameters(points: &[Vec3]) -> Vec<f64> {
2099    let mut parameters = vec![0.0; points.len()];
2100    for k in 1..points.len() {
2101        parameters[k] = parameters[k - 1] + points[k].sub(points[k - 1]).length();
2102    }
2103    let total = parameters[points.len() - 1].max(1e-300);
2104    for parameter in &mut parameters {
2105        *parameter /= total;
2106    }
2107    let last = points.len() - 1;
2108    parameters[last] = 1.0;
2109    parameters
2110}
2111
2112fn fit_piece(points: &[Vec3]) -> Result<NurbsCurve, KernelRefusal> {
2113    interpolate_curve(points, 3, &chord_parameters(points)).or_refuse(KernelStage::Fragment, "piece_fit")
2114}
2115
2116/// Distance from `target` to `curve` over the parameter bracket [a, b].
2117fn distance_in_bracket(curve: &NurbsCurve, target: Vec3, a: f64, b: f64) -> Result<f64, KernelRefusal> {
2118    let distance = |t: f64| -> Result<f64, KernelRefusal> {
2119        Ok(curve.evaluate(t).or_refuse(KernelStage::Fragment, "piece_eval")?.sub(target).length())
2120    };
2121    let ratio = 0.618_033_988_749_894_9;
2122    let (mut lo, mut hi) = (a, b);
2123    for _ in 0..80 {
2124        let c = hi - ratio * (hi - lo);
2125        let d = lo + ratio * (hi - lo);
2126        if distance(c)? < distance(d)? {
2127            hi = d;
2128        } else {
2129            lo = c;
2130        }
2131    }
2132    distance(0.5 * (lo + hi))
2133}
2134
2135/// `below Δ above` as intervals of `o` (both lists at one level).
2136fn symmetric_difference(below: &[(f64, f64)], above: &[(f64, f64)], angular: bool, tolerance: f64) -> Vec<(f64, f64)> {
2137    // Normalise to [base, base + 2π) for angular values, splitting wrapped ones.
2138    let normalise = |list: &[(f64, f64)]| -> Vec<(f64, f64)> {
2139        let mut out = Vec::new();
2140        for &(lo, hi) in list {
2141            if angular {
2142                let shift = lo.rem_euclid(TAU) - lo;
2143                let (lo, hi) = (lo + shift, hi + shift);
2144                if hi > TAU {
2145                    out.push((lo, TAU));
2146                    out.push((0.0, hi - TAU));
2147                } else {
2148                    out.push((lo, hi));
2149                }
2150            } else {
2151                out.push((lo, hi));
2152            }
2153        }
2154        out
2155    };
2156    let a = normalise(below);
2157    let b = normalise(above);
2158    let mut cuts: Vec<f64> = a.iter().chain(&b).flat_map(|&(lo, hi)| [lo, hi]).collect();
2159    cuts.sort_by(|x, y| x.total_cmp(y));
2160    cuts.dedup_by(|y, x| (*y - *x).abs() < tolerance);
2161    let inside = |list: &Vec<(f64, f64)>, x: f64| list.iter().any(|&(lo, hi)| x > lo && x < hi);
2162    let mut out: Vec<(f64, f64)> = Vec::new();
2163    for w in cuts.windows(2) {
2164        let m = 0.5 * (w[0] + w[1]);
2165        if inside(&a, m) != inside(&b, m) && w[1] - w[0] > tolerance {
2166            if let Some(last) = out.last_mut() {
2167                if (last.1 - w[0]).abs() < tolerance {
2168                    last.1 = w[1];
2169                    continue;
2170                }
2171            }
2172            out.push((w[0], w[1]));
2173        }
2174    }
2175    if angular && out.len() > 1 {
2176        let first = out[0];
2177        let last = *out.last().expect("nonempty");
2178        if first.0.abs() < tolerance && (last.1 - TAU).abs() < tolerance {
2179            out.pop();
2180            out[0] = (last.0 - TAU, first.1);
2181        }
2182    }
2183    out
2184}
2185
2186/// A reduced point on the section plane the body is built from.
2187fn reduced_to_plane(engine: &Engine, level: f64, o: f64) -> Vec3 {
2188    let clip = engine.clip;
2189    match engine.class {
2190        Class::Screw(c) => {
2191            let z = clip.axial[0];
2192            let alpha = o + z / c;
2193            engine.frame.world([level * alpha.cos(), level * alpha.sin(), z])
2194        }
2195        Class::Turn => {
2196            let theta = clip.angle.map(|a| a[0]).unwrap_or(0.0);
2197            engine.frame.world([level * theta.cos(), level * theta.sin(), o])
2198        }
2199        Class::Planar => engine.frame.world([level * o.cos(), level * o.sin(), clip.axial[0]]),
2200    }
2201}
2202
2203/// Join pieces end to end into closed loops (in the section plane).
2204fn assemble_loops(engine: &Engine, pieces: Vec<Piece>) -> Result<Vec<Vec<Piece>>, KernelRefusal> {
2205    let tolerance = 1e-6 * engine.scale;
2206    let ends: Vec<(Vec3, Vec3)> = pieces
2207        .iter()
2208        .map(|piece| {
2209            let first = piece.points[0];
2210            let last = *piece.points.last().expect("nonempty");
2211            (reduced_to_plane(engine, first.0, first.1), reduced_to_plane(engine, last.0, last.1))
2212        })
2213        .collect();
2214    let mut used = vec![false; pieces.len()];
2215    let mut orders: Vec<Vec<(usize, bool)>> = Vec::new();
2216    for start in 0..pieces.len() {
2217        if used[start] {
2218            continue;
2219        }
2220        used[start] = true;
2221        let mut order = vec![(start, false)];
2222        let origin = ends[start].0;
2223        let mut cursor = ends[start].1;
2224        let mut guard = 0;
2225        while cursor.sub(origin).length() > tolerance {
2226            guard += 1;
2227            if guard > pieces.len() {
2228                return Err(refuse("loop_open", "the section's boundary does not close".into()));
2229            }
2230            let mut next = None;
2231            let mut best = tolerance;
2232            for k in 0..pieces.len() {
2233                if used[k] {
2234                    continue;
2235                }
2236                for (reversed, end) in [(false, ends[k].0), (true, ends[k].1)] {
2237                    let d = end.sub(cursor).length();
2238                    if d < best {
2239                        best = d;
2240                        next = Some((k, reversed));
2241                    }
2242                }
2243            }
2244            let Some((k, reversed)) = next else {
2245                return Err(refuse(
2246                    "loop_open",
2247                    format!("the section's boundary does not close (a gap at {:?})", cursor),
2248                ));
2249            };
2250            used[k] = true;
2251            order.push((k, reversed));
2252            cursor = if reversed { ends[k].0 } else { ends[k].1 };
2253        }
2254        orders.push(order);
2255    }
2256    if orders.len() > 1 {
2257        // A loop inside another is a hole; not built here.
2258        let polygon = |order: &Vec<(usize, bool)>| -> Vec<Vec3> {
2259            order
2260                .iter()
2261                .flat_map(|&(k, _)| pieces[k].points.iter().map(|p| reduced_to_plane(engine, p.0, p.1)))
2262                .collect()
2263        };
2264        let polygons: Vec<Vec<Vec3>> = orders.iter().map(polygon).collect();
2265        for (i, a) in polygons.iter().enumerate() {
2266            for (j, b) in polygons.iter().enumerate() {
2267                if i != j && point_in_polygon(engine, a[0], b) {
2268                    return Err(refuse("section_hole", "the section has a hole, which is not built here".into()));
2269                }
2270            }
2271        }
2272    }
2273    let mut slots: Vec<Option<Piece>> = pieces.into_iter().map(Some).collect();
2274    Ok(orders
2275        .into_iter()
2276        .map(|order| {
2277            order
2278                .into_iter()
2279                .map(|(k, reversed)| {
2280                    let mut piece = slots[k].take().expect("each piece once");
2281                    if reversed {
2282                        piece.points.reverse();
2283                        piece.sigma = piece.sigma.iter().rev().map(|s| 1.0 - s).collect();
2284                    }
2285                    piece
2286                })
2287                .collect()
2288        })
2289        .collect())
2290}
2291
2292fn point_in_polygon(engine: &Engine, p: Vec3, polygon: &[Vec3]) -> bool {
2293    let local = |x: Vec3| -> [f64; 2] {
2294        let l = engine.frame.local(x);
2295        match engine.class {
2296            Class::Turn => [l[0].hypot(l[1]), l[2]],
2297            _ => [l[0], l[1]],
2298        }
2299    };
2300    let q = local(p);
2301    let mut inside = false;
2302    for k in 0..polygon.len() {
2303        let a = local(polygon[k]);
2304        let b = local(polygon[(k + 1) % polygon.len()]);
2305        if (a[1] > q[1]) != (b[1] > q[1]) {
2306            let x = a[0] + (q[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
2307            if x > q[0] {
2308                inside = !inside;
2309            }
2310        }
2311    }
2312    inside
2313}
2314
2315/// Fit each piece and build the body for one loop.
2316fn build_body(engine: &Engine, pieces: &[Piece]) -> Result<BrepSolid, KernelRefusal> {
2317    let clip = engine.clip;
2318    let mut profile: Vec<NurbsCurve> = Vec::new();
2319    // Snap shared ends so consecutive curves meet exactly.
2320    let count = pieces.len();
2321    let mut start_points: Vec<Vec3> = pieces
2322        .iter()
2323        .map(|p| reduced_to_plane(engine, p.points[0].0, p.points[0].1))
2324        .collect();
2325    // A level segment is built exactly from its own ends, so the piece after
2326    // it starts where it ends.
2327    for (index, piece) in pieces.iter().enumerate() {
2328        if piece.level_segment && engine.class != Class::Turn {
2329            let (level, o1) = *piece.points.last().expect("nonempty");
2330            start_points[(index + 1) % count] = reduced_to_plane(engine, level, o1);
2331        }
2332    }
2333    for (index, piece) in pieces.iter().enumerate() {
2334        let start = start_points[index];
2335        let end = start_points[(index + 1) % count];
2336        if piece.level_segment {
2337            let (level, o0) = piece.points[0];
2338            let o1 = piece.points.last().expect("nonempty").1;
2339            let curve = match engine.class {
2340                Class::Turn => make_line(start, end),
2341                Class::Screw(c) => {
2342                    let shift = clip.axial[0] / c;
2343                    level_arc(engine, level, o0 + shift, o1 + shift)
2344                }
2345                Class::Planar => level_arc(engine, level, o0, o1),
2346            }
2347            .or_refuse(KernelStage::Fragment, "level_segment")?;
2348            profile.push(curve);
2349            continue;
2350        }
2351        let mut points: Vec<Vec3> = piece
2352            .points
2353            .iter()
2354            .map(|p| reduced_to_plane(engine, p.0, p.1))
2355            .collect();
2356        points[0] = start;
2357        let last = points.len() - 1;
2358        points[last] = end;
2359        profile.push(fit_piece(&points)?);
2360    }
2361    match engine.class {
2362        Class::Screw(c) => {
2363            let span = (clip.axial[1] - clip.axial[0]) / c; // radians of screw
2364            let sections = ((span.abs() / TAU * 64.0).ceil() as usize).max(8) + 1;
2365            let mut lofted = Vec::with_capacity(sections);
2366            for k in 0..sections {
2367                let angle = span * k as f64 / (sections - 1) as f64;
2368                let lift = engine.frame.ez.scale(c * angle);
2369                let moved: Vec<NurbsCurve> = profile
2370                    .iter()
2371                    .map(|curve| transform_curve(curve, |x| rotate(x, clip.axis_origin, engine.frame.ez, angle).add(lift)))
2372                    .collect::<Result<_, _>>()?;
2373                lofted.push(moved);
2374            }
2375            loft_profile_brep(&lofted)
2376        }
2377        Class::Turn => {
2378            let angle = clip.angle.map(|a| a[1] - a[0]).unwrap_or(TAU);
2379            revolve_profile_brep(&profile, clip.axis_origin, engine.frame.ez, angle)
2380                .or_refuse(KernelStage::Fragment, "revolve")
2381        }
2382        Class::Planar => extrude_profile_brep(&profile, engine.frame.ez, clip.axial[1] - clip.axial[0]),
2383    }
2384}
2385
2386fn level_arc(engine: &Engine, level: f64, a0: f64, a1: f64) -> Result<NurbsCurve, String> {
2387    let centre = engine.frame.world([0.0, 0.0, match engine.class {
2388        Class::Screw(_) | Class::Planar => engine.clip.axial[0],
2389        Class::Turn => 0.0,
2390    }]);
2391    let (start, end) = if a1 >= a0 { (a0, a1) } else { (a1, a0) };
2392    let arc = make_arc(centre, engine.frame.ex, engine.frame.ey, level, start, end)?;
2393    if a1 >= a0 {
2394        Ok(arc)
2395    } else {
2396        arc.reversed()
2397    }
2398}
2399
2400fn transform_curve(curve: &NurbsCurve, map: impl Fn(Vec3) -> Vec3) -> Result<NurbsCurve, KernelRefusal> {
2401    let control_points = curve
2402        .control_points
2403        .iter()
2404        .map(|p| {
2405            let w = p.w;
2406            let x = map(Vec3::new(p.x / w, p.y / w, p.z / w));
2407            crate::Vec4 { x: x.x * w, y: x.y * w, z: x.z * w, w }
2408        })
2409        .collect();
2410    NurbsCurve::new(curve.degree, curve.knots.clone(), control_points).or_refuse(KernelStage::Fragment, "curve_new")
2411}
2412
2413/// Refuse a screw or turn whose first or last position reaches the clip.
2414fn check_ends_clear(engine: &Engine) -> Result<(), KernelRefusal> {
2415    let clip = engine.clip;
2416    // Tool boundary samples at its own place (clip frame).
2417    let mut samples: Vec<[f64; 3]> = Vec::new();
2418    for (index, source) in engine.sources.iter().enumerate() {
2419        if let Some(grid) = &engine.grids[index] {
2420            for i in 0..grid.nu {
2421                for j in 0..grid.nv {
2422                    let (u, v) = Engine::grid_param(grid, i, j);
2423                    samples.push(engine.frame.local(engine.source_point(*source, u, v)));
2424                }
2425            }
2426        }
2427    }
2428    let margin = 1e-6 * engine.scale;
2429    for t in [engine.motion.from, engine.motion.to] {
2430        for p in &samples {
2431            let x = engine.frame.local(engine.motion_apply(t, engine.frame.world(*p)));
2432            let level = x[0].hypot(x[1]);
2433            if level > clip.radius[1] + margin || level < clip.radius[0] - margin {
2434                continue;
2435            }
2436            if x[2] < clip.axial[0] - margin || x[2] > clip.axial[1] + margin {
2437                continue;
2438            }
2439            if let Some([a0, a1]) = clip.angle {
2440                let angle = x[1].atan2(x[0]);
2441                let mid = 0.5 * (a0 + a1);
2442                let rel = wrap_pi(angle - mid);
2443                if rel.abs() > 0.5 * (a1 - a0) + 1e-9 {
2444                    continue;
2445                }
2446            }
2447            return Err(refuse(
2448                "ends_reach_clip",
2449                format!(
2450                    "the tool reaches the clip region at the motion's {} (t = {t}); the sweep is \
2451                     not invariant there, so the region would be wrong",
2452                    if t == engine.motion.from { "start" } else { "end" }
2453                ),
2454            ));
2455        }
2456    }
2457    Ok(())
2458}
2459