Skip to main content

ogeom_bool/
lib.rs

1//! Boolean operations.
2//!
3//! *Elsewhere:* `BOPDS`, `BOPAlgo`, `BOPTools`, `IntTools` and `BRepAlgoAPI`.
4//!
5//! The architectural insight worth preserving: **fuse, common, cut, section and
6//! split are one algorithm (general fuse) plus a selection predicate.** The
7//! pipeline is
8//!
9//! 1. a data structure of indexed sub-shapes, per-type interference lists
10//!    (V/V, V/E, V/F, E/E, E/F, F/F), pave blocks and common blocks;
11//! 2. a pave filler running in strictly increasing dimension, ending in face/face
12//!    intersection, section-edge construction and pcurve generation;
13//! 3. a builder that splits faces in 2D parametric space, unifies same-domain
14//!    faces, rebuilds solids from face sets, and repairs tolerances;
15//! 4. filters that select from the general-fuse result.
16//!
17//! # One pipeline, any analytic face
18//!
19//! The pipeline runs on solids whose faces are any surface §7's intersectors
20//! answer for (planes, cylinders, spheres, cones, tori), with the planar
21//! case as nothing more than the case where every curve is a line. Sections
22//! come from [`intersect_surfaces`](ogeom_intersect::intersect_surfaces), exact
23//! with same-parameter pcurves where the projection has a closed form and
24//! *marched and fitted to a stated tolerance* where it does not. Paves (the
25//! points where sections meet boundary edges and each other) come from the
26//! exact curve/curve intersection. Every face is then split in its own
27//! parameter space by an arrangement of *strands*: polyline scaffolding, each
28//! naming the exact sub-curve it stands for, so the combinatorics run on
29//! polylines and the rebuilt result is exact curves, pcurves attached both
30//! sides, sewn back into shared topology by the sewing whose flipped-carry
31//! bug this crate found and §9 fixed.
32//!
33//! Pieces are classified against the other solid by the exact ray classifier
34//! and the filters select: fuse keeps what is outside, common what is inside,
35//! cut flips the tool's contribution. Same-domain contact (a piece lying
36//! *on* the other boundary) and tangential touching are refused with an
37//! error naming the deferred entry, never silently mishandled.
38
39mod arrange;
40mod bins;
41mod defeature;
42mod half_space;
43
44pub use defeature::remove_faces;
45
46use ogeom_algo::{
47    Built, Containment, History, is_shell_closed, make_edge_between, make_face_on, make_vertex,
48    make_wire, sew, shape_bounds,
49};
50use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
51use ogeom_geom::Curve2d as _;
52use ogeom_geom::Curve3d as _;
53use ogeom_geom::Surface as _;
54use ogeom_geom::{Curve, PlanarCurve, SurfaceGeometry, Transformable};
55use ogeom_math::{Point, Point2};
56use ogeom_topo::{
57    EdgeRepr, Filter, Location, Model, NodeData, Shape, ShapeType, explore, explore_unique,
58};
59
60use arrange::{
61    Strand, Traversal, assemble as arrange_pieces, inside_many, inside_many_slanted, inside_rings,
62};
63
64/// Parameter-space chord for the polyline scaffolding.
65const SCAFFOLD_CHORD: f64 = 1e-3;
66
67/// Parameter-space node snap for the arrangement.
68const PARAM_SNAP: f64 = 1e-6;
69
70// --- gathering ---------------------------------------------------------------
71
72/// An edge occurrence's identity: its node and where it is placed.
73///
74/// One node placed twice is two edges in space: a prism's far cap is its
75/// near cap moved, the same nodes under a displacement. Paves shared by
76/// node alone would split the near cap's edges wherever the far cap's were
77/// crossed. The placement is folded in as its hash, which keeps the key a
78/// plain value.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
80struct EdgeKey {
81    node: ogeom_topo::TShapeId,
82    placement: u64,
83}
84
85impl EdgeKey {
86    fn of(edge: &Shape) -> Self {
87        use core::hash::{Hash as _, Hasher as _};
88        let mut hasher = std::collections::hash_map::DefaultHasher::new();
89        edge.location().hash(&mut hasher);
90        Self {
91            node: edge.node(),
92            placement: hasher.finish(),
93        }
94    }
95
96    /// The node's index, for the debug traces.
97    fn index(self) -> u32 {
98        self.node.index()
99    }
100}
101
102/// One boundary strand source: an edge as one face uses it, with the pcurve
103/// that side of it.
104struct BoundaryEdge {
105    /// The edge occurrence's identity, for sharing paves across faces.
106    node: EdgeKey,
107    /// The curve in world space.
108    curve: Curve,
109    /// The portion the edge covers, in the curve's parameter.
110    crange: (f64, f64),
111    /// The pcurve on this face's surface.
112    pcurve: PlanarCurve,
113    /// The portion of the pcurve, mapped proportionally from `crange`.
114    prange: (f64, f64),
115    /// The other side of a seam, where this edge is one.
116    other_side: Option<(PlanarCurve, (f64, f64))>,
117    /// The radius within which the edge honestly lies: a fitted rail can
118    /// carry a few dozen microns of construction slop, and every filter that
119    /// compares this edge's curve against exact geometry widens by it.
120    tolerance: f64,
121    /// The looser of the edge's two end vertices' own radii: the doubt an
122    /// earlier junction recorded there, which the arrangement's node snap
123    /// on this face must reach.
124    ends_tolerance: f64,
125    /// Each end vertex in space with its own recorded radius: a vertex that
126    /// owns a span is a junction of this boolean too, and every strand end
127    /// inside it names it.
128    ends: [(Point, f64); 2],
129    /// A conservative box round the edge: its samples, grown by twice the
130    /// sag measured between them and by its own radius.
131    bound: ogeom_math::Aabb,
132}
133
134/// A pole: an edge that bounds a face in parameter space and collapses to
135/// a point in space.
136///
137/// A sphere's poles and a cone's apex have no curve to split, but they are
138/// half the chart's boundary; leave them out and the face's outline does
139/// not close, and nothing can be arranged inside it.
140struct PoleEdge {
141    /// Where the pole sits in space.
142    point: Point,
143    /// The chart line the pole runs along.
144    pcurve: PlanarCurve,
145    prange: (f64, f64),
146}
147
148/// One face, gathered and vetted.
149struct GFace {
150    face: Shape,
151    /// The surface in world space, placement applied.
152    surface: SurfaceGeometry,
153    /// A conservative world bound: the boundary edges' sampled extent plus
154    /// the surface's own allowance: nothing for a plane, measured sampling
155    /// slack for the ruled kinds whose rulings pin them to their boundary's
156    /// hull, most of the diagonal for anything that can genuinely bulge, a
157    /// dome past its equator. The poles join after. Gates the pair filter
158    /// and refusals; `OGEOM_BOOL_AUDIT_BOUNDS` audits its conservatism.
159    bound: ogeom_math::Aabb,
160    /// The scale the marching chord derives from: deliberately *not* the
161    /// filter box's diagonal, so the filter can tighten without silently
162    /// tightening the marcher.
163    chord_scale: f64,
164    edges: Vec<BoundaryEdge>,
165    poles: Vec<PoleEdge>,
166}
167
168/// An argument solid.
169struct GSolid {
170    solid: Shape,
171    faces: Vec<GFace>,
172}
173
174fn gather(model: &Model, solid: &Shape, tol: Tolerances) -> OgeomResult<GSolid> {
175    if model.kind_of(solid)? != ShapeType::Solid {
176        ogeom_bail!(Construction, "boolean arguments are solids");
177    }
178    for shell in explore_unique(model, solid, ShapeType::Shell)? {
179        if !is_shell_closed(model, &shell)? {
180            ogeom_bail!(Construction, "an open shell bounds no volume to operate on");
181        }
182    }
183
184    let mut faces = Vec::new();
185    for face in explore(model, solid, Filter::OfType(ShapeType::Face))? {
186        let Some(node) = model.node(&face) else {
187            ogeom_bail!(Dangling, "face is not in this model");
188        };
189        let NodeData::Face(data) = node.data() else {
190            ogeom_bail!(Construction, "face node holds no face data");
191        };
192        let Some(stored) = model.geometry().surface(data.surface) else {
193            ogeom_bail!(Dangling, "face refers to a surface not in this model");
194        };
195        let placement = face.transform(model.datums())?;
196        if (placement.scale_factor().abs() - 1.0).abs() > 1e-9 {
197            ogeom_bail!(
198                NotDone,
199                "a scaled placement changes a surface's parameterization out \
200                 from under its pcurves; bake the scale before a boolean"
201            );
202        }
203        let surface = stored.transformed(&placement, tol)?;
204        let surface_id = data.surface;
205
206        let mut edges = Vec::new();
207        let mut poles = Vec::new();
208        for edge in explore_unique(model, &face, ShapeType::Edge)? {
209            let Some(edge_node) = model.node(&edge) else {
210                ogeom_bail!(Dangling, "edge is not in this model");
211            };
212            let NodeData::Edge(edge_data) = edge_node.data() else {
213                ogeom_bail!(Construction, "edge node holds no edge data");
214            };
215            let Some(EdgeRepr::Curve3d { curve, range, .. }) = edge_data.curve3d() else {
216                // A degenerate edge (a sphere's pole, a cone's apex) has
217                // no extent to split, but it *does* bound the chart, and a
218                // chart whose top is missing bounds nothing.
219                if let Some(EdgeRepr::PCurve {
220                    curve: pc, range, ..
221                }) = edge_data.pcurve_for(surface_id, edge.location())
222                {
223                    let Some(planar) = model.geometry().pcurve(*pc) else {
224                        ogeom_bail!(Dangling, "pcurve is not in this model");
225                    };
226                    let at = explore_unique(model, &edge, ShapeType::Vertex)?;
227                    let Some(point) = at
228                        .first()
229                        .and_then(|v| model.node(v))
230                        .and_then(|n| n.data().as_vertex().map(|d| d.point))
231                    else {
232                        ogeom_bail!(Construction, "a pole with no vertex is nowhere");
233                    };
234                    let placed = edge.transform(model.datums())?.apply(point);
235                    poles.push(PoleEdge {
236                        point: placed,
237                        pcurve: planar.clone(),
238                        prange: *range,
239                    });
240                }
241                continue;
242            };
243            let Some(geometry) = model.geometry().curve(*curve) else {
244                ogeom_bail!(Dangling, "curve is not in this model");
245            };
246            let world = geometry.transformed(&edge.transform(model.datums())?, tol)?;
247            let (pcurve, prange, other_side) =
248                match edge_data.pcurve_for(surface_id, edge.location()) {
249                    Some(EdgeRepr::PCurve {
250                        curve: pc, range, ..
251                    }) => {
252                        let Some(planar) = model.geometry().pcurve(*pc) else {
253                            ogeom_bail!(Dangling, "pcurve is not in this model");
254                        };
255                        (planar.clone(), *range, None)
256                    }
257                    Some(EdgeRepr::Seam {
258                        forward,
259                        reversed,
260                        range,
261                        ..
262                    }) => {
263                        let (Some(f), Some(r)) = (
264                            model.geometry().pcurve(*forward),
265                            model.geometry().pcurve(*reversed),
266                        ) else {
267                            ogeom_bail!(Dangling, "seam pcurve is not in this model");
268                        };
269                        (f.clone(), *range, Some((r.clone(), *range)))
270                    }
271                    _ => ogeom_bail!(
272                        Construction,
273                        "an edge with no pcurve on its face cannot be split in \
274                         that face's parameter space"
275                    ),
276                };
277            let ends_tolerance = model
278                .children_of(&edge)?
279                .iter()
280                .filter_map(|v| model.node(v).and_then(|n| n.data().as_vertex()))
281                .fold(0.0_f64, |acc, d| acc.max(d.tolerance.get()));
282            // Each end's own vertex, matched to the curve's ends by position:
283            // the edge's vertex order and its curve's direction need not
284            // agree once the edge is reversed in its wire.
285            let ends = {
286                let (a, b) = (world.point_at(range.0, tol)?, world.point_at(range.1, tol)?);
287                let mut ends = [(a, tol.confusion()), (b, tol.confusion())];
288                for v in model.children_of(&edge)? {
289                    if let Some(d) = model.node(&v).and_then(|n| n.data().as_vertex()) {
290                        let at = v.transform(model.datums())?.apply(d.point);
291                        let k = usize::from(at.distance(b) < at.distance(a));
292                        ends[k].1 = ends[k].1.max(d.tolerance.get());
293                    }
294                }
295                ends
296            };
297            edges.push(BoundaryEdge {
298                node: EdgeKey::of(&edge),
299                curve: world,
300                crange: *range,
301                pcurve,
302                prange,
303                other_side,
304                tolerance: edge_data.tolerance.get(),
305                ends_tolerance,
306                ends,
307                bound: ogeom_math::Aabb::EMPTY,
308            });
309        }
310        if edges.is_empty() {
311            ogeom_bail!(Construction, "a face with no boundary bounds nothing");
312        }
313
314        // A seam edge bounds the chart twice *only when this face wraps*:
315        // when its other boundary reaches both columns, as a full drum's
316        // rims do. A face that merely sits against the meridian (a sphere
317        // octant whose boundary happens to be the seam) uses one column,
318        // and feeding the far copy into the arrangement leaves a strand
319        // nothing connects to. The decision is made here, once, by chart
320        // connectivity, and everything downstream (the arrangement, the
321        // trim tests, the rebuild) inherits it.
322        let seam_indices: Vec<usize> = edges
323            .iter()
324            .enumerate()
325            .filter(|(_, e)| e.other_side.is_some())
326            .map(|(i, _)| i)
327            .collect();
328        for i in seam_indices {
329            let mut pool: Vec<Point2> = Vec::new();
330            for (j, e) in edges.iter().enumerate() {
331                if j == i {
332                    continue;
333                }
334                for t in [e.prange.0, e.prange.1] {
335                    pool.push(e.pcurve.point_at(t, tol)?);
336                }
337                if let Some((other, orange)) = &e.other_side {
338                    for t in [orange.0, orange.1] {
339                        pool.push(other.point_at(t, tol)?);
340                    }
341                }
342            }
343            for p in &poles {
344                for t in [p.prange.0, p.prange.1] {
345                    pool.push(p.pcurve.point_at(t, tol)?);
346                }
347            }
348            let connected = |pc: &PlanarCurve, range: (f64, f64)| -> OgeomResult<bool> {
349                for t in [range.0, range.1] {
350                    let at = pc.point_at(t, tol)?;
351                    if !pool.iter().any(|q| q.distance(at) <= PARAM_SNAP) {
352                        return Ok(false);
353                    }
354                }
355                Ok(true)
356            };
357            let e = &edges[i];
358            let primary_connects = connected(&e.pcurve, e.prange)?;
359            let (other_pc, orange) = e
360                .other_side
361                .clone()
362                .unwrap_or_else(|| (e.pcurve.clone(), e.prange));
363            let other_connects = connected(&other_pc, orange)?;
364            match (primary_connects, other_connects) {
365                // Both columns meet the rest of the boundary: the face
366                // wraps, and both belong.
367                (true, true) => {}
368                // One column is this face's; the far copy would dangle.
369                (true, false) => edges[i].other_side = None,
370                (false, true) => {
371                    let e = &mut edges[i];
372                    e.pcurve = other_pc;
373                    e.prange = orange;
374                    e.other_side = None;
375                }
376                // Neither connects: leave both, and the arrangement's own
377                // refusal names the failure as it always did.
378                (false, false) => {}
379            }
380        }
381        let mut bound = ogeom_math::Aabb::EMPTY;
382        // For a ruled surface the box will be trusted to the boundary's own
383        // hull, so the boundary's sampling slack must be measured: how far
384        // the true edge sags from the 16-chord polyline, read at each
385        // chord's midpoint and doubled for the sag's asymmetry. Nothing else
386        // uses the measurement, and the extra evaluations are priced on
387        // spline edges, so nothing else pays for it: an unruled face keeps
388        // the exact box it always had, and the exact admit set with it.
389        let ruled = matches!(
390            &surface,
391            SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_)
392        );
393        let mut slack = 0.0_f64;
394        for e in &mut edges {
395            let mut previous: Option<Point> = None;
396            let (mut own, mut sag) = (ogeom_math::Aabb::EMPTY, 0.0_f64);
397            for i in 0..=16 {
398                #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
399                let t = e.crange.0 + (e.crange.1 - e.crange.0) * f64::from(i) / 16.0;
400                let p = e.curve.point_at(t, tol)?;
401                if let Some(q) = previous {
402                    let step = (e.crange.1 - e.crange.0) / 32.0;
403                    let mid = e.curve.point_at(t - step, tol)?;
404                    let off = mid.distance(Point::midpoint(q, p)) * 2.0;
405                    sag = sag.max(off);
406                    own = own.with_point(mid);
407                    if ruled {
408                        slack = slack.max(off);
409                        bound = bound.with_point(mid);
410                    }
411                }
412                own = own.with_point(p);
413                bound = bound.with_point(p);
414                previous = Some(p);
415            }
416            e.bound = own.expanded(sag + e.tolerance + tol.confusion() * 1e2);
417        }
418        // A plane never bulges past its boundary. A ruled surface (cylinder,
419        // cone) cannot either: every surface point lies on a straight ruling
420        // whose ends are on the boundary, so the face sits inside its
421        // boundary's hull and only the boundary's own sampling slack is owed.
422        // Anything else may genuinely bulge (a dome past its equator) and
423        // keeps most of its own diagonal as allowance. The audit behind
424        // OGEOM_BOOL_AUDIT_BOUNDS holds every arm to conservatism.
425        let bulge = match &surface {
426            SurfaceGeometry::Plane(_) => 0.0,
427            SurfaceGeometry::Cylinder(_) | SurfaceGeometry::Cone(_) => slack,
428            _ => bound.diagonal() * 0.75,
429        };
430        // The scale the marching chord is derived from, decoupled from the
431        // filter box. It reproduces exactly what the heuristic was tuned
432        // against (the diagonal as the blanket three-quarter bulge left it)
433        // because the chord is a tolerance, not a bound: tightening the
434        // filter must not silently tighten the marcher, which is exactly
435        // what a single box feeding both does.
436        let margin = tol.confusion() * 1e2;
437        let chord_scale = match &surface {
438            SurfaceGeometry::Plane(_) => bound.expanded(margin).diagonal(),
439            _ => bound.expanded(bound.diagonal() * 0.75 + margin).diagonal(),
440        };
441        // A pole bounds the chart with no edge to sample: a cone drilled to
442        // its apex reaches the apex, and a bound that omits it would let the
443        // filter drop a pair the apex genuinely meets. It joins *after* the
444        // bulge is taken from the boundary's own diagonal: the pole is an
445        // exact point, and letting it stretch the diagonal would inflate a
446        // dome's allowance by its own height over again.
447        let mut bound = bound.expanded(bulge);
448        for pole in &poles {
449            bound = bound.with_point(pole.point);
450        }
451        let bound = bound.expanded(tol.confusion() * 1e2);
452        faces.push(GFace {
453            poles,
454            face,
455            surface,
456            bound,
457            chord_scale,
458            edges,
459        });
460    }
461    if faces.is_empty() {
462        ogeom_bail!(Construction, "a solid with no faces bounds nothing");
463    }
464    Ok(GSolid {
465        solid: solid.clone(),
466        faces,
467    })
468}
469
470/// A parameter brought onto the turn its edge actually covers.
471///
472/// A periodic curve's own domain and an edge's range over it need not agree
473/// on which turn to count from: a sphere's seam runs its half meridian over
474/// `[-π/2, π/2]`, while every crossing found on it comes back in `[0, 2π)`.
475/// Left alone, a crossing at latitude `-0.96` arrives as `5.32`, sits outside
476/// the range, and is discarded, so the seam never splits where a section
477/// genuinely meets it, and the arrangement finds the chain hanging.
478fn onto_range(t: f64, curve: &Curve, range: (f64, f64), tol: Tolerances) -> f64 {
479    if !curve.is_periodic() {
480        return t;
481    }
482    let (a, b) = curve.domain();
483    let period = b - a;
484    if period <= 0.0 {
485        return t;
486    }
487    for k in [0.0, -1.0, 1.0, -2.0, 2.0] {
488        let shifted = period.mul_add(k, t);
489        if shifted >= range.0 - tol.parametric() && shifted <= range.1 + tol.parametric() {
490            return shifted;
491        }
492    }
493    t
494}
495
496/// The part of an overlap that falls within the second curve's own bounded
497/// range, stated in the *first* curve's parameter.
498///
499/// An overlap is between two curves; an edge covers only part of its curve.
500/// The correspondence the overlap states is affine, so the edge's range
501/// carries across as an interval, and on a periodic curve it carries across
502/// up to whole turns, so the shift that meets the overlap is the one meant.
503/// `None` where the edge's own stretch and the overlap do not meet.
504fn overlap_within(
505    overlap: &ogeom_intersect::Overlap,
506    range: (f64, f64),
507    curve: &Curve,
508    tol: Tolerances,
509) -> Option<(f64, f64)> {
510    let ordered = |r: (f64, f64)| if r.0 <= r.1 { r } else { (r.1, r.0) };
511    let span_a = overlap.on_a.1 - overlap.on_a.0;
512    let span_b = overlap.on_b.1 - overlap.on_b.0;
513    if span_b.abs() <= f64::MIN_POSITIVE {
514        return None;
515    }
516    let to_a = |t: f64| overlap.on_a.0 + span_a * (t - overlap.on_b.0) / span_b;
517    let (wlo, whi) = ordered((to_a(range.0), to_a(range.1)));
518    let (lo, hi) = ordered(overlap.on_a);
519    let period = if curve.is_periodic() {
520        let (a, b) = curve.domain();
521        // The turn measured in the *first* curve's parameter, which is where
522        // the intersection is being taken.
523        (b - a) * (span_a / span_b).abs()
524    } else {
525        0.0
526    };
527    let mut best: Option<(f64, f64)> = None;
528    for k in [0.0, 1.0, -1.0, 2.0, -2.0] {
529        let candidate = (
530            lo.max(period.mul_add(k, wlo)),
531            hi.min(period.mul_add(k, whi)),
532        );
533        if candidate.1 - candidate.0 > best.map_or(tol.parametric(), |(x, y)| y - x) {
534            best = Some(candidate);
535        }
536        if period == 0.0 {
537            break;
538        }
539    }
540    best
541}
542
543/// A parameter carried proportionally from one range to another.
544fn rescale(t: f64, from: (f64, f64), to: (f64, f64)) -> f64 {
545    let span = from.1 - from.0;
546    if span.abs() <= f64::MIN_POSITIVE {
547        return to.0;
548    }
549    to.0 + (to.1 - to.0) * (t - from.0) / span
550}
551
552/// The pcurve's course over a sub-range of the *curve's* parameters.
553/// The stretches of `curve` over `crange` that lie along the edge `e`
554/// within the pair's honesty, as overlaps with the edge's own parameters at
555/// their ends: the measured twin of the intersector's closed-form
556/// coincidence, for a fitted curve on an exact one.
557fn measured_overlaps(
558    curve: &Curve,
559    crange: (f64, f64),
560    tolerance: f64,
561    e: &BoundaryEdge,
562    tol: Tolerances,
563) -> OgeomResult<Vec<ogeom_intersect::Overlap>> {
564    const SAMPLES: usize = 48;
565    let width = (tolerance.max(e.tolerance) * 2.0).max(tol.confusion() * 1e3);
566    let mut near: Vec<bool> = Vec::with_capacity(SAMPLES + 1);
567    for i in 0..=SAMPLES {
568        #[allow(clippy::cast_precision_loss)]
569        let t = crange.0 + (crange.1 - crange.0) * (i as f64) / (SAMPLES as f64);
570        let p = curve.point_at(t, tol)?;
571        near.push(distance_to_edge_curve(&e.curve, e.crange, p, tol)? <= width);
572    }
573    let mut out = Vec::new();
574    let mut i = 0;
575    while i <= SAMPLES {
576        if !near[i] {
577            i += 1;
578            continue;
579        }
580        let start = i;
581        while i < SAMPLES && near[i + 1] {
582            i += 1;
583        }
584        let end = i;
585        i += 1;
586        // Three samples along is a stretch; fewer is a crossing's blur.
587        if end - start < 2 {
588            continue;
589        }
590        #[allow(clippy::cast_precision_loss)]
591        let at = |k: usize| crange.0 + (crange.1 - crange.0) * (k as f64) / (SAMPLES as f64);
592        // Each end refined by bisection into the neighbouring sample gap.
593        let refine = |inside: f64, outside: f64| -> OgeomResult<f64> {
594            let (mut a, mut b) = (inside, outside);
595            for _ in 0..24 {
596                let m = f64::midpoint(a, b);
597                let p = curve.point_at(m, tol)?;
598                if distance_to_edge_curve(&e.curve, e.crange, p, tol)? <= width {
599                    a = m;
600                } else {
601                    b = m;
602                }
603            }
604            Ok(a)
605        };
606        let lo = if start == 0 {
607            at(0)
608        } else {
609            refine(at(start), at(start - 1))?
610        };
611        let hi = if end == SAMPLES {
612            at(SAMPLES)
613        } else {
614            refine(at(end), at(end + 1))?
615        };
616        if hi - lo <= tol.parametric() {
617            continue;
618        }
619        let on_edge = |t: f64| -> OgeomResult<f64> {
620            let p = curve.point_at(t, tol)?;
621            let foot = ogeom_algo::project_on_curve(&e.curve, p, 64, tol)?;
622            Ok(onto_range(foot.parameter, &e.curve, e.crange, tol))
623        };
624        if *DEBUG_WIRE {
625            eprintln!(
626                "CONTACT along edge {} measured over ({lo:.6}, {hi:.6}) within {width:.2e}",
627                e.node.index()
628            );
629        }
630        out.push(ogeom_intersect::Overlap {
631            on_a: (lo, hi),
632            on_b: (on_edge(lo)?, on_edge(hi)?),
633        });
634    }
635    Ok(out)
636}
637
638fn pcurve_polyline(
639    pcurve: &PlanarCurve,
640    prange: (f64, f64),
641    crange: (f64, f64),
642    sub: (f64, f64),
643    surface: &SurfaceGeometry,
644    tol: Tolerances,
645) -> OgeomResult<Vec<Point2>> {
646    let lo = rescale(sub.0, crange, prange);
647    let hi = rescale(sub.1, crange, prange);
648    // Enough samples that the first step approximates the tangent and the
649    // scanline interior test has a faithful outline. Straight pcurves get
650    // two points; everything else a fixed fine sampling.
651    let mut count = match pcurve {
652        PlanarCurve::Line(_) => 1,
653        _ => {
654            let span = (hi - lo).abs().max(SCAFFOLD_CHORD);
655            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
656            let n = (span / SCAFFOLD_CHORD).sqrt().ceil() as usize;
657            n.clamp(8, 256)
658        }
659    };
660    // And never a step [`unwrap_polyline`] could read as a period jump. A
661    // straight pcurve is faithful at two points (a coaxial rim's image on
662    // a cylinder is exactly a line across the chart), but one that crosses
663    // more than half the turn looks to that reader like a wrap, and the
664    // strand comes back running the complementary way: a bore's mouth
665    // blended where the mouth circle starts anywhere but the wall's own
666    // seam. The chart's span decides the count, not the parameter's.
667    let ((ua, ub), (va, vb)) = surface.domain();
668    let (first, last) = (pcurve.point_at(lo, tol)?, pcurve.point_at(hi, tol)?);
669    for (periodic, period, reach) in [
670        (surface.is_periodic_u(), ub - ua, (last.x - first.x).abs()),
671        (surface.is_periodic_v(), vb - va, (last.y - first.y).abs()),
672    ] {
673        if !periodic || period <= 0.0 || !reach.is_finite() {
674            continue;
675        }
676        let steps = (reach / (period / 3.0)).ceil();
677        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
678        let steps = steps.clamp(1.0, 4096.0) as usize;
679        count = count.max(steps);
680    }
681    let mut out = Vec::with_capacity(count + 1);
682    for i in 0..=count {
683        #[allow(clippy::cast_precision_loss)]
684        let t = lo + (hi - lo) * i as f64 / count as f64;
685        out.push(pcurve.point_at(t, tol)?);
686    }
687    Ok(out)
688}
689
690// --- the filler --------------------------------------------------------------
691
692/// One section curve between a face of `a` and a face of `b`.
693struct SectionRec {
694    curve: Curve,
695    /// The pcurve on the A face's surface, sharing the curve's parameter.
696    pc_a: PlanarCurve,
697    /// The same on the B face.
698    pc_b: PlanarCurve,
699    face_a: usize,
700    face_b: usize,
701    closed: bool,
702    /// How far the curve may sit from the true intersection: zero for an
703    /// exact section, the trace-plus-fit budget for a marched one. Crossing
704    /// filters widen their acceptance by this, or a fitted section would
705    /// never register against the edges it genuinely meets.
706    tolerance: f64,
707}
708
709/// The parameter intervals of a contact curve over which *both* faces
710/// actually reach it.
711///
712/// Two surfaces touch along the whole of their contact; two faces touch
713/// along whatever part of it their trims both hold. That part is found by
714/// sampling (sixty-four stations along the curve, each asked of both
715/// charts) rather than by intersecting the contact with the boundary
716/// edges, because a contact meets those boundaries tangentially too and the
717/// crossing finder is the wrong instrument for it. The cost of sampling is
718/// the usual one: a stretch shorter than a station can be missed, and an
719/// endpoint is placed within a station of the truth.
720fn contact_intervals(
721    fused: &GeneralFused,
722    contact: &TangentRec,
723    tol: Tolerances,
724) -> OgeomResult<Vec<(f64, f64)>> {
725    let outline = |face: &GFace| -> OgeomResult<Vec<Vec<Point2>>> {
726        let mut lines = Vec::new();
727        for e in &face.edges {
728            lines.push(pcurve_polyline(
729                &e.pcurve,
730                e.prange,
731                e.crange,
732                e.crange,
733                &face.surface,
734                tol,
735            )?);
736        }
737        Ok(lines)
738    };
739    let rings_a = outline(&fused.a.faces[contact.face_a])?;
740    let rings_b = outline(&fused.b.faces[contact.face_b])?;
741    let refs_a: Vec<&[Point2]> = rings_a.iter().map(Vec::as_slice).collect();
742    let refs_b: Vec<&[Point2]> = rings_b.iter().map(Vec::as_slice).collect();
743
744    const STATIONS: usize = 64;
745    let domain = contact.curve.domain();
746    let span = domain.1 - domain.0;
747    let mut runs: Vec<(f64, f64)> = Vec::new();
748    let mut open: Option<f64> = None;
749    for k in 0..=STATIONS {
750        #[expect(
751            clippy::cast_precision_loss,
752            reason = "a station index, far below the mantissa"
753        )]
754        let t = span.mul_add(k as f64 / STATIONS as f64, domain.0);
755        // A periodic chart's parameters run out past its own window; the
756        // trim test is only meaningful once they are folded back into it.
757        let held = matches!(
758            (
759                contact.pc_a.point_at(t, tol),
760                contact.pc_b.point_at(t, tol)
761            ),
762            (Ok(pa), Ok(pb))
763                if arrange::inside_many(
764                    &refs_a,
765                    fold_point_into_chart(pa, &fused.a.faces[contact.face_a].surface),
766                ) && arrange::inside_many(
767                    &refs_b,
768                    fold_point_into_chart(pb, &fused.b.faces[contact.face_b].surface),
769                )
770        );
771        match (held, open) {
772            (true, None) => open = Some(t),
773            (false, Some(from)) => {
774                if t - from > tol.parametric() {
775                    runs.push((from, t));
776                }
777                open = None;
778            }
779            _ => {}
780        }
781    }
782    if let Some(from) = open
783        && domain.1 - from > tol.parametric()
784    {
785        runs.push((from, domain.1));
786    }
787    Ok(runs)
788}
789
790/// One curve along which two faces *touch* without crossing.
791///
792/// A contact carries no boundary parity (neither face passes through the
793/// other), so it takes no part in the arrangement or the classification.
794/// It is still a curve that exists on the result, and a section view
795/// through a tangency has to show it, so it is carried alongside.
796struct TangentRec {
797    curve: Curve,
798    pc_a: PlanarCurve,
799    pc_b: PlanarCurve,
800    face_a: usize,
801    face_b: usize,
802}
803
804/// One kept sub-range of one section.
805/// A strand's tolerance as a junction may trust it: a fitted section whose
806/// trace failed reports a budget of metres, and a weld that believed it
807/// would join every vertex of the model. Nothing this pipeline fits is
808/// honestly looser than ten thousand confusions.
809fn honest(tolerance: f64, tol: Tolerances) -> f64 {
810    tolerance.min(tol.confusion() * 1e4)
811}
812
813/// One split an edge is asked for, with how honestly it can be placed.
814#[derive(Debug, Clone, Copy)]
815struct Pave {
816    /// The parameter on the edge's curve.
817    t: f64,
818    /// How far the strand that asked for it may honestly sit from the exact
819    /// junction: its own tolerance, and its tangential doubt where the
820    /// crossing was a touch.
821    honesty: f64,
822}
823
824/// Paves the edge cannot tell apart, as one junction each.
825///
826/// A tolerant rail meeting a wedge's faces near one corner collects a
827/// cluster of crossings inside its own stated radius, and a fitted section
828/// and a fitted contact asking for the same corner land a few microns
829/// apart. Split at each, the edge shatters into dust no weld downstream can
830/// rejoin. So consecutive paves whose gap along the edge in space is within
831/// the edge's honesty or either pave's own are one cluster, its first pave
832/// speaking for it.
833#[derive(Debug, Clone, Copy)]
834struct PaveCluster {
835    /// The representative parameter: the cluster's first pave.
836    t: f64,
837    /// Where it sits.
838    at: Point,
839    /// How far the cluster's members reach from the representative.
840    span: f64,
841    /// The loosest honesty among the members.
842    honesty: f64,
843    /// How many paves the cluster holds.
844    members: usize,
845}
846
847fn cluster_paves(
848    curve: &Curve,
849    crange: (f64, f64),
850    edge_tolerance: f64,
851    paves: &[Pave],
852    tol: Tolerances,
853) -> OgeomResult<Vec<PaveCluster>> {
854    let mut ts: Vec<Pave> = paves
855        .iter()
856        .copied()
857        .filter(|p| p.t > crange.0 + tol.parametric() && p.t < crange.1 - tol.parametric())
858        .collect();
859    ts.sort_by(|x, y| x.t.partial_cmp(&y.t).unwrap_or(core::cmp::Ordering::Equal));
860    ts.dedup_by(|x, y| {
861        if (x.t - y.t).abs() <= tol.parametric() {
862            y.honesty = y.honesty.max(x.honesty);
863            true
864        } else {
865            false
866        }
867    });
868    let floor = edge_tolerance.max(tol.confusion() * 10.0);
869    // A pave within the edge's honesty of its own end *is* the end: split
870    // there, the sliver between them is dust one face keeps and the
871    // face across the edge drops, and the sew finds it used once.
872    let ends = [
873        curve.point_at(crange.0, tol)?,
874        curve.point_at(crange.1, tol)?,
875    ];
876    let mut clusters: Vec<PaveCluster> = Vec::new();
877    let mut prev: Option<(Point, f64)> = None;
878    for pave in ts {
879        let at = curve.point_at(pave.t, tol)?;
880        if ends
881            .iter()
882            .any(|e| e.distance(at) <= floor.max(pave.honesty))
883        {
884            continue;
885        }
886        let joined = prev.is_some_and(|(held, honesty): (Point, f64)| {
887            held.distance(at) <= floor.max(honesty).max(pave.honesty)
888        });
889        if joined && let Some(cluster) = clusters.last_mut() {
890            cluster.span = cluster.span.max(cluster.at.distance(at));
891            cluster.honesty = cluster.honesty.max(pave.honesty);
892            cluster.members += 1;
893        } else {
894            clusters.push(PaveCluster {
895                t: pave.t,
896                at,
897                span: 0.0,
898                honesty: pave.honesty,
899                members: 1,
900            });
901        }
902        prev = Some((at, pave.honesty));
903    }
904    Ok(clusters)
905}
906
907#[derive(Clone)]
908struct SectionPiece {
909    section: usize,
910    range: (f64, f64),
911    /// Whether this sub-range already *is* boundary on the A face, the B
912    /// face, or neither. A piece that is boundary on one side is still the
913    /// splitting curve on the other, and only the face it duplicates leaves
914    /// it out.
915    hugs: [bool; 2],
916    /// Set when the piece was admitted by a hug: a split of the other face
917    /// along this edge of the hugging face: (edge node, target from A,
918    /// target face). Two sections hugging one edge onto one face are the
919    /// same split, kept once.
920    hug_key: Option<(EdgeKey, bool, usize)>,
921}
922
923/// One boundary edge of one argument's face, lying in a face of the other
924/// argument's own surface: the splitting curve same-domain contact
925/// contributes, since coincident surfaces have no section curve to offer.
926struct ContactRec {
927    /// The owner's world curve and the sub-range its edge covers.
928    curve: Curve,
929    crange: (f64, f64),
930    /// The curve spoken in the *target* face's chart, over its own window,
931    /// mapped proportionally from `crange` exactly as an edge's own stored
932    /// pcurve is.
933    pcurve: PlanarCurve,
934    prange: (f64, f64),
935    /// The owner edge's node, whose paves this record shares.
936    node: EdgeKey,
937    /// The owner edge's own tolerance: how far its curve may honestly sit
938    /// from the exact geometry it meets, which is how far a crossing filter
939    /// must reach to see a fitted rail cross an exact seam.
940    tolerance: f64,
941    /// The target: which argument's face list, and which face.
942    target_from_a: bool,
943    target_face: usize,
944}
945
946/// A same-domain contact edge's image in the shared surface's chart, where
947/// no closed form exists: fitted by projection into the target's chart, or
948/// (where the edge overhangs the target's own window, a blend's leg on a
949/// spline host continued past the face) into the owner's, which is the
950/// same chart wherever an extension kept its parent's parameters. The
951/// owner's image is trusted only where lifting it through the *target*
952/// lands back on the edge, sampled over the run the target's window holds.
953///
954/// `None` for an edge lying wholly outside the target's window, which the
955/// target face cannot meet.
956///
957/// # Errors
958///
959/// [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) if neither
960/// chart holds the edge.
961fn projected_into_shared_chart(
962    curve: &Curve,
963    range: (f64, f64),
964    owner: &SurfaceGeometry,
965    target: &SurfaceGeometry,
966    tol: Tolerances,
967) -> OgeomResult<Option<PlanarCurve>> {
968    let against_target = ogeom_algo::pcurve_fit::fit_projected_pcurve(curve, range, target, tol);
969    if let Ok(fitted) = &against_target
970        && fitted.2
971    {
972        return Ok(Some(fitted.0.clone()));
973    }
974    let Ok(on_owner) = ogeom_algo::pcurve_fit::fit_projected_pcurve(curve, range, owner, tol)
975    else {
976        ogeom_bail!(
977            NotDone,
978            "same-domain contact whose edge could not be projected into the \
979             shared surface's chart: {}",
980            against_target
981                .err()
982                .map_or_else(String::new, |e| e.to_string())
983        );
984    };
985    let ((ua, ub), (va, vb)) = target.domain();
986    let mut checked = 0usize;
987    for i in 0..=16 {
988        let t = range.0 + (range.1 - range.0) * f64::from(i) / 16.0;
989        let uv = on_owner.0.point_at(t, tol)?;
990        if uv.x < ua || uv.x > ub || uv.y < va || uv.y > vb {
991            continue;
992        }
993        let lifted = target.point_at(uv.x, uv.y, tol)?;
994        if lifted.distance(curve.point_at(t, tol)?) > tol.confusion() * 1e4 {
995            ogeom_bail!(
996                NotDone,
997                "same-domain contact whose edge overhangs the shared surface's \
998                 window, on a chart the two surfaces do not share"
999            );
1000        }
1001        checked += 1;
1002    }
1003    if checked == 0 {
1004        return Ok(None);
1005    }
1006    Ok(Some(on_owner.0))
1007}
1008
1009/// Whether two surfaces are the *identical chart*: the same
1010/// parameterization, frame and all, not merely the same point set.
1011///
1012/// The five analytics only: a fitted surface never qualifies, because two
1013/// independent fits of one geometry agree nowhere in parameter space. This
1014/// is what lets a stored pcurve stand in for a closed-form projection in the
1015/// same-domain melt: on the identical chart, the owner's pcurve already is
1016/// the projection.
1017fn same_chart(a: &SurfaceGeometry, b: &SurfaceGeometry, tol: Tolerances) -> bool {
1018    use SurfaceGeometry as S;
1019    let frames = |fa: ogeom_math::Frame, fb: ogeom_math::Frame| -> bool {
1020        fa.origin().distance(fb.origin()) <= tol.confusion()
1021            && fa.z().vector().dot(fb.z().vector()) >= 1.0 - tol.angular()
1022            && fa.x().vector().dot(fb.x().vector()) >= 1.0 - tol.angular()
1023    };
1024    match (a, b) {
1025        (S::Plane(x), S::Plane(y)) => frames(x.plane().frame(), y.plane().frame()),
1026        (S::Cylinder(x), S::Cylinder(y)) => {
1027            frames(x.cylinder().frame(), y.cylinder().frame())
1028                && (x.cylinder().radius() - y.cylinder().radius()).abs() <= tol.confusion()
1029        }
1030        (S::Cone(x), S::Cone(y)) => {
1031            frames(x.cone().frame(), y.cone().frame())
1032                && (x.cone().reference_radius() - y.cone().reference_radius()).abs()
1033                    <= tol.confusion()
1034                && (x.cone().half_angle() - y.cone().half_angle()).abs() <= tol.angular()
1035        }
1036        (S::Sphere(x), S::Sphere(y)) => {
1037            frames(x.sphere().frame(), y.sphere().frame())
1038                && (x.sphere().radius() - y.sphere().radius()).abs() <= tol.confusion()
1039        }
1040        (S::Torus(x), S::Torus(y)) => {
1041            frames(x.torus().frame(), y.torus().frame())
1042                && (x.torus().major_radius() - y.torus().major_radius()).abs() <= tol.confusion()
1043                && (x.torus().minor_radius() - y.torus().minor_radius()).abs() <= tol.confusion()
1044        }
1045        // Two patches are one chart only as one patch: the same knots and
1046        // the same net, to the bit: a blend's leg built on the host's own
1047        // patch, widened in place and shared. Any other pair of patches
1048        // that merely coincides is not.
1049        (S::BSpline(x), S::BSpline(y)) => x == y,
1050        _ => false,
1051    }
1052}
1053
1054/// What a face's arrangement strand stands for.
1055#[derive(Clone)]
1056enum Tag {
1057    /// A sub-range of boundary edge `edge` (index into the face's edges).
1058    Boundary { edge: usize, range: (f64, f64) },
1059    /// A sub-range of a global section.
1060    Section { section: usize, range: (f64, f64) },
1061    /// A sub-range of a global contact edge: another face's boundary edge
1062    /// lying in this face's own surface, splitting it.
1063    Contact { contact: usize, range: (f64, f64) },
1064    /// A sub-range of a pole of the face's own chart. The edge is a point in
1065    /// space whatever the range says; the range is where it runs in the
1066    /// chart, which is the only place it has length.
1067    Pole { pole: usize, range: (f64, f64) },
1068}
1069
1070/// Where a piece stands relative to the other solid.
1071///
1072/// `In` and `Out` are the classifier's words. The two `On` states split the
1073/// case the classifier cannot decide alone: a piece lying on the other
1074/// boundary bounds material on one side here and one side there, and whether
1075/// those sides agree (outward normals aligned) or oppose is what every
1076/// operation's filter turns on.
1077#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1078enum PieceState {
1079    In,
1080    Out,
1081    /// On the other boundary, outward normals aligned: both solids' material
1082    /// on the same side. One copy of the piece bounds the union and the
1083    /// intersection alike.
1084    OnAligned,
1085    /// On the other boundary, outward normals opposed: material on both
1086    /// sides. The contact is interior to the union and vanishes from it.
1087    OnOpposed,
1088}
1089
1090/// Fold a param-space polyline into a periodic surface's chart, by one
1091/// constant offset per axis.
1092///
1093/// A section's pcurve is *unwrapped* across a seam: continuous, and allowed
1094/// to leave the stated domain, because that is what crossing a seam is. The
1095/// arrangement lives in one chart, and the filler has already split every
1096/// section at its seam crossings, so each strand spans at most one chart
1097/// width and a single period shift per axis brings it home. The shift is
1098/// chosen by the strand's midpoint, so endpoints sitting exactly on the
1099/// chart's edge stay on whichever side the strand's body is.
1100/// A point in a polyline's *interior*: its half-way member, except that a
1101/// straight strand is two points and its half-way member is an endpoint,
1102/// which may sit exactly on a seam or a trim. The chord midpoint is on the
1103/// curve for a straight image and strictly inside either way.
1104fn interior_of(line: &[Point2]) -> Point2 {
1105    if line.len() == 2 {
1106        Point2::new(
1107            f64::midpoint(line[0].x, line[1].x),
1108            f64::midpoint(line[0].y, line[1].y),
1109        )
1110    } else {
1111        line[line.len() / 2]
1112    }
1113}
1114
1115fn fold_into_chart(line: &mut [Point2], surface: &SurfaceGeometry) {
1116    let ((ua, ub), (va, vb)) = surface.domain();
1117    if line.is_empty() {
1118        return;
1119    }
1120    let mid = interior_of(line);
1121    if surface.is_periodic_u() {
1122        let span = ub - ua;
1123        if span > 0.0 {
1124            let shift = (ua + (mid.x - ua).rem_euclid(span)) - mid.x;
1125            for p in line.iter_mut() {
1126                p.x += shift;
1127            }
1128        }
1129    }
1130    if surface.is_periodic_v() {
1131        let span = vb - va;
1132        if span > 0.0 {
1133            let shift = (va + (mid.y - va).rem_euclid(span)) - mid.y;
1134            for p in line.iter_mut() {
1135                p.y += shift;
1136            }
1137        }
1138    }
1139}
1140
1141/// The shifts by whole periods worth trying on a chart point after its fold,
1142/// nearest first: none, then one period along each periodic direction.
1143fn period_shifts(surface: &SurfaceGeometry) -> Vec<(f64, f64)> {
1144    let ((ua, ub), (va, vb)) = surface.domain();
1145    let mut out = vec![(0.0, 0.0)];
1146    if surface.is_periodic_u() && ub > ua {
1147        out.push((ub - ua, 0.0));
1148        out.push((ua - ub, 0.0));
1149    }
1150    if surface.is_periodic_v() && vb > va {
1151        out.push((0.0, vb - va));
1152        out.push((0.0, va - vb));
1153    }
1154    out
1155}
1156
1157/// A chart point folded to the side of the face's seam its trim is on.
1158///
1159/// The surface's fold puts a point in the period starting at the surface's
1160/// own zero, which is where a face's trim is only when its seam runs there.
1161/// A patch whose seam was cut elsewhere (a band opened along the widest gap
1162/// its mesh left, running diagonally round) covers a period starting at the
1163/// seam, and a point just short of it belongs a period on. The fold is kept
1164/// where it lands inside the trim, or where no shift does.
1165fn fold_inside(p: Point2, surface: &SurfaceGeometry, trim: &[&[Point2]]) -> Point2 {
1166    let folded = fold_point_into_chart(p, surface);
1167    if trim.is_empty() {
1168        return folded;
1169    }
1170    period_shifts(surface)
1171        .into_iter()
1172        .map(|(du, dv)| Point2::new(folded.x + du, folded.y + dv))
1173        .find(|q| inside_many(trim, *q))
1174        .unwrap_or(folded)
1175}
1176
1177/// A strand folded into the chart as a whole, to the side of the face's
1178/// seam its interior lies inside the trim on, as [`fold_inside`] does for
1179/// a point.
1180fn fold_line_inside(line: &mut [Point2], surface: &SurfaceGeometry, trim: &[&[Point2]]) {
1181    fold_into_chart(line, surface);
1182    if line.is_empty() || trim.is_empty() {
1183        return;
1184    }
1185    let mid = interior_of(line);
1186    if let Some((du, dv)) = period_shifts(surface)
1187        .into_iter()
1188        .find(|(du, dv)| inside_many(trim, Point2::new(mid.x + du, mid.y + dv)))
1189    {
1190        for p in line.iter_mut() {
1191            p.x += du;
1192            p.y += dv;
1193        }
1194    }
1195}
1196
1197/// A face's boundary in its chart, each edge's image sampled, both sides of
1198/// a seam: enough to tell which side of the seam a point is on.
1199fn face_trim_lines(face: &GFace, tol: Tolerances) -> Vec<Vec<Point2>> {
1200    let sample = |pcurve: &PlanarCurve, range: (f64, f64)| -> Vec<Point2> {
1201        (0..=16)
1202            .filter_map(|k| {
1203                pcurve
1204                    .point_at(range.0 + (range.1 - range.0) * f64::from(k) / 16.0, tol)
1205                    .ok()
1206            })
1207            .collect()
1208    };
1209    let mut out = Vec::new();
1210    for e in &face.edges {
1211        out.push(sample(&e.pcurve, e.prange));
1212        if let Some((other, range)) = &e.other_side {
1213            out.push(sample(other, *range));
1214        }
1215    }
1216    out
1217}
1218
1219/// Remove period tears from a sampled polyline, axis by axis.
1220fn unwrap_polyline(line: &mut [Point2], surface: &SurfaceGeometry) {
1221    let ((ua, ub), (va, vb)) = surface.domain();
1222    let spans = (
1223        if surface.is_periodic_u() {
1224            ub - ua
1225        } else {
1226            0.0
1227        },
1228        if surface.is_periodic_v() {
1229            vb - va
1230        } else {
1231            0.0
1232        },
1233    );
1234    for i in 1..line.len() {
1235        if spans.0 > 0.0 {
1236            while line[i].x - line[i - 1].x > spans.0 * 0.5 {
1237                line[i].x -= spans.0;
1238            }
1239            while line[i].x - line[i - 1].x < -spans.0 * 0.5 {
1240                line[i].x += spans.0;
1241            }
1242        }
1243        if spans.1 > 0.0 {
1244            while line[i].y - line[i - 1].y > spans.1 * 0.5 {
1245                line[i].y -= spans.1;
1246            }
1247            while line[i].y - line[i - 1].y < -spans.1 * 0.5 {
1248                line[i].y += spans.1;
1249            }
1250        }
1251    }
1252}
1253
1254/// Fold a point into a periodic surface's chart.
1255fn fold_point_into_chart(p: Point2, surface: &SurfaceGeometry) -> Point2 {
1256    let mut one = [p];
1257    fold_into_chart(&mut one, surface);
1258    one[0]
1259}
1260
1261/// A sub-range of a closed curve, brought into its domain.
1262///
1263/// The filler split every wrap interval at the domain end, so a piece fits
1264/// within one period; the fold of its start may still land the end a hair
1265/// past the domain, which clamps.
1266fn folded_range(range: (f64, f64), domain: (f64, f64), closed: bool) -> (f64, f64) {
1267    if !closed {
1268        return range;
1269    }
1270    let f0 = fold(range.0, domain);
1271    let f1 = (f0 + (range.1 - range.0)).min(domain.1);
1272    (f0, f1)
1273}
1274
1275/// Fold a parameter into a closed curve's domain.
1276fn fold(t: f64, domain: (f64, f64)) -> f64 {
1277    let span = domain.1 - domain.0;
1278    if span <= 0.0 {
1279        return domain.0;
1280    }
1281    domain.0 + (t - domain.0).rem_euclid(span)
1282}
1283
1284/// The parameter to evaluate a section at: folded where the curve is closed
1285/// and its pieces may run past the domain end, left alone where it is not.
1286///
1287/// An open section's own end *is* the domain end, and folding it lands on the
1288/// domain start instead: the far end of the curve. On a plane's section
1289/// through a ball's poles, where the marcher hands back two open half circles
1290/// each running pole to pole, that is the difference between an edge bounded
1291/// by the pole it reaches and one bounded by the pole on the other side.
1292fn at_param(t: f64, domain: (f64, f64), closed: bool) -> f64 {
1293    if closed { fold(t, domain) } else { t }
1294}
1295
1296/// The sections between two gathered solids, with the paves they put on
1297/// boundary edges.
1298#[allow(clippy::type_complexity)]
1299fn fill(
1300    ga: &GSolid,
1301    gb: &GSolid,
1302    admit_all: bool,
1303    tol: Tolerances,
1304) -> OgeomResult<(
1305    Vec<SectionRec>,
1306    Vec<SectionPiece>,
1307    Vec<ContactRec>,
1308    Vec<TangentRec>,
1309    Vec<Vec<(f64, f64)>>,
1310    std::collections::HashMap<EdgeKey, Vec<Pave>>,
1311    Vec<Vec<usize>>,
1312    Vec<Vec<usize>>,
1313    Vec<Junction>,
1314)> {
1315    use ogeom_intersect::{
1316        CurveCurveOptions, IntersectOptions, SurfaceIntersection, intersect_curves,
1317        intersect_surfaces,
1318    };
1319    let mut sections: Vec<SectionRec> = Vec::new();
1320    let mut contacts: Vec<ContactRec> = Vec::new();
1321    let mut tangents: Vec<TangentRec> = Vec::new();
1322    let mut same_pairs: Vec<(usize, usize)> = Vec::new();
1323    // Marched sections are fitted; the fit is driven below the confusion
1324    // tolerance so a fitted curve meets edges, vertices and the mesh welder
1325    // on the same terms as an exact one. The budget each still carries is
1326    // recorded per section and widens the crossing filters.
1327    for (ia, fa) in ga.faces.iter().enumerate() {
1328        for (ib, fb) in gb.faces.iter().enumerate() {
1329            let admitted = fa.bound.intersects(&fb.bound);
1330            if !admitted && !admit_all {
1331                // The faces cannot meet, whatever their surfaces do.
1332                continue;
1333            }
1334            let scale = fa.chord_scale.min(fb.chord_scale);
1335            let chord = (scale * 1e-7).max(tol.confusion() * 0.5);
1336            let options = IntersectOptions {
1337                // The fit budget scales with the chord: a section against a
1338                // fitted surface cannot honestly land closer than the
1339                // geometry it cuts, and whatever it carries is stated on
1340                // the record and widens every filter downstream.
1341                tolerance: chord,
1342                marching: ogeom_intersect::Marching {
1343                    chord,
1344                    ..ogeom_intersect::Marching::default()
1345                },
1346            };
1347            // Coincidence is asked before the intersector is, but only where
1348            // the closed forms have already declined the pair. The marcher
1349            // documents that it is not the one to answer it (it seeds on
1350            // sign changes, and a pair that never separates has none), so
1351            // what it traces over a coincident pair is noise wearing a
1352            // section's name, and it costs seconds to produce.
1353            let met = if ogeom_intersect::surface_surface(&fa.surface, &fb.surface, tol).is_err()
1354                && surfaces_coincide(&fa.surface, &fb.surface, options.tolerance, tol)
1355            {
1356                SurfaceIntersection::Same
1357            } else {
1358                intersect_surfaces(&fa.surface, &fb.surface, options, tol)?
1359            };
1360            match met {
1361                SurfaceIntersection::Apart => {}
1362                SurfaceIntersection::Same => {
1363                    // Coincident surfaces offer no section curve; what splits
1364                    // each face is the *other* face's boundary. Exact pcurve
1365                    // projection carries an edge into the other chart, and
1366                    // planes always have one; a curved same-domain pair whose
1367                    // edges do not project in closed form is still refused.
1368                    same_pairs.push((ia, ib));
1369                    for (owner_from_a, owner, target_from_a, target, target_face) in
1370                        [(false, fb, true, fa, ia), (true, fa, false, fb, ib)]
1371                    {
1372                        let _ = owner_from_a;
1373                        for e in &owner.edges {
1374                            let (pcurve, prange) = match ogeom_intersect::exact_pcurve_of(
1375                                &e.curve,
1376                                &target.surface,
1377                                tol,
1378                            ) {
1379                                Some(exact) => (exact, e.crange),
1380                                // A fitted edge has no closed-form projection,
1381                                // but when the two faces sit on the
1382                                // *identical chart*, which is exactly the
1383                                // situation `Same` names for the analytics,
1384                                // the owner's own stored pcurve already is
1385                                // the projection, attached at construction,
1386                                // and it travels with its own window the way
1387                                // every stored pcurve does. A chart that
1388                                // merely coincides as a point set is still
1389                                // refused.
1390                                None if same_chart(&owner.surface, &target.surface, tol) => {
1391                                    (e.pcurve.clone(), e.prange)
1392                                }
1393                                // Two patches that coincide as point sets
1394                                // without being one chart (a blend's leg
1395                                // on a spline host continued past the
1396                                // face, against the face's own patch) get
1397                                // the edge fitted by projection into the
1398                                // target's chart, same-parameter with the
1399                                // edge, the way every reader derives a
1400                                // pcurve it was not given.
1401                                None => {
1402                                    let Some(pcurve) = projected_into_shared_chart(
1403                                        &e.curve,
1404                                        e.crange,
1405                                        &owner.surface,
1406                                        &target.surface,
1407                                        tol,
1408                                    )?
1409                                    else {
1410                                        // Wholly outside the target's window
1411                                        // is wholly outside the target: a
1412                                        // blend's run-out past the face it
1413                                        // melts with splits nothing there.
1414                                        continue;
1415                                    };
1416                                    if *DEBUG_WIRE {
1417                                        eprintln!(
1418                                            "SAME owner {} face {} (from_a {owner_from_a}) edge {} of kind {:?}: {:?} .. {:?} against target face {target_face}",
1419                                            if owner_from_a { "a" } else { "b" },
1420                                            if owner_from_a { ia } else { ib },
1421                                            e.node.index(),
1422                                            core::mem::discriminant(&e.curve),
1423                                            e.curve.point_at(e.crange.0, tol).ok(),
1424                                            e.curve.point_at(e.crange.1, tol).ok()
1425                                        );
1426                                    }
1427                                    (pcurve, e.crange)
1428                                }
1429                            };
1430                            contacts.push(ContactRec {
1431                                curve: e.curve.clone(),
1432                                crange: e.crange,
1433                                pcurve,
1434                                prange,
1435                                node: e.node,
1436                                tolerance: e.tolerance,
1437                                target_from_a,
1438                                target_face,
1439                            });
1440                        }
1441                    }
1442                }
1443                // A touch at a point bounds nothing: no curve to split a
1444                // face along, no side that is inside on one hand and
1445                // outside on the other. It contributes to the arrangement
1446                // exactly what a tangential contact curve does, which is
1447                // nothing, and the result carries the touch as the
1448                // non-manifold contact it is.
1449                SurfaceIntersection::Touching(_) => {}
1450                SurfaceIntersection::Along(curves) => {
1451                    for sc in curves {
1452                        // A tangential curve is contact, not crossing: the
1453                        // two faces meet along it and neither passes
1454                        // through the other, so it is set aside from the
1455                        // arrangement entirely and kept for the consumers
1456                        // that draw contact rather than classify by it.
1457                        if sc.tangential {
1458                            if let (Some(pa), Some(pb)) = (sc.on_a, sc.on_b) {
1459                                tangents.push(TangentRec {
1460                                    curve: sc.curve,
1461                                    pc_a: pa,
1462                                    pc_b: pb,
1463                                    face_a: ia,
1464                                    face_b: ib,
1465                                });
1466                            }
1467                            continue;
1468                        }
1469                        // A section can be no longer than a turn round the
1470                        // faces it cuts: a marched trace that wandered off
1471                        // beside a chart's pole came back twenty-five times
1472                        // the circle it stood for, faithfully fitted. That
1473                        // is no section, and is refused by name.
1474                        if !sc.exact {
1475                            use ogeom_geom::Curve3d as _;
1476                            let (lo, hi) = sc.curve.domain();
1477                            let mut length = 0.0_f64;
1478                            let mut last: Option<Point> = None;
1479                            for k in 0..=64 {
1480                                let t = if k == 64 {
1481                                    hi
1482                                } else {
1483                                    lo + (hi - lo) * f64::from(k) / 64.0
1484                                };
1485                                let p = sc.curve.point_at(t, tol)?;
1486                                if let Some(q) = last {
1487                                    length += q.distance(p);
1488                                }
1489                                last = Some(p);
1490                            }
1491                            let turn = 4.0 * fa.bound.diagonal().max(fb.bound.diagonal());
1492                            if *DEBUG_WIRE {
1493                                eprintln!(
1494                                    "SECTION CHECK faces {ia}/{ib} exact {} closed {} length {length:.4} turn {turn:.4} tol {:.2e}",
1495                                    sc.exact, sc.closed, sc.tolerance
1496                                );
1497                            }
1498                            if length > turn {
1499                                ogeom_bail!(
1500                                    NotDone,
1501                                    "a marched section of length {length} runs beyond a turn \
1502                                     round the faces it cuts ({turn}); its trace wandered \
1503                                     beside a chart's pole; see docs/PARITY.md, bool.booleans"
1504                                );
1505                            }
1506                        }
1507                        match (sc.on_a.clone(), sc.on_b.clone()) {
1508                            (Some(pa), Some(pb)) => sections.push(SectionRec {
1509                                curve: sc.curve,
1510                                pc_a: pa,
1511                                pc_b: pb,
1512                                face_a: ia,
1513                                face_b: ib,
1514                                closed: sc.closed,
1515                                tolerance: sc.tolerance,
1516                            }),
1517                            _ => {
1518                                // A section running through a chart
1519                                // degeneracy (a plane cutting a ball on its
1520                                // own axis meets it at both poles) has no
1521                                // single chart image, because the longitude
1522                                // jumps half a turn there. Each piece
1523                                // *between* the poles does have one, and it
1524                                // is exact. Split first; march only if that
1525                                // fails.
1526                                if let Some(split) = split_at_degeneracies(&sc.curve, fa, fb, tol)?
1527                                {
1528                                    for (curve, pa, pb) in split {
1529                                        sections.push(SectionRec {
1530                                            curve,
1531                                            pc_a: pa,
1532                                            pc_b: pb,
1533                                            face_a: ia,
1534                                            face_b: ib,
1535                                            closed: false,
1536                                            tolerance: 0.0,
1537                                        });
1538                                    }
1539                                    continue;
1540                                }
1541                                // An exact curve whose projection has no
1542                                // closed form keeps its exactness and has
1543                                // its chart image fitted: the curve's own
1544                                // points inverted on the surface, read in
1545                                // the chart, fitted at the curve's own
1546                                // parameters: a plane's circle passing
1547                                // beside a sphere chart's pole, whose image
1548                                // swings fast but is a curve all the same.
1549                                // Marching the pair, which follows, wandered
1550                                // beside the pole in both ways there are.
1551                                let image = |surface: &SurfaceGeometry,
1552                                             have: Option<&PlanarCurve>|
1553                                 -> OgeomResult<Option<(PlanarCurve, f64)>> {
1554                                    if let Some(pc) = have {
1555                                        return Ok(Some((pc.clone(), 0.0)));
1556                                    }
1557                                    fitted_image(&sc.curve, surface, options.tolerance, tol)
1558                                };
1559                                if let (Some((pa, ea)), Some((pb, eb))) = (
1560                                    image(&fa.surface, sc.on_a.as_ref())?,
1561                                    image(&fb.surface, sc.on_b.as_ref())?,
1562                                ) {
1563                                    sections.push(SectionRec {
1564                                        curve: sc.curve,
1565                                        pc_a: pa,
1566                                        pc_b: pb,
1567                                        face_a: ia,
1568                                        face_b: ib,
1569                                        closed: sc.closed,
1570                                        tolerance: sc.tolerance.max(ea).max(eb),
1571                                    });
1572                                    continue;
1573                                }
1574                                // No image fits: march the pair instead, so
1575                                // curve and pcurves are fitted *together*.
1576                                if *DEBUG_WIRE {
1577                                    eprintln!(
1578                                        "MARCH PAIR faces {ia}/{ib} a {:?} b {:?} admitted {admitted}",
1579                                        core::mem::discriminant(&fa.surface),
1580                                        core::mem::discriminant(&fb.surface)
1581                                    );
1582                                }
1583                                let shared = if admitted {
1584                                    fa.bound.intersection(&fb.bound)
1585                                } else {
1586                                    // Audit only: disjoint bounds have no
1587                                    // window, and an empty window would mask
1588                                    // the very miss being hunted.
1589                                    fa.bound.union(&fb.bound)
1590                                };
1591                                for fitted in march_pair(
1592                                    &windowed_to(&fa.surface, &shared),
1593                                    &windowed_to(&fb.surface, &shared),
1594                                    &options,
1595                                    tol,
1596                                )? {
1597                                    // The same two refusals the intersector's
1598                                    // own marched sections meet: a fit a
1599                                    // thousand chords off its trace, and a
1600                                    // trace longer than a turn round the
1601                                    // faces. A plane's circle passing beside
1602                                    // a sphere chart's pole marched both ways
1603                                    // here (a section six tenths of a
1604                                    // millimetre off, a section twenty-five
1605                                    // laps long), and stated as data they
1606                                    // welded the tool into a point.
1607                                    let budget =
1608                                        (options.marching.chord * 1e3).max(options.tolerance * 1e3);
1609                                    if fitted.fit_error > budget {
1610                                        ogeom_bail!(
1611                                            NotDone,
1612                                            "a marched section's fit misses its trace by {} \
1613                                             against a chord of {}; a branch passing beside a \
1614                                             chart's pole fits nothing yet; see docs/PARITY.md, \
1615                                             boolean.general",
1616                                            fitted.fit_error,
1617                                            options.marching.chord
1618                                        );
1619                                    }
1620                                    let curve: Curve = fitted.curve.into();
1621                                    let length = {
1622                                        use ogeom_geom::Curve3d as _;
1623                                        let (lo, hi) = curve.domain();
1624                                        let mut length = 0.0_f64;
1625                                        let mut last: Option<Point> = None;
1626                                        for k in 0..=64 {
1627                                            let t = if k == 64 {
1628                                                hi
1629                                            } else {
1630                                                lo + (hi - lo) * f64::from(k) / 64.0
1631                                            };
1632                                            let p = curve.point_at(t, tol)?;
1633                                            if let Some(q) = last {
1634                                                length += q.distance(p);
1635                                            }
1636                                            last = Some(p);
1637                                        }
1638                                        length
1639                                    };
1640                                    let turn = 4.0 * fa.bound.diagonal().max(fb.bound.diagonal());
1641                                    if length > turn {
1642                                        ogeom_bail!(
1643                                            NotDone,
1644                                            "a marched section of length {length} runs beyond a \
1645                                             turn round the faces it cuts ({turn}); its trace \
1646                                             wandered beside a chart's pole; see docs/PARITY.md, \
1647                                             boolean.general"
1648                                        );
1649                                    }
1650                                    sections.push(SectionRec {
1651                                        closed: curve.is_closed(tol),
1652                                        tolerance: options.marching.chord + fitted.fit_error,
1653                                        curve,
1654                                        pc_a: fitted.on_a.into(),
1655                                        pc_b: fitted.on_b.into(),
1656                                        face_a: ia,
1657                                        face_b: ib,
1658                                    });
1659                                }
1660                                break;
1661                            }
1662                        }
1663                    }
1664                }
1665            }
1666        }
1667    }
1668
1669    // Boundary polylines per face, for the trim tests.
1670    let outline = |face: &GFace| -> OgeomResult<Vec<Vec<Point2>>> {
1671        let mut lines = Vec::new();
1672        for e in &face.edges {
1673            lines.push(pcurve_polyline(
1674                &e.pcurve,
1675                e.prange,
1676                e.crange,
1677                e.crange,
1678                &face.surface,
1679                tol,
1680            )?);
1681            if let Some((other, orange)) = &e.other_side {
1682                lines.push(pcurve_polyline(
1683                    other,
1684                    *orange,
1685                    e.crange,
1686                    e.crange,
1687                    &face.surface,
1688                    tol,
1689                )?);
1690            }
1691        }
1692        weld_outline_ends(&mut lines, outline_snap(face, tol));
1693        Ok(lines)
1694    };
1695    let mut outlines_a = Vec::new();
1696    for f in &ga.faces {
1697        outlines_a.push(outline(f)?);
1698    }
1699    let mut outlines_b = Vec::new();
1700    for f in &gb.faces {
1701        outlines_b.push(outline(f)?);
1702    }
1703
1704    // Crossings of each section with the boundary edges of both its faces,
1705    // and with every other section sharing a face.
1706    let mut paves: std::collections::HashMap<EdgeKey, Vec<Pave>> = std::collections::HashMap::new();
1707    let mut pieces: Vec<SectionPiece> = Vec::new();
1708    // Each section's paving depends only on the sections and the two
1709    // gathered solids, all read-only here, and writes nothing the next
1710    // section reads. So the measuring runs in parallel and the accumulating
1711    // runs afterwards in section order: the same split `tessellate` uses,
1712    // and the same reason: nothing about scheduling can reach the answer.
1713    type SectionWork = (Vec<(EdgeKey, Pave)>, Vec<SectionPiece>, Vec<Junction>);
1714    let mut hug_junctions: Vec<Junction> = Vec::new();
1715    let paved: Vec<OgeomResult<SectionWork>> = ogeom_core::parallel::map_ordered(
1716        &sections,
1717        |si, section: &SectionRec| {
1718            ogeom_core::progress::checkpoint()?;
1719            let mut paves: Vec<(EdgeKey, Pave)> = Vec::new();
1720            let mut pieces: Vec<SectionPiece> = Vec::new();
1721            let mut junctions: Vec<Junction> = Vec::new();
1722            // A fitted section meets an edge within its own budget, not within
1723            // rounding.
1724            let reach = tol.confusion().max(section.tolerance * 2.0);
1725            let cc = CurveCurveOptions {
1726                gap: reach.max(CurveCurveOptions::default().gap),
1727                ..CurveCurveOptions::default()
1728            };
1729            let domain = section.curve.domain();
1730            let mut trim_ts: Vec<f64> = Vec::new();
1731            // Crossings with boundary edges: side, edge, parameter on the
1732            // edge, parameter on the section, and how honestly the stop sits.
1733            let mut hits: Vec<(usize, EdgeKey, f64, f64, f64)> = Vec::new();
1734            // Spans of the section running *along* a boundary edge. The split
1735            // such a span would make already exists as boundary (stacked boxes'
1736            // perpendicular side planes meet exactly at the boxes' own edges),
1737            // so the span is excluded from that face's strands rather than
1738            // refused or duplicated.
1739            //
1740            // Which face, though, is the whole point. A plane through a bore's
1741            // axis meets the wall along two rulings, and one of them is the
1742            // wall's own seam: on the wall that ruling is boundary already, and
1743            // on the plane it is the curve that separates the two halves the
1744            // section leaves. Dropped from both, the plane keeps one region where
1745            // it has two, and the result does not close. So the exclusion is
1746            // recorded per face.
1747            let mut along: [Vec<(f64, f64, usize)>; 2] = [Vec::new(), Vec::new()];
1748            for (side, own) in [
1749                (0_usize, &ga.faces[section.face_a]),
1750                (1, &gb.faces[section.face_b]),
1751            ] {
1752                // The other face the section must also lie in: a crossing
1753                // with this edge outside it stops nothing that is kept, and a
1754                // face bounded by thousands of edges would try every one.
1755                let across = if side == 0 {
1756                    &gb.faces[section.face_b]
1757                } else {
1758                    &ga.faces[section.face_a]
1759                };
1760                for (ei, e) in own.edges.iter().enumerate() {
1761                    if !admit_all && !e.bound.expanded(reach).intersects(&across.bound) {
1762                        continue;
1763                    }
1764                    // On a plane, an edge within its own radius of the face
1765                    // (the boundary of a merged group of near-coplanar facets)
1766                    // stands off the plane the section lies in by as much, and
1767                    // meets the section only that near; it departs from the
1768                    // face no other way, so a near miss there is the crossing.
1769                    // A curved face's fitted rail can pass that near a section
1770                    // without crossing it, and keeps the section's reach.
1771                    let edge_reach = if matches!(own.surface, SurfaceGeometry::Plane(_)) {
1772                        reach + e.tolerance
1773                    } else {
1774                        reach
1775                    };
1776                    let cc = CurveCurveOptions {
1777                        gap: cc.gap.max(edge_reach),
1778                        ..cc
1779                    };
1780                    let found = intersect_curves(&section.curve, &e.curve, cc, tol)?;
1781                    for crossing in &found.crossings {
1782                        if crossing.gap > edge_reach {
1783                            continue;
1784                        }
1785                        let mut on_b = onto_range(crossing.on_b, &e.curve, e.crange, tol);
1786                        let mut honesty = honest(section.tolerance, tol);
1787                        // A crossing at a boundary edge's own end *is* that end's
1788                        // vertex, exactly. The stop the section keeps must be the
1789                        // vertex's parameter on the section: the meet of two
1790                        // curves a fit tolerance apart lands a couple of microns
1791                        // off, the boundary side keeps its exact vertex, and the
1792                        // rebuilt wire gapes by the difference.
1793                        let mut on_a = crossing.on_a;
1794                        // The window is the fit-slop scale, not the section's
1795                        // own: the boundary curve may be a fitted intersection
1796                        // from an earlier boolean carrying a couple of microns
1797                        // of wobble, and a stop that misses the vertex by that
1798                        // much gapes the rebuilt wire by the same. A fitted
1799                        // section's stop misses by its own honesty once more:
1800                        // the trace is off the true crossing by that, and
1801                        // the crossing found against the edge by that again.
1802                        let weld =
1803                            (reach + honest(section.tolerance, tol)).max(tol.confusion() * 1e2);
1804                        let mut at_end: Option<(f64, Point)> = None;
1805                        for end in [e.crange.0, e.crange.1] {
1806                            let vertex = e.curve.point_at(end, tol)?;
1807                            if vertex.distance(crossing.point) <= weld + crossing.reach {
1808                                at_end = Some((end, vertex));
1809                                let snapped =
1810                                    ogeom_algo::project_on_curve(&section.curve, vertex, 64, tol)?;
1811                                if snapped.distance <= weld + crossing.reach {
1812                                    on_a = snapped.parameter;
1813                                    // And the edge's own parameter is the
1814                                    // end's: two rim circles through one
1815                                    // block corner meet at a shallow angle
1816                                    // there, the crossing lands a few
1817                                    // hundredths of a millimetre along the
1818                                    // rim from the corner, and paved there
1819                                    // it split the rim into a sliver.
1820                                    on_b = end;
1821                                }
1822                                break;
1823                            }
1824                        }
1825                        // A touch at the section's own end *is* the end. A
1826                        // band's section through a wall meets the wall's drum
1827                        // edge tangentially where the band is tangent to the
1828                        // drum, and the touch of two fitted curves at a
1829                        // shallow angle wanders along them by far more than
1830                        // their honesty; but the section stops there because
1831                        // it leaves its own face, and that stop is the
1832                        // junction: the crossing takes the end's parameter
1833                        // and the edge splits under the end itself.
1834                        let touch = crossing.reach > 0.0
1835                            || tangential(
1836                                &section.curve,
1837                                crossing.on_a,
1838                                &e.curve,
1839                                crossing.on_b,
1840                                tol,
1841                            )?;
1842                        if touch {
1843                            let half = (domain.1 - domain.0) * 0.5;
1844                            for end in [domain.0, domain.1] {
1845                                if (crossing.on_a - end).abs() > half {
1846                                    continue;
1847                                }
1848                                let tip = section
1849                                    .curve
1850                                    .point_at(at_param(end, domain, section.closed), tol)?;
1851                                if crossing.reach > 0.0
1852                                    && tip.distance(crossing.point) > crossing.reach + weld
1853                                {
1854                                    continue;
1855                                }
1856                                // The tip stands where the marcher left the
1857                                // other face (on a fitted rail, to the rail's
1858                                // own honesty), so the edge is asked at the
1859                                // hug width, not the section's.
1860                                let foot = ogeom_algo::project_on_curve(&e.curve, tip, 64, tol)?;
1861                                let width =
1862                                    (crossing.reach + honest(weld, tol)).max(tol.confusion() * 1e3);
1863                                if *DEBUG_WIRE {
1864                                    eprintln!(
1865                                        "PAVE s{si}: touch on edge {} at {:.6}: tip {:.3e} off the edge, width {width:.2e}",
1866                                        e.node.index(),
1867                                        crossing.on_a,
1868                                        foot.distance
1869                                    );
1870                                }
1871                                if foot.distance <= width {
1872                                    on_a = end;
1873                                    on_b = onto_range(foot.parameter, &e.curve, e.crange, tol);
1874                                    // A touch at the edge's own end vertex
1875                                    // is that vertex: a section tangent to
1876                                    // the edge there runs along it for a
1877                                    // stretch either side, stops somewhere
1878                                    // on that stretch, and split where it
1879                                    // stopped the edge keeps a sliver the
1880                                    // face across the vertex never has.
1881                                    if let Some((end_b, vertex)) = at_end
1882                                        && tip.distance(vertex) <= width.min(tol.confusion() * 1e5)
1883                                    {
1884                                        on_b = end_b;
1885                                        let gap = tip.distance(vertex);
1886                                        if gap > tol.confusion() * 1e2 {
1887                                            junctions.push(Junction {
1888                                                at: vertex,
1889                                                reach: gap + tol.confusion() * 1e2,
1890                                                onto_vertex: true,
1891                                            });
1892                                        }
1893                                    }
1894                                    // Along the edge the touch is known only
1895                                    // as far as the two curves stay together:
1896                                    // a rail grazing a bore is met there by
1897                                    // the sections on the faces either side
1898                                    // of it a hundredth of a millimetre
1899                                    // apart, and those are one junction.
1900                                    honesty = honesty.max(foot.distance).max(crossing.reach);
1901                                }
1902                                break;
1903                            }
1904                        }
1905                        if *DEBUG_WIRE {
1906                            eprintln!(
1907                                "PAVE s{si}: side {side} edge {} crossing at {on_a:.6} (edge {on_b:.6}) gap {:.2e} reach {:.2e} honesty {honesty:.2e} at {:?}",
1908                                e.node.index(),
1909                                crossing.gap,
1910                                crossing.reach,
1911                                crossing.point
1912                            );
1913                        }
1914                        hits.push((side, e.node, on_b, on_a, honesty));
1915                    }
1916                    for overlap in &found.overlaps {
1917                        // The curves overlap; what is *boundary* is the stretch
1918                        // the edge actually covers. A sphere's seam and the far
1919                        // half of the same great circle lie on one curve, and
1920                        // reading the whole curve as boundary makes the meridian
1921                        // opposite the seam disappear, which is the octant's own
1922                        // edge, on a ball cut at its corner.
1923                        let Some((lo, hi)) = overlap_within(overlap, e.crange, &e.curve, tol)
1924                        else {
1925                            continue;
1926                        };
1927                        if *DEBUG_WIRE {
1928                            eprintln!("PAVE s{si}: side {side} overlap ({lo:.6}, {hi:.6})");
1929                        }
1930                        // The edge splits where the shared stretch ends, as
1931                        // the section does: the stretch itself is the edge's
1932                        // to carry, and the section's next piece must meet
1933                        // the edge at a vertex the edge actually has. A band
1934                        // meeting a wall tangentially to the wall's own top
1935                        // edge hugs it for a few microns from their corner;
1936                        // without the split at the hug's far end the section
1937                        // dangles there and is pruned, and the wall never
1938                        // splits.
1939                        // The section leaves the edge at the hug's end by
1940                        // what the hug allowed, which no single strand's
1941                        // honesty covers: that junction owns the gap.
1942                        for t in [lo, hi] {
1943                            let at = section
1944                                .curve
1945                                .point_at(at_param(t, domain, section.closed), tol)?;
1946                            let foot = ogeom_algo::project_on_curve(&e.curve, at, 64, tol)?;
1947                            if foot.distance <= reach.max(tol.confusion() * 1e3) {
1948                                let on_b = onto_range(foot.parameter, &e.curve, e.crange, tol);
1949                                hits.push((
1950                                    side,
1951                                    e.node,
1952                                    on_b,
1953                                    t,
1954                                    honest(section.tolerance, tol).max(foot.distance),
1955                                ));
1956                                if foot.distance > tol.confusion() * 1e2 {
1957                                    junctions.push(Junction {
1958                                        at: foot.point,
1959                                        reach: foot.distance + tol.confusion() * 1e2,
1960                                        onto_vertex: false,
1961                                    });
1962                                }
1963                            }
1964                        }
1965                        trim_ts.push(lo);
1966                        trim_ts.push(hi);
1967                        along[side].push((lo, hi, ei));
1968                    }
1969                }
1970            }
1971            // Inside a stretch the section runs along one of a face's edges,
1972            // that face's other edges cannot genuinely cross it: a boundary
1973            // is a simple loop, and they meet the hugged edge only at its
1974            // ends, which the stretch's own ends already stop at. What the
1975            // sampler reports there is the tangential dust of a leg touching
1976            // the arc it ends on: thirty near-crossings inside a micron,
1977            // which read as stops would shatter the section and pave the
1978            // leg at each.
1979            hits.retain(|(side, _, _, on_a, _)| {
1980                !along[*side].iter().any(|(lo, hi, _)| {
1981                    *on_a > lo + tol.parametric() && *on_a < hi - tol.parametric()
1982                })
1983            });
1984            let edge_hits: Vec<(EdgeKey, f64, f64, f64)> = hits
1985                .iter()
1986                .map(|(_, node, on_b, on_a, honesty)| (*node, *on_b, *on_a, *honesty))
1987                .collect();
1988            trim_ts.extend(hits.iter().map(|(_, _, _, on_a, _)| *on_a));
1989            let mut cross_ts: Vec<f64> = Vec::new();
1990            for (sj, other) in sections.iter().enumerate() {
1991                if sj == si {
1992                    continue;
1993                }
1994                if other.face_a != section.face_a && other.face_b != section.face_b {
1995                    continue;
1996                }
1997                let both = reach.max(tol.confusion().max(other.tolerance * 2.0));
1998                // Where two sections share one face, a crossing matters only
1999                // inside the faces they do not share, since a section is kept
2000                // only where it lies in both its faces: one wall met by a
2001                // thousand facets carries a thousand sections, and only those
2002                // whose facets' bounds meet can cross where it counts. The
2003                // audit's unfiltered pass tries every pair still.
2004                if !admit_all {
2005                    let apart = |x: &ogeom_math::Aabb, y: &ogeom_math::Aabb| {
2006                        !x.expanded(both).intersects(&y.expanded(both))
2007                    };
2008                    let unshared_a = other.face_a != section.face_a
2009                        && apart(
2010                            &ga.faces[section.face_a].bound,
2011                            &ga.faces[other.face_a].bound,
2012                        );
2013                    let unshared_b = other.face_b != section.face_b
2014                        && apart(
2015                            &gb.faces[section.face_b].bound,
2016                            &gb.faces[other.face_b].bound,
2017                        );
2018                    if unshared_a || unshared_b {
2019                        continue;
2020                    }
2021                }
2022                let cc2 = CurveCurveOptions {
2023                    gap: both.max(CurveCurveOptions::default().gap),
2024                    ..CurveCurveOptions::default()
2025                };
2026                let found = intersect_curves(&section.curve, &other.curve, cc2, tol)?;
2027                for crossing in &found.crossings {
2028                    if crossing.gap > both {
2029                        continue;
2030                    }
2031                    let mut at = crossing.on_a;
2032                    // A touch at this section's end is the end, as against an
2033                    // edge: two sections through one wall from a band and
2034                    // the leg it is tangent to touch where the band's ends.
2035                    let touch = crossing.reach > 0.0
2036                        || tangential(
2037                            &section.curve,
2038                            crossing.on_a,
2039                            &other.curve,
2040                            crossing.on_b,
2041                            tol,
2042                        )?;
2043                    if touch {
2044                        let half = (domain.1 - domain.0) * 0.5;
2045                        for end in [domain.0, domain.1] {
2046                            if (crossing.on_a - end).abs() > half {
2047                                continue;
2048                            }
2049                            let tip = section
2050                                .curve
2051                                .point_at(at_param(end, domain, section.closed), tol)?;
2052                            let foot = ogeom_algo::project_on_curve(&other.curve, tip, 64, tol)?;
2053                            let width =
2054                                (crossing.reach + honest(both, tol)).max(tol.confusion() * 1e3);
2055                            if foot.distance <= width {
2056                                at = end;
2057                            }
2058                            break;
2059                        }
2060                    }
2061                    // Two sections through one face that lie on two different
2062                    // faces of the other solid cross only on the edge those
2063                    // two share, where each already stops. Meeting there
2064                    // tangentially (a wall through a plane and the fillet
2065                    // tangent to it), their own crossing is ill-conditioned
2066                    // and lands off the edge, as far off as the two stay
2067                    // within their doubt of each other; the edge's stop is
2068                    // the one where they cannot be told apart up to it.
2069                    let shared = |edges: &[BoundaryEdge], node: EdgeKey| {
2070                        edges.iter().any(|e| e.node == node)
2071                    };
2072                    let apart_on = if other.face_a == section.face_a {
2073                        Some(&gb.faces[other.face_b].edges)
2074                    } else if other.face_b == section.face_b {
2075                        Some(&ga.faces[other.face_a].edges)
2076                    } else {
2077                        None
2078                    };
2079                    if let Some(edges) = apart_on {
2080                        let close = both.max(tol.confusion() * 1e2);
2081                        let mut best: Option<(f64, f64)> = None;
2082                        for (node, _, on_a, _) in &edge_hits {
2083                            if !shared(edges, *node)
2084                                || best.is_some_and(|(bd, _)| (on_a - at).abs() >= bd)
2085                            {
2086                                continue;
2087                            }
2088                            let mut together = true;
2089                            for k in 1..8 {
2090                                let t = (on_a - at).mul_add(f64::from(k) / 8.0, at);
2091                                let p = section
2092                                    .curve
2093                                    .point_at(at_param(t, domain, section.closed), tol)?;
2094                                if ogeom_algo::project_on_curve(&other.curve, p, 16, tol)?.distance
2095                                    > close
2096                                {
2097                                    together = false;
2098                                    break;
2099                                }
2100                            }
2101                            if together {
2102                                best = Some(((on_a - at).abs(), *on_a));
2103                            }
2104                        }
2105                        if let Some((_, on_a)) = best {
2106                            at = on_a;
2107                        }
2108                    }
2109                    if *DEBUG_WIRE {
2110                        eprintln!(
2111                            "PAVE s{si}: cross s{sj} at {at:.6} gap {:.2e}",
2112                            crossing.gap
2113                        );
2114                    }
2115                    cross_ts.push(at);
2116                }
2117            }
2118
2119            // Candidate intervals between trim crossings, kept where the middle
2120            // sits inside both faces' trims.
2121            trim_ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
2122            trim_ts.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
2123            // Stops the section cannot tell apart are one stop: two
2124            // crossings a few nanometres apart along it would make an
2125            // interval of dust that one face keeps and the other drops.
2126            {
2127                let floor = (honest(section.tolerance, tol) * 3.0).max(tol.confusion() * 10.0);
2128                let mut kept: Vec<f64> = Vec::with_capacity(trim_ts.len());
2129                let mut held: Option<Point> = None;
2130                for t in &trim_ts {
2131                    let at = section
2132                        .curve
2133                        .point_at(at_param(*t, domain, section.closed), tol)?;
2134                    if held.is_none_or(|h: Point| h.distance(at) > floor) {
2135                        kept.push(*t);
2136                        held = Some(at);
2137                    }
2138                }
2139                trim_ts = kept;
2140            }
2141            let mut candidates: Vec<(f64, f64)> = Vec::new();
2142            if section.closed {
2143                let period = domain.1 - domain.0;
2144                if trim_ts.is_empty() {
2145                    candidates.push(domain);
2146                } else {
2147                    for i in 0..trim_ts.len() {
2148                        let lo = trim_ts[i];
2149                        let hi = if i + 1 < trim_ts.len() {
2150                            trim_ts[i + 1]
2151                        } else {
2152                            trim_ts[0] + period
2153                        };
2154                        candidates.push((lo, hi));
2155                    }
2156                }
2157            } else {
2158                let mut stops = vec![domain.0];
2159                stops.extend(
2160                    trim_ts
2161                        .iter()
2162                        .copied()
2163                        .filter(|t| *t > domain.0 && *t < domain.1),
2164                );
2165                stops.push(domain.1);
2166                for pair in stops.windows(2) {
2167                    candidates.push((pair[0], pair[1]));
2168                }
2169            }
2170
2171            // Whether the section at `t` lies inside each face's trim.
2172            let inside_each = |t: f64| -> OgeomResult<[bool; 2]> {
2173                let tf = if section.closed { fold(t, domain) } else { t };
2174                let la: Vec<&[Point2]> = outlines_a[section.face_a]
2175                    .iter()
2176                    .map(Vec::as_slice)
2177                    .collect();
2178                let lb: Vec<&[Point2]> = outlines_b[section.face_b]
2179                    .iter()
2180                    .map(Vec::as_slice)
2181                    .collect();
2182                let ua = fold_inside(
2183                    section.pc_a.point_at(tf, tol)?,
2184                    &ga.faces[section.face_a].surface,
2185                    &la,
2186                );
2187                let ub = fold_inside(
2188                    section.pc_b.point_at(tf, tol)?,
2189                    &gb.faces[section.face_b].surface,
2190                    &lb,
2191                );
2192                Ok([inside_many(&la, ua), inside_many(&lb, ub)])
2193            };
2194
2195            for (lo, hi) in candidates {
2196                if hi - lo <= tol.parametric() {
2197                    continue;
2198                }
2199                let mid = f64::midpoint(lo, hi);
2200                let mid_folded = if section.closed {
2201                    fold(mid, domain)
2202                } else {
2203                    mid
2204                };
2205                let held = inside_each(mid)?;
2206                if *DEBUG_WIRE {
2207                    let tf = if section.closed {
2208                        fold(mid, domain)
2209                    } else {
2210                        mid
2211                    };
2212                    eprintln!(
2213                        "PAVE s{si}: candidate ({lo:.6}, {hi:.6}) inside {held:?} at a {:?} b {:?}",
2214                        section
2215                            .pc_a
2216                            .point_at(tf, tol)
2217                            .ok()
2218                            .map(|q| fold_point_into_chart(q, &ga.faces[section.face_a].surface)),
2219                        section
2220                            .pc_b
2221                            .point_at(tf, tol)
2222                            .ok()
2223                            .map(|q| fold_point_into_chart(q, &gb.faces[section.face_b].surface))
2224                    );
2225                }
2226                // A section that runs along a boundary edge of a face splits
2227                // nothing *there*: the split already exists as boundary. The
2228                // analytic overlap detection above catches the same-support
2229                // cases; this catches the rest (a fitted section tracing a
2230                // boundary curve, a surface meeting another exactly at its own
2231                // trim) by measurement rather than by recognising supports.
2232                // The hug is asked before the trim: a section on a face's own
2233                // edge reads inside or outside that face by a hair, and the
2234                // corner tool's block face meets a band exactly along the arc
2235                // that bounds it. On the hugging face it is boundary already;
2236                // what matters is whether the *other* face holds it.
2237                let mut hugs = [false; 2];
2238                // The edges each side hugs (a rim already in pieces is
2239                // several): they must split wherever the section does, and
2240                // the section wherever they end, or the two faces walk
2241                // different subdivisions of one curve.
2242                let mut hugged: [Vec<usize>; 2] = [Vec::new(), Vec::new()];
2243                for (side, own, side_from_a, side_face) in [
2244                    (0_usize, &ga.faces[section.face_a], true, section.face_a),
2245                    (1, &gb.faces[section.face_b], false, section.face_b),
2246                ] {
2247                    if let Some((_, _, ei)) = along[side]
2248                        .iter()
2249                        .find(|(alo, ahi, _)| mid_folded >= *alo && mid_folded <= *ahi)
2250                    {
2251                        hugs[side] = true;
2252                        hugged[side].push(*ei);
2253                        continue;
2254                    }
2255                    let mut all_near = true;
2256                    let mut votes: Vec<usize> = vec![0; own.edges.len()];
2257                    let mut contact_votes: Vec<usize> = vec![0; contacts.len()];
2258                    for i in 0..=4 {
2259                        let t = lo + (hi - lo) * f64::from(i) / 4.0;
2260                        let tf = if section.closed { fold(t, domain) } else { t };
2261                        let at = section.curve.point_at(tf, tol)?;
2262                        // Wider than the crossing filters on purpose for a
2263                        // fitted section: a tangentially-traced curve wobbles
2264                        // about the boundary it hugs by far more than a fit
2265                        // budget, and a genuine section keeps a distance of
2266                        // feature scale, not microns. An exact section keeps
2267                        // to the edge's own honesty: a plane's ellipse across
2268                        // a band touches the band's rail tangentially, and
2269                        // the stretch within a tenth of a millimetre of the
2270                        // rail is the section, not the rail; read as a hug
2271                        // it left the cap's crescent unable to close.
2272                        let floor = if section.tolerance > 0.0 {
2273                            tol.confusion() * 1e3
2274                        } else {
2275                            tol.confusion() * 10.0
2276                        };
2277                        let mut near = false;
2278                        for (ei, e) in own.edges.iter().enumerate() {
2279                            let width = reach.max(floor).max(e.tolerance * 2.0);
2280                            let d = distance_to_edge_curve(&e.curve, e.crange, at, tol)?;
2281                            if d <= width {
2282                                if *DEBUG_WIRE {
2283                                    eprintln!(
2284                                        "PAVE s{si}: side {side} sample {i} hugs edge {} at {d:.2e} (width {width:.2e}) range {:?} point {at:?}",
2285                                        e.node.index(),
2286                                        e.crange
2287                                    );
2288                                }
2289                                // Every edge within reach is hugged, not the
2290                                // first found: where three rims converge on
2291                                // a pole corner all three come within a
2292                                // micron of the section, and the one it
2293                                // actually runs along must not lose its
2294                                // vote to a neighbour listed before it.
2295                                near = true;
2296                                votes[ei] += 1;
2297                            }
2298                        }
2299                        let width = reach.max(floor);
2300                        // A contact edge is a strand on this face too (the other
2301                        // solid's boundary, carried into a chart they share), so a
2302                        // section tracing one would be the same curve twice, and
2303                        // the arrangement cannot walk a line it meets from both
2304                        // sides at once. Two boxes side by side put the low one's
2305                        // lid exactly there.
2306                        for (ci, c) in contacts.iter().enumerate() {
2307                            if c.target_from_a != side_from_a || c.target_face != side_face {
2308                                continue;
2309                            }
2310                            if distance_to_edge_curve(&c.curve, c.crange, at, tol)?
2311                                <= width.max(c.tolerance * 2.0)
2312                            {
2313                                near = true;
2314                                contact_votes[ci] += 1;
2315                            }
2316                        }
2317                        if !near {
2318                            all_near = false;
2319                            break;
2320                        }
2321                    }
2322                    // And along one of them all the way: a section winding
2323                    // round a drum passes the drum's seam once a turn, and a
2324                    // stretch spanning whole turns puts every sample on the
2325                    // seam without running along it anywhere between.
2326                    let one_line = votes.iter().chain(&contact_votes).any(|&v| v == 5);
2327                    hugs[side] = all_near && one_line;
2328                    if all_near {
2329                        hugged[side].extend(
2330                            votes
2331                                .iter()
2332                                .enumerate()
2333                                .filter(|(_, c)| **c > 0)
2334                                .map(|(ei, _)| ei),
2335                        );
2336                    }
2337                }
2338                if *DEBUG_WIRE {
2339                    eprintln!("PAVE s{si}: candidate ({lo:.6}, {hi:.6}) hugs {hugs:?}");
2340                }
2341                // Inside both faces the piece is a section of both. Hugging
2342                // one face's own edge while that face's trim reads it out
2343                // (by a hair, on the edge), it is still the other face's
2344                // split, and is admitted there, unless that edge is already
2345                // carried onto the other face as a contact: then the split
2346                // is laid down once already, and a section would lay it
2347                // twice.
2348                let mut hug_key: Option<(EdgeKey, bool, usize)> = None;
2349                let admitted = if held[0] && held[1] {
2350                    true
2351                } else if hugs[0] != hugs[1] {
2352                    let hugging = usize::from(hugs[1]);
2353                    let other = 1 - hugging;
2354                    let (own, other_from_a, other_face) = if hugging == 0 {
2355                        (&ga.faces[section.face_a], false, section.face_b)
2356                    } else {
2357                        (&gb.faces[section.face_b], true, section.face_a)
2358                    };
2359                    let carried = hugged[hugging].iter().any(|&ei| {
2360                        contacts.iter().any(|c| {
2361                            c.node == own.edges[ei].node
2362                                && c.target_from_a == other_from_a
2363                                && c.target_face == other_face
2364                        })
2365                    });
2366                    if held[other] && !carried {
2367                        hug_key = hugged[hugging]
2368                            .first()
2369                            .map(|&ei| (own.edges[ei].node, other_from_a, other_face));
2370                        true
2371                    } else {
2372                        false
2373                    }
2374                } else {
2375                    false
2376                };
2377                if !admitted {
2378                    continue;
2379                }
2380                if hugs[0] && hugs[1] {
2381                    // Boundary on both sides: the split exists twice over and
2382                    // adding it a third time would cancel what it copies.
2383                    continue;
2384                }
2385                // Keep the paves that end a kept interval: those are where edges
2386                // genuinely split.
2387                for (node, on_edge, on_section, honesty) in &edge_hits {
2388                    let s = *on_section;
2389                    let near = |x: f64| {
2390                        (s - x).abs() <= tol.parametric()
2391                            || (section.closed
2392                                && ((s + (domain.1 - domain.0)) - x).abs() <= tol.parametric())
2393                    };
2394                    if near(lo) || near(hi) {
2395                        paves.push((
2396                            *node,
2397                            Pave {
2398                                t: *on_edge,
2399                                honesty: *honesty,
2400                            },
2401                        ));
2402                    }
2403                }
2404                // Split at section/section crossings inside the kept interval,
2405                // so every face sees the same subdivision.
2406                let mut cuts = vec![lo];
2407                // A wrap interval also splits at the curve's own domain end, so
2408                // every piece lives within one period and evaluates in-domain
2409                // after a single fold.
2410                if section.closed
2411                    && domain.1 > lo + tol.parametric()
2412                    && domain.1 < hi - tol.parametric()
2413                {
2414                    cuts.push(domain.1);
2415                }
2416                for &c in &cross_ts {
2417                    let c2 = if section.closed && c < lo {
2418                        c + (domain.1 - domain.0)
2419                    } else {
2420                        c
2421                    };
2422                    if c2 > lo + tol.parametric() && c2 < hi - tol.parametric() {
2423                        cuts.push(c2);
2424                    }
2425                }
2426                cuts.push(hi);
2427                // A hugged edge's own ends are cuts of the section too: the
2428                // edge may already be several pieces (a sphere's rim split
2429                // at its chart seam), and the section must walk the same
2430                // pieces, or the two faces never sew along it.
2431                for side in 0..2 {
2432                    if !hugs[side] {
2433                        continue;
2434                    }
2435                    let own = if side == 0 {
2436                        &ga.faces[section.face_a]
2437                    } else {
2438                        &gb.faces[section.face_b]
2439                    };
2440                    for &ei in &hugged[side] {
2441                        let e = &own.edges[ei];
2442                        for end in [e.crange.0, e.crange.1] {
2443                            let p = e.curve.point_at(end, tol)?;
2444                            let foot = ogeom_algo::project_on_curve(&section.curve, p, 64, tol)?;
2445                            if *DEBUG_WIRE {
2446                                eprintln!(
2447                                    "PAVE s{si}: side {side} hugged edge {} end {end:.6} lands at {:.6} off {:.2e} against ({lo:.6}, {hi:.6})",
2448                                    e.node.index(),
2449                                    foot.parameter,
2450                                    foot.distance
2451                                );
2452                            }
2453                            if foot.distance > reach.max(tol.confusion() * 1e3) {
2454                                continue;
2455                            }
2456                            let mut t = foot.parameter;
2457                            if section.closed {
2458                                let period = domain.1 - domain.0;
2459                                while t < lo {
2460                                    t += period;
2461                                }
2462                                while t - period >= lo {
2463                                    t -= period;
2464                                }
2465                            }
2466                            if t > lo + tol.parametric() && t < hi - tol.parametric() {
2467                                if *DEBUG_WIRE {
2468                                    eprintln!(
2469                                        "PAVE s{si}: side {side} cut at {t:.6} where hugged edge {} ends",
2470                                        e.node.index()
2471                                    );
2472                                }
2473                                cuts.push(t);
2474                            }
2475                        }
2476                    }
2477                }
2478                cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
2479                // Cuts the section cannot tell apart are one cut: two
2480                // crossings a few nanometres apart along it would leave a
2481                // piece of dust one face keeps and the other drops. The
2482                // interval's own ends stay.
2483                {
2484                    let floor = (honest(section.tolerance, tol) * 3.0).max(tol.confusion() * 10.0);
2485                    let mut kept: Vec<f64> = Vec::with_capacity(cuts.len());
2486                    let mut held: Option<Point> = None;
2487                    for (index, c) in cuts.iter().enumerate() {
2488                        let at = section
2489                            .curve
2490                            .point_at(at_param(*c, domain, section.closed), tol)?;
2491                        let last = index + 1 == cuts.len();
2492                        if index == 0 || last || held.is_none_or(|h: Point| h.distance(at) > floor)
2493                        {
2494                            kept.push(*c);
2495                            held = Some(at);
2496                        }
2497                    }
2498                    cuts = kept;
2499                }
2500                // A hugged edge is boundary where this piece is section: it
2501                // must split at the piece's interior cuts too, or the face
2502                // that keeps the edge walks one long strand where its
2503                // neighbour walks three, and the sew pairs none of them.
2504                for side in 0..2 {
2505                    if !hugs[side] {
2506                        continue;
2507                    }
2508                    let own = if side == 0 {
2509                        &ga.faces[section.face_a]
2510                    } else {
2511                        &gb.faces[section.face_b]
2512                    };
2513                    for &ei in &hugged[side] {
2514                        let e = &own.edges[ei];
2515                        // The piece's own ends as well as its interior cuts: a
2516                        // stop against another face's edge is a split of the
2517                        // hugged edge too, or the section's neighbour walks two
2518                        // strands where the hugged edge's face walks one.
2519                        for c in &cuts {
2520                            let at = section
2521                                .curve
2522                                .point_at(at_param(*c, domain, section.closed), tol)?;
2523                            let foot = ogeom_algo::project_on_curve(&e.curve, at, 64, tol)?;
2524                            // A cut off the edge (past the stretch the
2525                            // section hugs, on a neighbouring edge) is no
2526                            // split of it; stated as one with its distance
2527                            // for honesty it seeds a junction a feature wide.
2528                            if foot.distance > reach.max(tol.confusion() * 1e3) {
2529                                continue;
2530                            }
2531                            let on_e = onto_range(foot.parameter, &e.curve, e.crange, tol);
2532                            if on_e <= e.crange.0.min(e.crange.1) + tol.parametric()
2533                                || on_e >= e.crange.0.max(e.crange.1) - tol.parametric()
2534                            {
2535                                continue;
2536                            }
2537                            if *DEBUG_WIRE {
2538                                eprintln!(
2539                                    "PAVE s{si}: side {side} hugged edge {} paved at {on_e:.6} for the cut at {c:.6}",
2540                                    e.node.index()
2541                                );
2542                            }
2543                            paves.push((
2544                                e.node,
2545                                Pave {
2546                                    t: on_e,
2547                                    honesty: honest(section.tolerance, tol).max(foot.distance),
2548                                },
2549                            ));
2550                        }
2551                    }
2552                }
2553                for pair in cuts.windows(2) {
2554                    let (lo2, hi2) = (pair[0], pair[1]);
2555                    let from = section
2556                        .curve
2557                        .point_at(at_param(lo2, domain, section.closed), tol)?;
2558                    let to = section
2559                        .curve
2560                        .point_at(at_param(hi2, domain, section.closed), tol)?;
2561                    if from.distance(to) <= tol.confusion() {
2562                        // A full loop: two arcs, so every strand has two
2563                        // distinct endpoints.
2564                        let mid = f64::midpoint(lo2, hi2);
2565                        pieces.push(SectionPiece {
2566                            section: si,
2567                            range: (lo2, mid),
2568                            hugs,
2569                            hug_key,
2570                        });
2571                        pieces.push(SectionPiece {
2572                            section: si,
2573                            range: (mid, hi2),
2574                            hugs,
2575                            hug_key,
2576                        });
2577                    } else {
2578                        pieces.push(SectionPiece {
2579                            section: si,
2580                            range: (lo2, hi2),
2581                            hugs,
2582                            hug_key,
2583                        });
2584                    }
2585                }
2586            }
2587            Ok((paves, pieces, junctions))
2588        },
2589    );
2590    for work in paved {
2591        let (found, made, hugged) = work?;
2592        hug_junctions.extend(hugged);
2593        for (node, at) in found {
2594            paves.entry(node).or_default().push(at);
2595        }
2596        pieces.extend(made);
2597    }
2598    // A piece running along a face's own edge and that edge are one curve,
2599    // split twice: the piece where its section's cuts fell, the edge where
2600    // every section that met it paved it, each worked out apart. A loop
2601    // hugging a rim is split at its middle, the rim wherever another
2602    // section crossed it; the two faces then walk different pieces of one
2603    // circle and never sew. Now that every pave is in, each such piece is
2604    // cut wherever its edge is split, and the edge split wherever the piece
2605    // ends, twice over so a split one piece adds reaches the others.
2606    for _ in 0..2 {
2607        let mut settled: Vec<SectionPiece> = Vec::with_capacity(pieces.len());
2608        for piece in pieces.drain(..) {
2609            if !(piece.hugs[0] || piece.hugs[1]) {
2610                settled.push(piece);
2611                continue;
2612            }
2613            let section = &sections[piece.section];
2614            let domain = section.curve.domain();
2615            let period = domain.1 - domain.0;
2616            let (lo, hi) = piece.range;
2617            let at = |t: f64| {
2618                section
2619                    .curve
2620                    .point_at(at_param(t, domain, section.closed), tol)
2621            };
2622            let width = (tol.confusion() * 1e3).max(section.tolerance * 3.0);
2623            let mid = at(f64::midpoint(lo, hi))?;
2624            let mut hugged: Vec<&BoundaryEdge> = Vec::new();
2625            for side in 0..2 {
2626                if !piece.hugs[side] {
2627                    continue;
2628                }
2629                let face = if side == 0 {
2630                    &ga.faces[section.face_a]
2631                } else {
2632                    &gb.faces[section.face_b]
2633                };
2634                for e in &face.edges {
2635                    if distance_to_edge_curve(&e.curve, e.crange, mid, tol)?
2636                        <= width.max(e.tolerance * 2.0)
2637                    {
2638                        hugged.push(e);
2639                    }
2640                }
2641            }
2642            let mut cuts: Vec<f64> = Vec::new();
2643            for e in &hugged {
2644                let mut on_edge: Vec<f64> = vec![e.crange.0, e.crange.1];
2645                if let Some(list) = paves.get(&e.node) {
2646                    on_edge.extend(list.iter().map(|pave| pave.t));
2647                }
2648                for t in on_edge {
2649                    let q = e.curve.point_at(t, tol)?;
2650                    let foot = ogeom_algo::project_on_curve(&section.curve, q, 64, tol)?;
2651                    if foot.distance > width.max(e.tolerance * 2.0) {
2652                        continue;
2653                    }
2654                    let mut f = foot.parameter;
2655                    if section.closed && period > 0.0 {
2656                        while f < lo {
2657                            f += period;
2658                        }
2659                        while f - period >= lo {
2660                            f -= period;
2661                        }
2662                    }
2663                    if f > lo + tol.parametric() && f < hi - tol.parametric() {
2664                        cuts.push(f);
2665                    }
2666                }
2667            }
2668            cuts.push(lo);
2669            cuts.push(hi);
2670            cuts.sort_by(f64::total_cmp);
2671            cuts.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
2672            // The edge split wherever the piece now ends.
2673            for e in &hugged {
2674                for &c in &cuts {
2675                    let q = at(c)?;
2676                    let foot = ogeom_algo::project_on_curve(&e.curve, q, 64, tol)?;
2677                    if foot.distance > width.max(e.tolerance * 2.0) {
2678                        continue;
2679                    }
2680                    let on_e = onto_range(foot.parameter, &e.curve, e.crange, tol);
2681                    if on_e <= e.crange.0.min(e.crange.1) + tol.parametric()
2682                        || on_e >= e.crange.0.max(e.crange.1) - tol.parametric()
2683                    {
2684                        continue;
2685                    }
2686                    let list = paves.entry(e.node).or_default();
2687                    if list
2688                        .iter()
2689                        .all(|pave| (pave.t - on_e).abs() > tol.parametric())
2690                    {
2691                        list.push(Pave {
2692                            t: on_e,
2693                            honesty: honest(section.tolerance, tol).max(foot.distance),
2694                        });
2695                    }
2696                }
2697            }
2698            for pair in cuts.windows(2) {
2699                settled.push(SectionPiece {
2700                    section: piece.section,
2701                    range: (pair[0], pair[1]),
2702                    hugs: piece.hugs,
2703                    hug_key: piece.hug_key,
2704                });
2705            }
2706        }
2707        pieces = settled;
2708    }
2709    // Two sections hugging onto one face along one line lay the same split
2710    // down twice (a top face's trace and its band's, both along the rail
2711    // they share, which need not be one edge node once a shell has been
2712    // built), and a line met from both sides at once breaks the
2713    // arrangement. Pieces of different sections admitted by a hug onto the
2714    // same face, each lying on the other's curve, are one split, and the
2715    // section covering the most of it speaks for it.
2716    {
2717        let width = tol.confusion() * 1e3;
2718        let mut drop: Vec<usize> = Vec::new();
2719        let hugged_onto: Vec<usize> = pieces
2720            .iter()
2721            .enumerate()
2722            .filter(|(_, p)| p.hug_key.is_some())
2723            .map(|(i, _)| i)
2724            .collect();
2725        let mid_of = |p: &SectionPiece| -> OgeomResult<Point> {
2726            let section = &sections[p.section];
2727            let domain = section.curve.domain();
2728            section.curve.point_at(
2729                at_param(f64::midpoint(p.range.0, p.range.1), domain, section.closed),
2730                tol,
2731            )
2732        };
2733        let length_of = |p: &SectionPiece| -> OgeomResult<f64> {
2734            let section = &sections[p.section];
2735            let domain = section.curve.domain();
2736            let a = section
2737                .curve
2738                .point_at(at_param(p.range.0, domain, section.closed), tol)?;
2739            let b = section
2740                .curve
2741                .point_at(at_param(p.range.1, domain, section.closed), tol)?;
2742            Ok(a.distance(b))
2743        };
2744        // A hug-admitted piece against every other piece kept on the same
2745        // face: a section admitted the ordinary way, inside both faces,
2746        // already speaks for the split, and the hug-admitted one yields
2747        // to it; two hug-admitted ones yield to the longer.
2748        for &x in &hugged_onto {
2749            let px = &pieces[x];
2750            let Some((_, target_from_a, target_face)) = px.hug_key else {
2751                continue;
2752            };
2753            for (y, py) in pieces.iter().enumerate() {
2754                if y == x || py.section == px.section || drop.contains(&x) || drop.contains(&y) {
2755                    continue;
2756                }
2757                let sy = &sections[py.section];
2758                let on_target = if target_from_a {
2759                    sy.face_a == target_face && !py.hugs[0]
2760                } else {
2761                    sy.face_b == target_face && !py.hugs[1]
2762                };
2763                if !on_target {
2764                    continue;
2765                }
2766                let sx = &sections[px.section];
2767                // A closed section's pieces are ranged past its domain end
2768                // and folded on use; the distance helper samples the range
2769                // it is given, so it is given the folded one.
2770                let stretch = |section: &SectionRec, range: (f64, f64)| -> (f64, f64) {
2771                    folded_range(range, section.curve.domain(), section.closed)
2772                };
2773                let on_y =
2774                    distance_to_edge_curve(&sy.curve, stretch(sy, py.range), mid_of(px)?, tol)?
2775                        <= width;
2776                let on_x =
2777                    distance_to_edge_curve(&sx.curve, stretch(sx, px.range), mid_of(py)?, tol)?
2778                        <= width;
2779                if !(on_x && on_y) {
2780                    continue;
2781                }
2782                let loser = if py.hug_key.is_some() && length_of(px)? > length_of(py)? {
2783                    y
2784                } else {
2785                    x
2786                };
2787                if *DEBUG_WIRE {
2788                    eprintln!(
2789                        "PAVE: s{} and s{} split one face along one line; s{} speaks for it",
2790                        px.section,
2791                        py.section,
2792                        pieces[if loser == x { y } else { x }].section
2793                    );
2794                }
2795                drop.push(loser);
2796                if loser == x {
2797                    break;
2798                }
2799            }
2800        }
2801        let mut index = 0_usize;
2802        pieces.retain(|_| {
2803            let keep = !drop.contains(&index);
2804            index += 1;
2805            keep
2806        });
2807    }
2808    // The audit's verdict. A dropped pair whose surfaces intersect is not a
2809    // filter bug: planes meet along an infinite line the paving then trims
2810    // to the faces, usually to nothing. A dropped pair whose section
2811    // *survives paving* is: some of that curve lies inside both faces, so
2812    // the faces genuinely meet and the filter's boxes failed to. Contacts
2813    // need no check: a contact is an owner edge lying in the target face,
2814    // which forces the boxes to overlap where the edge does.
2815    //
2816    // This membership check is the audit's first tooth; the second is the
2817    // strict replay in general_fuse, which runs the *filtered* fill for the
2818    // production result and diffs this unfiltered one against it, so the
2819    // non-compositionality of paving (a section's kept intervals see the
2820    // other sections' paves) is caught rather than stated as a limit.
2821    if admit_all {
2822        for piece in &pieces {
2823            let section = &sections[piece.section];
2824            let (fa, fb) = (&ga.faces[section.face_a], &gb.faces[section.face_b]);
2825            assert!(
2826                fa.bound.intersects(&fb.bound),
2827                "bound filter audit: faces {}/{} were dropped by the bound \
2828                 filter, yet their section paved a surviving piece over \
2829                 {:?}; the filter under-approximates",
2830                section.face_a,
2831                section.face_b,
2832                piece.range,
2833            );
2834        }
2835    }
2836    // A contact edge splits where it crosses the target face's boundary,
2837    // and that crossing is a pave on *both* edges, so the owner's own face
2838    // splits its boundary consistently and the pieces sew back shared.
2839    let mut contact_along: Vec<Vec<(f64, f64)>> = vec![Vec::new(); contacts.len()];
2840    for (ci, contact) in contacts.iter().enumerate() {
2841        let target = if contact.target_from_a {
2842            &ga.faces[contact.target_face]
2843        } else {
2844            &gb.faces[contact.target_face]
2845        };
2846        // A fitted contact sits off exact geometry by its own tolerance, and
2847        // the crossings it genuinely makes gape by the same: the filter and
2848        // the finder both widen, or a fitted rail never registers against
2849        // the exact seam it crosses.
2850        let reach = tol.confusion().max(contact.tolerance * 2.0);
2851        let cc = CurveCurveOptions {
2852            gap: reach.max(CurveCurveOptions::default().gap),
2853            ..CurveCurveOptions::default()
2854        };
2855        for e in &target.edges {
2856            let found = intersect_curves(&contact.curve, &e.curve, cc, tol)?;
2857            for crossing in &found.crossings {
2858                if crossing.gap > reach {
2859                    continue;
2860                }
2861                if crossing.on_a < contact.crange.0 + tol.parametric()
2862                    || crossing.on_a > contact.crange.1 - tol.parametric()
2863                {
2864                    continue;
2865                }
2866                // Touching is not crossing, at curve level as at surface
2867                // level: where the two curves run nearly parallel (a blend
2868                // arc meeting the edge it is tangent to), the "crossing" is
2869                // numerical noise smeared along the contact, and paving it
2870                // would plant a vertex a hair off both curves.
2871                if tangential(&contact.curve, crossing.on_a, &e.curve, crossing.on_b, tol)? {
2872                    continue;
2873                }
2874                let honesty = honest(contact.tolerance, tol).max(crossing.reach);
2875                paves.entry(contact.node).or_default().push(Pave {
2876                    t: crossing.on_a,
2877                    honesty,
2878                });
2879                let on_b = onto_range(crossing.on_b, &e.curve, e.crange, tol);
2880                if on_b > e.crange.0 + tol.parametric() && on_b < e.crange.1 - tol.parametric() {
2881                    paves
2882                        .entry(e.node)
2883                        .or_default()
2884                        .push(Pave { t: on_b, honesty });
2885                }
2886            }
2887            // A span of the contact running along a target boundary edge
2888            // splits nothing: it is already boundary on both sides
2889            // (identically stacked boxes are all such spans), and duplicating
2890            // it as a strand would cancel the boundary it copies.
2891            // The intersector answers coincidence in closed form for the
2892            // analytic pairs only; a fitted curve lying on an exact one
2893            // (a marched sphere's rim on a wedge cap's arc) comes back as
2894            // crossings or nothing. That span is measured instead: the
2895            // stretch of the contact within the pair's honesty of the edge.
2896            // An analytic overlap survives only where it lands inside the
2897            // contact's own window; a trimmed circle written a turn up
2898            // clips to nothing, and is then measured like a fitted one.
2899            let survives = |overlap: &ogeom_intersect::Overlap| -> bool {
2900                let (lo, hi) = if overlap.on_a.0 <= overlap.on_a.1 {
2901                    overlap.on_a
2902                } else {
2903                    (overlap.on_a.1, overlap.on_a.0)
2904                };
2905                let carried = if contact.curve.is_periodic() {
2906                    let (dlo, dhi) = contact.curve.domain();
2907                    let period = dhi - dlo;
2908                    let turn = ((contact.crange.0 - lo) / period).floor();
2909                    [turn, turn + 1.0, turn - 1.0]
2910                        .into_iter()
2911                        .map(|k| (k.mul_add(period, lo), k.mul_add(period, hi)))
2912                        .map(|(a, b)| b.min(contact.crange.1) - a.max(contact.crange.0))
2913                        .fold(f64::NEG_INFINITY, f64::max)
2914                } else {
2915                    hi.min(contact.crange.1) - lo.max(contact.crange.0)
2916                };
2917                carried > tol.parametric()
2918            };
2919            // The closed-form overlap is between the two *curves*; the
2920            // stretch that is boundary is what the *edge* covers of it. A
2921            // box cut from an L-bracket flush with the bracket's wall puts
2922            // the box's wall-side edge on the line of the end face's own
2923            // edge along the wall, a length below it: read as along that
2924            // edge over the whole curve, the strip's side was never paved
2925            // and the end face kept the strip. Clipped through the
2926            // overlap's own correspondence so the carry below stays affine.
2927            let clipped: Vec<ogeom_intersect::Overlap> = found
2928                .overlaps
2929                .iter()
2930                .filter_map(|overlap| {
2931                    let (lo, hi) = overlap_within(overlap, e.crange, &contact.curve, tol)?;
2932                    let span = overlap.on_a.1 - overlap.on_a.0;
2933                    if span.abs() <= f64::MIN_POSITIVE {
2934                        return None;
2935                    }
2936                    let to_b = |t: f64| {
2937                        overlap.on_b.0
2938                            + (overlap.on_b.1 - overlap.on_b.0) * (t - overlap.on_a.0) / span
2939                    };
2940                    Some(ogeom_intersect::Overlap {
2941                        on_a: (lo, hi),
2942                        on_b: (to_b(lo), to_b(hi)),
2943                    })
2944                })
2945                .collect();
2946            // A line and a curved conic share no stretch, however loosely
2947            // either is held: a straight edge tangent to an arc stays within
2948            // the measuring width of it for a short run either side of the
2949            // touch, and read as a span there it splits the arc at a vertex
2950            // the face across the arc never has.
2951            let conic = |c: &Curve| {
2952                matches!(
2953                    c,
2954                    Curve::Circle(_) | Curve::Ellipse(_) | Curve::Hyperbola(_) | Curve::Parabola(_)
2955                )
2956            };
2957            let straight_on_conic = (matches!(contact.curve, Curve::Line(_)) && conic(&e.curve))
2958                || (conic(&contact.curve) && matches!(e.curve, Curve::Line(_)));
2959            let measured: Vec<ogeom_intersect::Overlap> =
2960                if clipped.iter().any(survives) || straight_on_conic {
2961                    Vec::new()
2962                } else {
2963                    measured_overlaps(&contact.curve, contact.crange, contact.tolerance, e, tol)?
2964                };
2965            for overlap in clipped.iter().chain(measured.iter()) {
2966                let ordered = |r: (f64, f64)| if r.0 <= r.1 { r } else { (r.1, r.0) };
2967                let (lo, hi) = ordered(overlap.on_a);
2968                // The overlap is between the two *curves*; what interferes is
2969                // the stretch both *edges* actually cover. A hole's arc and
2970                // the disc that fills it lie on one circle, so the curves
2971                // overlap over the whole turn while the arc covers three
2972                // quarters of it), and paving at the turn's ends says nothing,
2973                // where paving at the arc's ends is exactly the split the
2974                // other side needs to sew against.
2975                // A periodic curve's overlap is answered on its base turn;
2976                // an edge written a turn up (a wedge cap's arc at 2π..2.5π)
2977                // covers it only once the turn is carried across.
2978                let (lo, hi) = if contact.curve.is_periodic() {
2979                    let (dlo, dhi) = contact.curve.domain();
2980                    let period = dhi - dlo;
2981                    let turn = ((contact.crange.0 - lo) / period).floor();
2982                    let mut best = (lo, hi);
2983                    let mut best_span = f64::NEG_INFINITY;
2984                    for k in [turn, turn + 1.0, turn - 1.0] {
2985                        let cand = (k.mul_add(period, lo), k.mul_add(period, hi));
2986                        let span = cand.1.min(contact.crange.1) - cand.0.max(contact.crange.0);
2987                        if span > best_span {
2988                            best_span = span;
2989                            best = cand;
2990                        }
2991                    }
2992                    best
2993                } else {
2994                    (lo, hi)
2995                };
2996                let (lo, hi) = (lo.max(contact.crange.0), hi.min(contact.crange.1));
2997                if hi - lo <= tol.parametric() {
2998                    continue;
2999                }
3000                for t in [lo, hi] {
3001                    paves.entry(contact.node).or_default().push(Pave {
3002                        t,
3003                        honesty: contact.tolerance,
3004                    });
3005                }
3006                if *DEBUG_WIRE {
3007                    eprintln!(
3008                        "CONTACT c{ci} along edge {} over ({lo:.6}, {hi:.6}) of {:?}: {:?} .. {:?}",
3009                        e.node.index(),
3010                        contact.crange,
3011                        contact.curve.point_at(lo, tol).ok(),
3012                        contact.curve.point_at(hi, tol).ok()
3013                    );
3014                }
3015                contact_along[ci].push((lo, hi));
3016                // The *target* edge splits where the shared stretch ends,
3017                // exactly as the contact does. Without this, the face across
3018                // the overlap keeps one long boundary edge where its new
3019                // neighbours carry two short ones, and sew (which matches
3020                // edges whole) can pair it with neither. The clamped ends are
3021                // carried across by the correspondence the overlap itself
3022                // states, which is affine over the shared stretch.
3023                let span = overlap.on_a.1 - overlap.on_a.0;
3024                let carry = |t: f64| -> f64 {
3025                    if span.abs() <= f64::MIN_POSITIVE {
3026                        overlap.on_b.0
3027                    } else {
3028                        overlap.on_b.0
3029                            + (overlap.on_b.1 - overlap.on_b.0) * (t - overlap.on_a.0) / span
3030                    }
3031                };
3032                let (mut tlo, mut thi) = (carry(lo), carry(hi));
3033                if tlo > thi {
3034                    core::mem::swap(&mut tlo, &mut thi);
3035                }
3036                let target_domain = e.curve.domain();
3037                let periodic = e.curve.is_periodic();
3038                for t in [tlo, thi] {
3039                    // A correspondence across a full turn can run the
3040                    // parameter past the domain; the pave belongs where the
3041                    // edge actually is.
3042                    let t = if periodic { fold(t, target_domain) } else { t };
3043                    if t > e.crange.0 + tol.parametric() && t < e.crange.1 - tol.parametric() {
3044                        paves.entry(e.node).or_default().push(Pave {
3045                            t,
3046                            honesty: contact.tolerance,
3047                        });
3048                    }
3049                }
3050            }
3051        }
3052    }
3053
3054    let mut same_a: Vec<Vec<usize>> = vec![Vec::new(); ga.faces.len()];
3055    let mut same_b: Vec<Vec<usize>> = vec![Vec::new(); gb.faces.len()];
3056    for (ia, ib) in same_pairs {
3057        same_a[ia].push(ib);
3058        same_b[ib].push(ia);
3059    }
3060    Ok((
3061        sections,
3062        pieces,
3063        contacts,
3064        tangents,
3065        contact_along,
3066        paves,
3067        same_a,
3068        same_b,
3069        hug_junctions,
3070    ))
3071}
3072
3073/// An exact section cut at the chart degeneracies it runs through, each
3074/// piece carrying exact pcurves on both faces.
3075///
3076/// The degeneracies are not guessed from the surfaces: they are the faces'
3077/// own *pole edges*, the degenerate edges the topology already carries, so a
3078/// face whose chart has a pole says so and one that has none costs nothing.
3079///
3080/// `None` means the split is no use here (no degeneracy on the curve, or a
3081/// piece whose projection still has no closed form), and the caller falls
3082/// back to marching, which is the honest answer rather than a fitted pcurve
3083/// pretending to be exact.
3084fn split_at_degeneracies(
3085    curve: &Curve,
3086    fa: &GFace,
3087    fb: &GFace,
3088    tol: Tolerances,
3089) -> OgeomResult<Option<Vec<(Curve, PlanarCurve, PlanarCurve)>>> {
3090    let mut stops: Vec<f64> = Vec::new();
3091    let domain = curve.domain();
3092    // The split points are the *surfaces'* chart degeneracies, not merely the
3093    // faces' pole edges: a meridian section runs through both of a sphere's
3094    // poles, and a face that owns only the north one still cannot chart an
3095    // arc that wraps through the south. The surface knows where its chart
3096    // collapses whether or not the face's trim reaches there.
3097    let mut candidates: Vec<Point> = Vec::new();
3098    for face in [fa, fb] {
3099        for pole in &face.poles {
3100            candidates.push(pole.point);
3101        }
3102        match &face.surface {
3103            SurfaceGeometry::Sphere(s) => {
3104                let sphere = s.sphere();
3105                let axis = sphere.frame().z().vector();
3106                candidates.push(sphere.centre() + axis * sphere.radius());
3107                candidates.push(sphere.centre() - axis * sphere.radius());
3108            }
3109            SurfaceGeometry::Cone(c) => {
3110                candidates.push(c.cone().apex());
3111            }
3112            _ => {}
3113        }
3114    }
3115    for point in candidates {
3116        let found = ogeom_algo::project_on_curve(curve, point, 256, tol)?;
3117        if found.distance <= tol.confusion() {
3118            stops.push(found.parameter);
3119        }
3120    }
3121    if stops.is_empty() {
3122        return Ok(None);
3123    }
3124    stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
3125    stops.dedup_by(|a, b| (*a - *b).abs() <= tol.parametric());
3126
3127    // A closed curve is cut into the arcs between successive stops, the last
3128    // wrapping past the domain end; an open one keeps its own ends as stops.
3129    let closed = curve.is_closed(tol) || curve.is_periodic();
3130    let mut arcs: Vec<(f64, f64)> = Vec::new();
3131    if closed {
3132        if stops.len() < 2 {
3133            // One stop on a closed curve leaves one arc, from the stop right
3134            // round to itself, which still has the pole at both ends.
3135            let period = domain.1 - domain.0;
3136            arcs.push((stops[0], stops[0] + period));
3137        } else {
3138            let period = domain.1 - domain.0;
3139            for i in 0..stops.len() {
3140                let lo = stops[i];
3141                let hi = if i + 1 < stops.len() {
3142                    stops[i + 1]
3143                } else {
3144                    stops[0] + period
3145                };
3146                arcs.push((lo, hi));
3147            }
3148        }
3149    } else {
3150        let mut cuts = vec![domain.0];
3151        cuts.extend(
3152            stops
3153                .iter()
3154                .copied()
3155                .filter(|t| *t > domain.0 + tol.parametric() && *t < domain.1 - tol.parametric()),
3156        );
3157        cuts.push(domain.1);
3158        for pair in cuts.windows(2) {
3159            arcs.push((pair[0], pair[1]));
3160        }
3161    }
3162
3163    let mut out = Vec::with_capacity(arcs.len());
3164    for (lo, hi) in arcs {
3165        if hi - lo <= tol.parametric() {
3166            continue;
3167        }
3168        let (Some(pa), Some(pb)) = (
3169            ogeom_intersect::exact_pcurve_over(curve, (lo, hi), &fa.surface, tol),
3170            ogeom_intersect::exact_pcurve_over(curve, (lo, hi), &fb.surface, tol),
3171        ) else {
3172            return Ok(None);
3173        };
3174        let Ok(piece) = ogeom_geom::TrimmedCurve::new(curve.clone(), lo, hi, tol) else {
3175            return Ok(None);
3176        };
3177        out.push((piece.into(), pa, pb));
3178    }
3179    if out.is_empty() {
3180        return Ok(None);
3181    }
3182    Ok(Some(out))
3183}
3184
3185/// A surface narrowed to the reach of a bound, for the marcher's benefit.
3186///
3187/// Seeding samples a surface's *parameter box*, so a plane stored over
3188/// ±10^9 (which is how an unbounded carrier reaches this code) is sampled
3189/// at a spacing a hundred million times anything it could be meeting, and
3190/// no seed ever lands near the curve. Narrowing to where the two faces
3191/// actually are is what lets the seeding see it. The chart is untouched:
3192/// only the window the marcher walks changes, so pcurves fitted in it mean
3193/// the same thing on the surface as stored.
3194fn windowed_to(surface: &SurfaceGeometry, bound: &ogeom_math::Aabb) -> SurfaceGeometry {
3195    let corners = bound.corners();
3196    if corners.is_empty() {
3197        return surface.clone();
3198    }
3199    match surface {
3200        SurfaceGeometry::Plane(p) => {
3201            let frame = p.plane().frame();
3202            let (mut u0, mut u1, mut v0, mut v1) = (
3203                f64::INFINITY,
3204                f64::NEG_INFINITY,
3205                f64::INFINITY,
3206                f64::NEG_INFINITY,
3207            );
3208            for c in &corners {
3209                let local = frame.to_local(*c);
3210                u0 = u0.min(local.x);
3211                u1 = u1.max(local.x);
3212                v0 = v0.min(local.y);
3213                v1 = v1.max(local.y);
3214            }
3215            let margin = ((u1 - u0) + (v1 - v0)).mul_add(0.25, 1.0);
3216            let (want_u, want_v) = ((u0 - margin, u1 + margin), (v0 - margin, v1 + margin));
3217            let (have_u, have_v) = ogeom_geom::Surface::domain(p);
3218            if have_u.1 - have_u.0 <= want_u.1 - want_u.0
3219                && have_v.1 - have_v.0 <= want_v.1 - want_v.0
3220            {
3221                return surface.clone();
3222            }
3223            ogeom_geom::PlaneSurface::over(p.plane(), want_u, want_v)
3224                .map_or_else(|_| surface.clone(), Into::into)
3225        }
3226        SurfaceGeometry::Cylinder(c) => {
3227            let frame = c.cylinder().frame();
3228            let (mut h0, mut h1) = (f64::INFINITY, f64::NEG_INFINITY);
3229            for corner in &corners {
3230                let h = (*corner - frame.origin()).dot(frame.z().vector());
3231                h0 = h0.min(h);
3232                h1 = h1.max(h);
3233            }
3234            let margin = (h1 - h0).mul_add(0.25, 1.0);
3235            let want = (h0 - margin, h1 + margin);
3236            let have = ogeom_geom::Surface::domain(c).1;
3237            if have.1 - have.0 <= want.1 - want.0 {
3238                return surface.clone();
3239            }
3240            ogeom_geom::CylinderSurface::new(c.cylinder(), want)
3241                .map_or_else(|_| surface.clone(), Into::into)
3242        }
3243        other => other.clone(),
3244    }
3245}
3246
3247/// March a pair whose exact section has no closed-form pcurve.
3248fn march_pair(
3249    a: &SurfaceGeometry,
3250    b: &SurfaceGeometry,
3251    options: &ogeom_intersect::IntersectOptions,
3252    tol: Tolerances,
3253) -> OgeomResult<Vec<ogeom_intersect::IntersectionCurve>> {
3254    use ogeom_intersect::{approximate_branch, branches};
3255    let traced = branches(a, b, options.marching, tol)?;
3256    let mut out = Vec::new();
3257    for branch in &traced {
3258        out.push(approximate_branch(a, b, branch, options.tolerance, tol)?);
3259    }
3260    if out.is_empty() {
3261        // No branch found is two different stories. Surfaces that measurably
3262        // stand apart over their stated extents (a blend's cylinder and a
3263        // far-off bevel plane whose ellipse of intersection lies beyond
3264        // both) simply do not interact, and an empty section is the true
3265        // answer. Only a pair that comes close and still resolves nothing
3266        // is beyond the intersector.
3267        if surfaces_stand_apart(a, b, tol) {
3268            return Ok(out);
3269        }
3270        ogeom_bail!(
3271            NotDone,
3272            "an exact section has no closed-form pcurve and marching resolved \
3273             no branch; the configuration is beyond the intersector's current \
3274             reach"
3275        );
3276    }
3277    Ok(out)
3278}
3279
3280/// Whether two surfaces measurably keep their distance over their stated
3281/// extents.
3282///
3283/// A conservative grid measurement: sample the smaller-extent surface and
3284/// project each sample onto the other; apart means every sample clears a
3285/// margin scaled to the extents. Surfaces with unbounded or enormous stated
3286/// domains (an imported plane's billion units) are never called apart this
3287/// way, because a grid over them samples nothing.
3288fn surfaces_stand_apart(a: &SurfaceGeometry, b: &SurfaceGeometry, tol: Tolerances) -> bool {
3289    use ogeom_geom::Surface as _;
3290    let extent_of = |s: &SurfaceGeometry| -> f64 {
3291        let ((ua, ub), (va, vb)) = s.domain();
3292        (ub - ua).abs().max((vb - va).abs())
3293    };
3294    let (sample, against) = if extent_of(a) <= extent_of(b) {
3295        (a, b)
3296    } else {
3297        (b, a)
3298    };
3299    let span = extent_of(sample);
3300    if !span.is_finite() || span > 1e4 {
3301        return false;
3302    }
3303    let ((ua, ub), (va, vb)) = sample.domain();
3304    const GRID: usize = 9;
3305    // One seeding grid over the far surface, asked a hundred times: the
3306    // same seeds and the same Newton the per-call projection would use, so
3307    // the verdict is bit-identical, at hundreds of evaluations instead of
3308    // tens of thousands for every genuine near-miss.
3309    let Ok(seeds) = ogeom_algo::SurfaceSeeds::over(against, 16, tol) else {
3310        return false;
3311    };
3312    let mut clearance = f64::INFINITY;
3313    for i in 0..=GRID {
3314        for j in 0..=GRID {
3315            #[allow(clippy::cast_precision_loss)]
3316            let u = ua + (ub - ua) * i as f64 / GRID as f64;
3317            #[allow(clippy::cast_precision_loss)]
3318            let v = va + (vb - va) * j as f64 / GRID as f64;
3319            let Ok(p) = sample.point_at(u, v, tol) else {
3320                return false;
3321            };
3322            let Ok(projection) = seeds.project(against, p, tol) else {
3323                return false;
3324            };
3325            clearance = clearance.min(projection.distance);
3326            if clearance <= tol.confusion() * 1e3 {
3327                return false;
3328            }
3329        }
3330    }
3331    clearance > tol.confusion() * 1e3
3332}
3333
3334/// Whether two surfaces are one surface wherever they overlap, measured.
3335///
3336/// The closed forms answer this for the pairs they know. Where there is no
3337/// closed form it does not stop being a fair question (two patches restated
3338/// from one plane are the same surface, and nothing in their control points
3339/// says so), but it stops being answerable exactly, so it is measured here
3340/// and only for the pairs the analytic layer has already declined.
3341///
3342/// Sampled on the *smaller* window, because the answer is about the region
3343/// the two share and a stated window is not that region: a plane's own
3344/// extends for a billion units either way, and a grid over it samples
3345/// nothing. A sample whose foot lands on the rim of the other is skipped
3346/// rather than counted against: the other patch simply does not reach that
3347/// far, and a distance measured to its rim is about the window, not the
3348/// surface.
3349///
3350/// One-sided by construction: a pair that crosses puts interior samples well
3351/// off the other, so it cannot pass, and a pair this cannot resolve marches
3352/// exactly as it did before.
3353fn surfaces_coincide(
3354    a: &SurfaceGeometry,
3355    b: &SurfaceGeometry,
3356    reach: f64,
3357    tol: Tolerances,
3358) -> bool {
3359    use ogeom_geom::Surface as _;
3360    /// Samples per direction over the window, and how many must land inside
3361    /// the other before agreement means anything.
3362    const GRID: usize = 6;
3363    const EVIDENCE: usize = 4;
3364
3365    let span = |s: &SurfaceGeometry| -> f64 {
3366        let ((ua, ub), (va, vb)) = s.domain();
3367        (ub - ua).abs().max((vb - va).abs())
3368    };
3369    let (sampled, against) = if span(a) <= span(b) { (a, b) } else { (b, a) };
3370    let ((ua, ub), (va, vb)) = sampled.domain();
3371    if !(ua.is_finite() && ub.is_finite() && va.is_finite() && vb.is_finite()) {
3372        return false;
3373    }
3374    let ((wu0, wu1), (wv0, wv1)) = against.domain();
3375    // A twentieth of the window in from each rim: enough that a foot the
3376    // search pinned to the rim is not read as one the surface truly reaches.
3377    let (mu, mv) = ((wu1 - wu0) * 0.05, (wv1 - wv0) * 0.05);
3378
3379    let mut evidence = 0_usize;
3380    for i in 0..=GRID {
3381        for j in 0..=GRID {
3382            #[allow(clippy::cast_precision_loss)]
3383            let u = ua + (ub - ua) * (i as f64 / GRID as f64);
3384            #[allow(clippy::cast_precision_loss)]
3385            let v = va + (vb - va) * (j as f64 / GRID as f64);
3386            let Ok(p) = sampled.point_at(u, v, tol) else {
3387                return false;
3388            };
3389            let Ok(foot) = ogeom_algo::project_on_surface(against, p, 16, tol) else {
3390                return false;
3391            };
3392            let (fu, fv) = foot.parameters;
3393            if fu <= wu0 + mu || fu >= wu1 - mu || fv <= wv0 + mv || fv >= wv1 - mv {
3394                continue;
3395            }
3396            if foot.distance > reach {
3397                return false;
3398            }
3399            evidence += 1;
3400        }
3401    }
3402    evidence >= EVIDENCE
3403}
3404
3405// --- the general fuse --------------------------------------------------------
3406
3407/// One piece of one argument face, classified against the other argument.
3408struct FacePiece {
3409    /// Which argument's face list, and which face.
3410    from_a: bool,
3411    face: usize,
3412    rings: Vec<Vec<Traversal<Tag>>>,
3413    /// The rings as chart polylines, for the coincidence pairing below.
3414    outlines: Vec<Vec<Point2>>,
3415    /// The interior point the state was decided at, in space.
3416    probe: Point,
3417    state: PieceState,
3418    /// Whether the *other* argument already contributes this same patch of
3419    /// this same surface.
3420    ///
3421    /// Two faces lying on one surface with their material on the same side
3422    /// bound the union and the intersection once between them, not twice,
3423    /// but "once" is a statement about the *patch*, not about which argument
3424    /// it came from. Where a tool's cap fills a hole its own bore left, the
3425    /// part has no piece there at all, and dropping the tool's by argument
3426    /// identity leaves the result with a hole in it. So the duplicate is
3427    /// identified by containment, and only a piece genuinely stood in for is
3428    /// dropped.
3429    covered: bool,
3430}
3431
3432struct GeneralFused {
3433    a: GSolid,
3434    b: GSolid,
3435    sections: Vec<SectionRec>,
3436    contacts: Vec<ContactRec>,
3437    /// Curves the two boundaries touch along without crossing. They take no
3438    /// part in the classification below (that is what tangency means) and
3439    /// are carried for the consumers that want the contact itself.
3440    tangents: Vec<TangentRec>,
3441    pieces: Vec<FacePiece>,
3442    /// Junctions several paves describe, each resolved once for every
3443    /// strand that ends in it.
3444    junctions: Vec<Junction>,
3445}
3446
3447/// One junction the paving found several times over.
3448///
3449/// A tolerant rail meeting a wedge's faces near one corner collects a
3450/// cluster of crossings inside its own stated radius: each face's section
3451/// stops at its own crossing with the rail, and the crossings sit a few
3452/// tenths of a micron to a few hundred apart along it. The rail's strands
3453/// split once per cluster, at its first pave; the sections' ends still name
3454/// their own crossings; and the two descriptions of the junction can sit
3455/// apart by the cluster's whole span, which no single strand's honesty
3456/// covers. So the junction is one vertex standing at the first pave, owning
3457/// the span the paves disagree by, and every strand end inside that span
3458/// names it, whichever pave its own trim stopped at.
3459#[derive(Debug, Clone, Copy)]
3460struct Junction {
3461    /// Where the cluster's first pave sits.
3462    at: Point,
3463    /// How far a strand end may sit from `at` and still be this junction:
3464    /// the cluster's span plus the rail's own reach.
3465    reach: f64,
3466    /// Whether `at` is an edge's own end vertex that a section tangent to
3467    /// the edge was welded onto: the section's chart image is bent onto it
3468    /// rather than left where the section stopped.
3469    onto_vertex: bool,
3470}
3471
3472/// Junctions whose balls overlap, merged transitively into one junction
3473/// each: at the members' centroid, reaching as far as the farthest member's
3474/// own reach extends from it.
3475fn merge_junctions(junctions: Vec<Junction>) -> Vec<Junction> {
3476    let n = junctions.len();
3477    let mut parent: Vec<usize> = (0..n).collect();
3478    fn root(parent: &mut [usize], mut i: usize) -> usize {
3479        while parent[i] != i {
3480            parent[i] = parent[parent[i]];
3481            i = parent[i];
3482        }
3483        i
3484    }
3485    // Swept along x: two balls overlap only if their centres are within
3486    // both reaches along every axis, so each junction is asked only of those
3487    // ahead of it by no more than its reach and the widest. The groups are
3488    // the same as asking every pair; a converted part carries tens of
3489    // thousands of junctions, and every pair was seconds.
3490    let widest = junctions.iter().map(|j| j.reach).fold(0.0_f64, f64::max);
3491    let mut order: Vec<usize> = (0..n).collect();
3492    order.sort_by(|&x, &y| junctions[x].at.x.total_cmp(&junctions[y].at.x));
3493    for (k, &i) in order.iter().enumerate() {
3494        let limit = junctions[i].at.x + junctions[i].reach + widest;
3495        for &j in &order[k + 1..] {
3496            if junctions[j].at.x > limit {
3497                break;
3498            }
3499            if junctions[i].at.distance(junctions[j].at) <= junctions[i].reach + junctions[j].reach
3500            {
3501                let (ri, rj) = (root(&mut parent, i), root(&mut parent, j));
3502                if ri != rj {
3503                    parent[rj] = ri;
3504                }
3505            }
3506        }
3507    }
3508    let mut groups: Vec<Vec<usize>> = Vec::new();
3509    let mut of_root: Vec<Option<usize>> = vec![None; n];
3510    for i in 0..n {
3511        let r = root(&mut parent, i);
3512        match of_root[r] {
3513            Some(g) => groups[g].push(i),
3514            None => {
3515                of_root[r] = Some(groups.len());
3516                groups.push(vec![i]);
3517            }
3518        }
3519    }
3520    // A group is one junction only while it stays a junction's size: a
3521    // chain of overlapping balls along an edge would otherwise merge into
3522    // one reaching the chain's length, and the rebuild would weld the edge
3523    // into a point. A group reaching beyond four of its widest member stays
3524    // as it was.
3525    let mut out = Vec::with_capacity(groups.len());
3526    for members in groups {
3527        if members.len() == 1 {
3528            out.push(junctions[members[0]]);
3529            continue;
3530        }
3531        let mut sum = ogeom_math::Vector::ZERO;
3532        for &m in &members {
3533            sum += junctions[m].at - Point::ORIGIN;
3534        }
3535        #[allow(clippy::cast_precision_loss)]
3536        let at = Point::ORIGIN + sum / (members.len() as f64);
3537        let reach = members
3538            .iter()
3539            .map(|&m| at.distance(junctions[m].at) + junctions[m].reach)
3540            .fold(0.0_f64, f64::max);
3541        let widest = members
3542            .iter()
3543            .map(|&m| junctions[m].reach)
3544            .fold(0.0_f64, f64::max);
3545        if reach > widest * 4.0 {
3546            out.extend(members.iter().map(|&m| junctions[m]));
3547        } else {
3548            out.push(Junction {
3549                at,
3550                reach,
3551                onto_vertex: members.iter().any(|&m| junctions[m].onto_vertex),
3552            });
3553        }
3554    }
3555    out
3556}
3557
3558/// The junctions the paves describe more than once, or more loosely than
3559/// the edge itself, per edge in face order.
3560fn pave_junctions(
3561    ga: &GSolid,
3562    gb: &GSolid,
3563    paves: &std::collections::HashMap<EdgeKey, Vec<Pave>>,
3564    tol: Tolerances,
3565) -> OgeomResult<Vec<Junction>> {
3566    let mut seen: std::collections::HashSet<EdgeKey> = std::collections::HashSet::new();
3567    let mut junctions = Vec::new();
3568    for e in ga
3569        .faces
3570        .iter()
3571        .chain(gb.faces.iter())
3572        .flat_map(|f| f.edges.iter())
3573    {
3574        if !seen.insert(e.node) {
3575            continue;
3576        }
3577        let Some(ts) = paves.get(&e.node) else {
3578            continue;
3579        };
3580        // A tolerant edge (a merged facet group's boundary, a fitted rail)
3581        // is met by a section up to its own radius off it, so the section's
3582        // end and the edge's split point are one junction that far apart,
3583        // on either side of the edge.
3584        let tolerant = e.tolerance > tol.confusion() * 1e2;
3585        let floor = if tolerant {
3586            e.tolerance * 2.0
3587        } else {
3588            e.tolerance
3589        }
3590        .max(tol.confusion() * 10.0);
3591        for cluster in cluster_paves(&e.curve, e.crange, e.tolerance, ts, tol)? {
3592            if cluster.members > 1 || cluster.honesty > tol.confusion() * 1e2 || tolerant {
3593                junctions.push(Junction {
3594                    at: cluster.at,
3595                    reach: cluster.span + cluster.honesty.max(floor),
3596                    onto_vertex: false,
3597                });
3598            }
3599        }
3600    }
3601    Ok(junctions)
3602}
3603
3604/// The face's outward normal at a chart point: the surface's, flipped when
3605/// the face presents its other side.
3606fn outward_normal(face: &GFace, at: Point2, tol: Tolerances) -> OgeomResult<ogeom_math::Vector> {
3607    let n = face.surface.normal_at(at.x, at.y, tol)?.vector();
3608    Ok(
3609        if face.face.orientation() == ogeom_topo::Orientation::Reversed {
3610            -n
3611        } else {
3612            n
3613        },
3614    )
3615}
3616
3617/// The chart point of a world point lying on a planar face, if it lands
3618/// inside the face's trim.
3619/// How far apart two of a face's edges may honestly end in its chart: the
3620/// loosest edge's tolerance, or the hug width where an edge is a fitted
3621/// section that once ran along another before parting from it.
3622fn outline_snap(face: &GFace, tol: Tolerances) -> f64 {
3623    face.edges
3624        .iter()
3625        .fold(tol.confusion() * 1e2, |acc, e| acc.max(e.tolerance * 2.0))
3626        .max(
3627            if face.edges.iter().any(|e| e.tolerance > tol.confusion()) {
3628                tol.confusion() * 1e3
3629            } else {
3630                0.0
3631            },
3632        )
3633}
3634
3635/// Close the gaps between a face's outline polylines.
3636///
3637/// The edges are polylined one by one, and where two of them meet at a
3638/// vertex that owns some tolerance (a fitted section's end welded to the
3639/// edge it hugged, a few dozen microns off), their polylines stop that far
3640/// apart. A ray cast for containment slips through such a gap, and a probe
3641/// standing inside the face within a hug's width of the seam reads as
3642/// outside. Ends within `snap` of another polyline's end are made one
3643/// point, so the outline is closed exactly as the topology says it is.
3644fn weld_outline_ends(lines: &mut [Vec<Point2>], snap: f64) {
3645    if snap <= 0.0 {
3646        return;
3647    }
3648    // Every polyline end, as (line, is its last point, where).
3649    let ends: Vec<(usize, bool, Point2)> = lines
3650        .iter()
3651        .enumerate()
3652        .flat_map(|(i, line)| {
3653            let first = line.first().map(|p| (i, false, *p));
3654            let last = line.last().map(|p| (i, true, *p));
3655            first.into_iter().chain(last)
3656        })
3657        .collect();
3658    // Each end moves onto the nearest other end within reach that sorts
3659    // before it, so a matched pair lands on one point rather than trading
3660    // places.
3661    let mut moves: Vec<(usize, bool, Point2)> = Vec::new();
3662    for &(i, end_i, p) in &ends {
3663        let nearest = ends
3664            .iter()
3665            // Ends closer than the parametric snap already meet for every
3666            // purpose here, and moving one by rounding noise takes a probe
3667            // that stands exactly on a chart's seam off it.
3668            .filter(|&&(j, end_j, q)| {
3669                let d = p.distance(q);
3670                (j, end_j) < (i, end_i) && j != i && d > PARAM_SNAP && d <= snap
3671            })
3672            .min_by(|a, b| {
3673                p.distance(a.2)
3674                    .partial_cmp(&p.distance(b.2))
3675                    .unwrap_or(core::cmp::Ordering::Equal)
3676            });
3677        if let Some(&(_, _, q)) = nearest {
3678            moves.push((i, end_i, q));
3679        }
3680    }
3681    for (i, end_i, q) in moves {
3682        let slot = if end_i {
3683            lines[i].last_mut()
3684        } else {
3685            lines[i].first_mut()
3686        };
3687        if let Some(p) = slot {
3688            *p = q;
3689        }
3690    }
3691}
3692
3693fn chart_point_of(face: &GFace, p: Point, tol: Tolerances) -> Option<Point2> {
3694    // Closed-form inversion for the analytic surfaces: the same-domain
3695    // resolution asks "where does this probe sit in the partner's chart", and
3696    // the partner may be any surface a face melts along: a plane against a
3697    // plane, but equally a wall band against the cylinder it copies. The
3698    // reach check keeps the answer honest: a point off the surface has no
3699    // chart position, whatever the inversion returns.
3700    use ogeom_math::elementary;
3701    let reach = tol.confusion() * 10.0;
3702    let raw = match &face.surface {
3703        SurfaceGeometry::Plane(x) => {
3704            let local = x.plane().frame().to_local(p);
3705            if local.z.abs() > reach {
3706                return None;
3707            }
3708            Point2::new(local.x, local.y)
3709        }
3710        SurfaceGeometry::Cylinder(x) => {
3711            let cylinder = x.cylinder();
3712            if cylinder.distance_to(p) > reach {
3713                return None;
3714            }
3715            let (u, v) = elementary::cylinder_parameters(&cylinder, p, tol).ok()?;
3716            Point2::new(u, v)
3717        }
3718        SurfaceGeometry::Cone(x) => {
3719            let cone = x.cone();
3720            if cone.distance_to(p) > reach {
3721                return None;
3722            }
3723            let (u, v) = elementary::cone_parameters(&cone, p, tol).ok()?;
3724            Point2::new(u, v)
3725        }
3726        SurfaceGeometry::Sphere(x) => {
3727            let sphere = x.sphere();
3728            if sphere.distance_to(p) > reach {
3729                return None;
3730            }
3731            let (u, v) = elementary::sphere_parameters(&sphere, p, tol).ok()?;
3732            Point2::new(u, v)
3733        }
3734        SurfaceGeometry::Torus(x) => {
3735            let torus = x.torus();
3736            if torus.distance_to(p) > reach {
3737                return None;
3738            }
3739            let (u, v) = elementary::torus_parameters(&torus, p, tol).ok()?;
3740            Point2::new(u, v)
3741        }
3742        // No closed form (a fitted patch, a swept or revolved surface)
3743        // inverts by projection, held to the same reach: a blend's leg on
3744        // a spline host is a partner like any other.
3745        other => {
3746            let foot = ogeom_algo::project_on_surface(other, p, 24, tol).ok()?;
3747            if foot.distance > reach {
3748                return None;
3749            }
3750            Point2::new(foot.parameters.0, foot.parameters.1)
3751        }
3752    };
3753    let at = fold_point_into_chart(raw, &face.surface);
3754    let mut lines: Vec<Vec<Point2>> = Vec::new();
3755    for e in &face.edges {
3756        lines.push(
3757            pcurve_polyline(&e.pcurve, e.prange, e.crange, e.crange, &face.surface, tol).ok()?,
3758        );
3759        // A seam bounds the chart twice (once per column), and a trim test
3760        // that sees only one side reads half the band as outside.
3761        if let Some((other, orange)) = &e.other_side {
3762            lines.push(
3763                pcurve_polyline(other, *orange, e.crange, e.crange, &face.surface, tol).ok()?,
3764            );
3765        }
3766    }
3767    weld_outline_ends(&mut lines, outline_snap(face, tol));
3768    let borrowed: Vec<&[Point2]> = lines.iter().map(Vec::as_slice).collect();
3769    // The face's boundary polylines are unwrapped (a winding ring may span
3770    // any one period's window, not necessarily the chart's canonical one,
3771    // and a wire chained onto one branch of the chart may sit whole periods
3772    // away from it), so the probe is carried to the outline's own branch
3773    // first, and asked at the neighbouring images as well.
3774    let mut shifts = vec![0.0];
3775    if face.surface.is_periodic_u() {
3776        let ((ua, ub), _) = face.surface.domain();
3777        if ub > ua {
3778            let period = ub - ua;
3779            let (sum, count) = lines
3780                .iter()
3781                .flatten()
3782                .fold((0.0_f64, 0_usize), |(s, n), q| (s + q.x, n + 1));
3783            #[allow(clippy::cast_precision_loss)]
3784            let centre = if count > 0 { sum / count as f64 } else { at.x };
3785            let home = ((centre - at.x) / period).round() * period;
3786            shifts = vec![home, home + period, home - period];
3787        }
3788    }
3789    for shift in shifts {
3790        let shifted = Point2::new(at.x + shift, at.y);
3791        if inside_many(&borrowed, shifted) {
3792            return Some(shifted);
3793        }
3794    }
3795    None
3796}
3797
3798/// Whether two curves meet tangentially at a crossing: the parallel-noise
3799/// gate for pave placement. One degree is far below any deliberate crossing
3800/// and far above the smear a tangency leaves in the general intersector.
3801fn tangential(a: &Curve, ta: f64, b: &Curve, tb: f64, tol: Tolerances) -> OgeomResult<bool> {
3802    let da = a.d1_at(ta, tol)?;
3803    let db = b.d1_at(tb, tol)?;
3804    let (ma, mb) = (da.magnitude(), db.magnitude());
3805    if ma <= tol.confusion() || mb <= tol.confusion() {
3806        return Ok(true);
3807    }
3808    Ok(da.cross(db).magnitude() / (ma * mb) < 2e-2)
3809}
3810
3811/// The chart image of an exact curve on a surface, fitted at the curve's
3812/// own parameters from its points inverted on the surface, with the
3813/// distance the image honestly sits off the curve, or nothing, when the
3814/// fit misses its budget by more than a chart's swing beside a pole allows.
3815fn fitted_image(
3816    curve: &Curve,
3817    surface: &SurfaceGeometry,
3818    budget: f64,
3819    tol: Tolerances,
3820) -> OgeomResult<Option<(PlanarCurve, f64)>> {
3821    use ogeom_geom::Surface as _;
3822    const SAMPLES: usize = 96;
3823    let (lo, hi) = curve.domain();
3824    if !hi.is_finite() || !lo.is_finite() || hi <= lo {
3825        return Ok(None);
3826    }
3827    let ((ua, ub), (va, vb)) = surface.domain();
3828    let periods = (
3829        if surface.is_periodic_u() {
3830            ub - ua
3831        } else {
3832            0.0
3833        },
3834        if surface.is_periodic_v() {
3835            vb - va
3836        } else {
3837            0.0
3838        },
3839    );
3840    // The image sampled where it moves: beside a pole a circle's image
3841    // swings half a turn within a hair of arc, and a uniform sampling puts
3842    // one point in the swing. Between any two samples whose images sit
3843    // further apart than a small step of the chart a sample is added, until
3844    // the image is walked at that step or the budget of samples is spent.
3845    let invert = |t: f64, guess: Option<(f64, f64)>| -> OgeomResult<Option<((f64, f64), f64)>> {
3846        let p = curve.point_at(t, tol)?;
3847        // A plane's image is its frame's own coordinates, exact and
3848        // unbounded: the face's chart window is a trim, not a limit on
3849        // where the curve may be read, and a projection clamped to it puts
3850        // the circle's far side off the surface by the window's shortfall.
3851        if let SurfaceGeometry::Plane(plane) = surface {
3852            let local = plane.plane().frame().to_local(p);
3853            let back = plane
3854                .plane()
3855                .frame()
3856                .to_world(ogeom_math::Point::new(local.x, local.y, 0.0));
3857            return Ok(Some(((local.x, local.y), back.distance(p))));
3858        }
3859        // A sphere's image is its frame's longitude and latitude, exact
3860        // where the projection's Newton cannot refine beside a pole and
3861        // hands back a coarse scan sample instead; the reading is checked
3862        // round the trip and left to the projection if the chart's
3863        // convention is not this one.
3864        if let SurfaceGeometry::Sphere(ball) = surface {
3865            let frame = ball.sphere().frame();
3866            let local = frame.to_local(p);
3867            let flat = local.x.hypot(local.y);
3868            // Kept a hair inside the poles: a spline fitted through points
3869            // on the pole itself overshoots it between them, and the chart
3870            // refuses a latitude past its end.
3871            let half = core::f64::consts::FRAC_PI_2 - 1e-6;
3872            let v = local.z.atan2(flat).clamp(-half, half);
3873            let u = local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU);
3874            let back = surface.point_at(u, v, tol)?;
3875            if back.distance(p) <= budget.max(tol.confusion() * 1e2) {
3876                return Ok(Some(((u, v), back.distance(p))));
3877            }
3878        }
3879        let mut foot = match guess {
3880            Some(g) => ogeom_algo::project_on_surface_from(surface, p, g, tol)?,
3881            None => ogeom_algo::project_on_surface(surface, p, 24, tol)?,
3882        };
3883        if foot.distance > budget.max(tol.confusion() * 1e2) && guess.is_some() {
3884            foot = ogeom_algo::project_on_surface(surface, p, 48, tol)?;
3885        }
3886        if foot.distance > budget.max(tol.confusion() * 1e2) {
3887            if *DEBUG_WIRE {
3888                eprintln!(
3889                    "IMAGE FIT: a point sits {:.2e} off the surface; no image",
3890                    foot.distance
3891                );
3892            }
3893            return Ok(None);
3894        }
3895        Ok(Some((foot.parameters, foot.distance)))
3896    };
3897    let unwrap = |uv: (f64, f64), before: Option<Point2>| -> Point2 {
3898        let mut uv = Point2::new(uv.0, uv.1);
3899        if let Some(prev) = before {
3900            for (coord, period, before) in [
3901                (&mut uv.x, periods.0, prev.x),
3902                (&mut uv.y, periods.1, prev.y),
3903            ] {
3904                if period > 0.0 {
3905                    while *coord - before > period / 2.0 {
3906                        *coord -= period;
3907                    }
3908                    while before - *coord > period / 2.0 {
3909                        *coord += period;
3910                    }
3911                }
3912            }
3913        }
3914        uv
3915    };
3916    let mut samples: Vec<(f64, Point2)> = Vec::with_capacity(SAMPLES + 1);
3917    let mut guess: Option<(f64, f64)> = None;
3918    for k in 0..=SAMPLES {
3919        #[allow(clippy::cast_precision_loss)]
3920        let t = lo + (hi - lo) * (k as f64) / (SAMPLES as f64);
3921        let Some((uv, _)) = invert(t, guess)? else {
3922            return Ok(None);
3923        };
3924        guess = Some(uv);
3925        let before = samples.last().map(|(_, p)| *p);
3926        samples.push((t, unwrap(uv, before)));
3927    }
3928    let step = {
3929        let (su, sv) = (
3930            if periods.0 > 0.0 { periods.0 } else { ub - ua },
3931            if periods.1 > 0.0 { periods.1 } else { vb - va },
3932        );
3933        su.min(sv) * 0.01
3934    };
3935    const CAP: usize = 4096;
3936    let mut refined = true;
3937    while refined && samples.len() < CAP {
3938        refined = false;
3939        let mut next: Vec<(f64, Point2)> = Vec::with_capacity(samples.len() * 2);
3940        for pair in samples.windows(2) {
3941            let ((t0, p0), (t1, p1)) = (pair[0], pair[1]);
3942            next.push((t0, p0));
3943            if p0.distance(p1) > step && t1 - t0 > (hi - lo) * 1e-7 && next.len() < CAP {
3944                let tm = f64::midpoint(t0, t1);
3945                let Some((uv, _)) = invert(tm, Some((p0.x, p0.y)))? else {
3946                    return Ok(None);
3947                };
3948                next.push((tm, unwrap(uv, Some(p0))));
3949                refined = true;
3950            }
3951        }
3952        next.push(samples[samples.len() - 1]);
3953        samples = next;
3954    }
3955    let params: Vec<f64> = samples.iter().map(|(t, _)| *t).collect();
3956    let image: Vec<Point2> = samples.iter().map(|(_, p)| *p).collect();
3957    let fitted =
3958        ogeom_geom::fit::fit_points_2d_at(&params, &image, 3, tol.confusion() * 10.0, tol)?;
3959    let planar: PlanarCurve = fitted.curve.into();
3960    // The honest error is in space: the surface read through the image
3961    // against the curve itself, between the samples as well as at them.
3962    // Checked at the samples and between each pair of them: where the
3963    // image swings the samples are dense, and a uniform check would step
3964    // over the swing and read the fit as honest.
3965    // An image that leaves the chart between its samples (a spline
3966    // overshooting the pole it was fitted up to) is no image either.
3967    let mut off = 0.0_f64;
3968    let mut checks: Vec<f64> = Vec::with_capacity(params.len() * 2);
3969    for pair in params.windows(2) {
3970        checks.push(pair[0]);
3971        checks.push(f64::midpoint(pair[0], pair[1]));
3972    }
3973    if let Some(&t) = params.last() {
3974        checks.push(t);
3975    }
3976    for t in checks {
3977        let uv = planar.point_at(t, tol)?;
3978        let Ok(on_surface) = surface.point_at(uv.x, uv.y, tol) else {
3979            if *DEBUG_WIRE {
3980                eprintln!("IMAGE FIT: the image leaves the chart at {uv:?}; no image");
3981            }
3982            return Ok(None);
3983        };
3984        off = off.max(on_surface.distance(curve.point_at(t, tol)?));
3985    }
3986    if *DEBUG_WIRE {
3987        eprintln!(
3988            "IMAGE FIT: image off the curve by {off:.2e} against {:.2e} (fit error {:.2e} met {})",
3989            budget.max(tol.confusion() * 1e3),
3990            fitted.error,
3991            fitted.met
3992        );
3993    }
3994    if off > budget.max(tol.confusion() * 1e3) {
3995        return Ok(None);
3996    }
3997    Ok(Some((planar, off)))
3998}
3999
4000/// The distance from a point to a bounded edge curve, through a sampling
4001/// fine enough for the along-boundary question it answers.
4002fn distance_to_edge_curve(
4003    curve: &Curve,
4004    crange: (f64, f64),
4005    p: Point,
4006    tol: Tolerances,
4007) -> OgeomResult<f64> {
4008    // Coarse bracket, then two rounds of local refinement: the answer feeds
4009    // the hug filters, whose widths are fractions of a millimetre, and a
4010    // long arc's 48-segment polyline sags by more than that on its own.
4011    let scan = |lo: f64, hi: f64, steps: u32| -> OgeomResult<(f64, f64)> {
4012        let mut best = f64::INFINITY;
4013        let mut best_t = lo;
4014        let mut previous: Option<(f64, Point)> = None;
4015        for i in 0..=steps {
4016            let t = lo + (hi - lo) * f64::from(i) / f64::from(steps);
4017            let at = curve.point_at(t, tol)?;
4018            if let Some((t0, last)) = previous {
4019                let d = at - last;
4020                let len2 = d.dot(d);
4021                let s = if len2 > 0.0 {
4022                    ((p - last).dot(d) / len2).clamp(0.0, 1.0)
4023                } else {
4024                    0.0
4025                };
4026                let dist = p.distance(last + d * s);
4027                if dist < best {
4028                    best = dist;
4029                    best_t = (t - t0).mul_add(s, t0);
4030                }
4031            }
4032            previous = Some((t, at));
4033        }
4034        Ok((best, best_t))
4035    };
4036    let span = crange.1 - crange.0;
4037    let (_, t1) = scan(crange.0, crange.1, 48)?;
4038    let step = span / 48.0;
4039    let (_, t2) = scan((t1 - step).max(crange.0), (t1 + step).min(crange.1), 16)?;
4040    let fine = span / (48.0 * 8.0);
4041    let (best, _) = scan((t2 - fine).max(crange.0), (t2 + fine).min(crange.1), 16)?;
4042    Ok(best)
4043}
4044
4045/// Pair up the pieces two coincident faces contribute for the same patch of
4046/// one surface, and mark the second argument's copy as stood in for.
4047///
4048/// The substitution is by *region*, not by argument: a piece of `b` is a
4049/// duplicate exactly where a piece of `a`, on the same side and on a surface
4050/// they share, already covers the point `b`'s piece was classified at. Where
4051/// `a` has nothing there (a bore refilled by the cylinder that cut it, whose
4052/// caps fill holes the part no longer has faces for), nothing stands in, and
4053/// `b`'s piece is the only description of that patch there is.
4054fn mark_covered_coincidences(ga: &GSolid, pieces: &mut [FacePiece], tol: Tolerances) {
4055    // Which pieces of the first argument stand on a shared surface.
4056    let from_a: Vec<(usize, usize)> = pieces
4057        .iter()
4058        .enumerate()
4059        .filter(|(_, p)| p.from_a && p.state == PieceState::OnAligned)
4060        .map(|(i, p)| (i, p.face))
4061        .collect();
4062    if from_a.is_empty() {
4063        return;
4064    }
4065    let mut covered: Vec<usize> = Vec::new();
4066    for (index, piece) in pieces.iter().enumerate() {
4067        if piece.from_a || piece.state != PieceState::OnAligned {
4068            continue;
4069        }
4070        for &(other, face_a) in &from_a {
4071            let host = &ga.faces[face_a];
4072            // The point `b`'s piece stands at, read in `a`'s face's chart. A
4073            // point that is not on that surface at all has no chart position,
4074            // and `chart_point_of` says so.
4075            let Some(at) = chart_point_of(host, piece.probe, tol) else {
4076                continue;
4077            };
4078            // The host piece's rings, not its boundary strands: a ring is a
4079            // closed loop whose last point does not repeat its first, so the
4080            // test has to close it. Asked as though the rings were strands
4081            // that jointly close, the segment from the ring's end back to its
4082            // start goes uncounted, and a point the ring plainly encloses
4083            // comes back outside whenever that missing segment would have
4084            // been crossed.
4085            if inside_rings(&pieces[other].outlines, at) {
4086                covered.push(index);
4087                break;
4088            }
4089        }
4090    }
4091    for index in covered {
4092        pieces[index].covered = true;
4093    }
4094}
4095
4096/// The debug dumps, read once rather than once per face.
4097///
4098/// `env::var` takes a process-wide lock and allocates; the strand dump asked
4099/// it inside the per-face loop, where a large model asks thousands of times
4100/// to be told no.
4101/// The face-bound filter's audit: admit every pair, and name any the filter
4102/// would have dropped that then produces a record. A conservative filter is
4103/// a correctness precondition (a pair wrongly dropped is absorbed by the
4104/// empty-result fallback today, and would be a wrong solid if that fallback
4105/// ever came up empty too), and this is the check that makes the
4106/// precondition falsifiable. Costs one branch per pair when off.
4107static AUDIT_BOUNDS: std::sync::LazyLock<bool> =
4108    std::sync::LazyLock::new(|| std::env::var("OGEOM_BOOL_AUDIT_BOUNDS").is_ok());
4109static DEBUG_WIRE: std::sync::LazyLock<bool> =
4110    std::sync::LazyLock::new(|| std::env::var("OGEOM_DEBUG_WIRE").is_ok());
4111static DEBUG_STRANDS: std::sync::LazyLock<bool> =
4112    std::sync::LazyLock::new(|| std::env::var("OGEOM_DEBUG_STRANDS").is_ok());
4113static ARRANGE_DEBUG: std::sync::LazyLock<bool> =
4114    std::sync::LazyLock::new(|| std::env::var("OGEOM_ARRANGE_DEBUG").is_ok());
4115
4116/// The strict audit's verdict: the filtered fill and the unfiltered fill
4117/// keep the same material.
4118///
4119/// Pieces are compared as merged parameter intervals per section: the
4120/// unfiltered run sees more curves, and a dropped pair's dead section can
4121/// still cross a live one and split its pieces differently, so individual
4122/// piece boundaries are noise and the kept *union* is the signal. Sections
4123/// are keyed by their face pair and a sampled point, because indices differ
4124/// between the runs.
4125fn audit_fill_equivalence(
4126    filtered: (&[SectionRec], &[SectionPiece]),
4127    unfiltered: (&[SectionRec], &[SectionPiece]),
4128    tol: Tolerances,
4129) {
4130    type Key = (usize, usize, [i64; 3]);
4131    let key_of = |section: &SectionRec, tol: Tolerances| -> Key {
4132        let (lo, hi) = section.curve.domain();
4133        let p = section
4134            .curve
4135            .point_at(f64::midpoint(lo, hi), tol)
4136            .unwrap_or(ogeom_math::Point::ORIGIN);
4137        let grid = (tol.confusion() * 100.0).max(1e-9);
4138        #[allow(clippy::cast_possible_truncation)]
4139        let q = |x: f64| (x / grid).round() as i64;
4140        (section.face_a, section.face_b, [q(p.x), q(p.y), q(p.z)])
4141    };
4142    let merged = |sections: &[SectionRec], pieces: &[SectionPiece], tol: Tolerances| {
4143        let mut kept: std::collections::HashMap<Key, Vec<(f64, f64)>> =
4144            std::collections::HashMap::new();
4145        for piece in pieces {
4146            kept.entry(key_of(&sections[piece.section], tol))
4147                .or_default()
4148                .push(piece.range);
4149        }
4150        let slop = tol.parametric() * 10.0;
4151        for ranges in kept.values_mut() {
4152            ranges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
4153            let mut out: Vec<(f64, f64)> = Vec::with_capacity(ranges.len());
4154            for &(lo, hi) in ranges.iter() {
4155                match out.last_mut() {
4156                    Some(last) if lo <= last.1 + slop => last.1 = last.1.max(hi),
4157                    _ => out.push((lo, hi)),
4158                }
4159            }
4160            *ranges = out;
4161        }
4162        kept
4163    };
4164    let a = merged(filtered.0, filtered.1, tol);
4165    let b = merged(unfiltered.0, unfiltered.1, tol);
4166    let slop = tol.parametric() * 20.0;
4167    let matches = |x: &Vec<(f64, f64)>, y: &Vec<(f64, f64)>| {
4168        x.len() == y.len()
4169            && x.iter()
4170                .zip(y)
4171                .all(|(p, q)| (p.0 - q.0).abs() <= slop && (p.1 - q.1).abs() <= slop)
4172    };
4173    for (key, ranges) in &b {
4174        let held = a.get(key);
4175        assert!(
4176            held.is_some_and(|r| matches(r, ranges)),
4177            "strict bound-filter audit: the unfiltered fill keeps {ranges:?} \
4178             on section {key:?}, the filtered fill keeps {held:?}; the \
4179             filter drops material"
4180        );
4181    }
4182    for (key, ranges) in &a {
4183        assert!(
4184            b.contains_key(key),
4185            "strict bound-filter audit: the filtered fill keeps {ranges:?} \
4186             on section {key:?} the unfiltered fill never made"
4187        );
4188    }
4189}
4190
4191fn general_fuse(model: &Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<GeneralFused> {
4192    ogeom_core::progress::stage("boolean: gather");
4193    let ga = gather(model, a, tol)?;
4194    let gb = gather(model, b, tol)?;
4195    ogeom_core::progress::stage("boolean: intersect");
4196    let (sections, section_pieces, contacts, tangents, contact_along, paves, same_a, same_b, hugs) =
4197        fill(&ga, &gb, false, tol)?;
4198    let mut junctions = pave_junctions(&ga, &gb, &paves, tol)?;
4199    if *DEBUG_WIRE {
4200        for j in &junctions {
4201            eprintln!("JUNCTION from paves at {:?} reach {:.3e}", j.at, j.reach);
4202        }
4203        for j in &hugs {
4204            eprintln!("JUNCTION from hugs at {:?} reach {:.3e}", j.at, j.reach);
4205        }
4206    }
4207    junctions.extend(hugs);
4208    for face in ga.faces.iter().chain(gb.faces.iter()) {
4209        // An input vertex that owns a span (the corner an earlier boolean
4210        // welded three rims into, each ending a fraction of a micron from
4211        // the others) is a junction here as well: the rebuild's positional
4212        // weld reaches only a few honesties, and minted separately those
4213        // ends stand as three vertices with hairlines between them.
4214        for e in &face.edges {
4215            for (at, radius) in e.ends {
4216                if radius > tol.confusion() * 10.0 {
4217                    if *DEBUG_WIRE {
4218                        eprintln!("JUNCTION from vertex at {at:?} radius {radius:.3e}");
4219                    }
4220                    junctions.push(Junction {
4221                        at,
4222                        reach: radius,
4223                        onto_vertex: false,
4224                    });
4225                }
4226            }
4227        }
4228    }
4229    // Junctions whose balls overlap are one junction: two pave clusters a
4230    // fraction of a micron apart at a corner where rims meet, each owning
4231    // a span that reaches into the other's, would weld a strand end to
4232    // whichever covers it first, and the corner would stand as two
4233    // vertices with a hairline between them, kept on one face, dropped
4234    // on another. Merged, the corner is one vertex whose reach covers
4235    // every member's span, on every face alike.
4236    junctions = merge_junctions(junctions);
4237    if *DEBUG_WIRE {
4238        for j in &junctions {
4239            eprintln!("JUNCTION at {:?} reach {:.3e}", j.at, j.reach);
4240        }
4241        let kind = |sf: &SurfaceGeometry| match sf {
4242            SurfaceGeometry::Plane(_) => "plane",
4243            SurfaceGeometry::Cylinder(_) => "cyl",
4244            SurfaceGeometry::BSpline(_) => "bspline",
4245            _ => "other",
4246        };
4247        for (si, sec) in sections.iter().enumerate() {
4248            let fa = &ga.faces[sec.face_a];
4249            let fb = &gb.faces[sec.face_b];
4250            let d = sec.curve.domain();
4251            let mut worst_a: f64 = 0.0;
4252            let mut worst_b: f64 = 0.0;
4253            let mut len = 0.0;
4254            let mut prev: Option<Point> = None;
4255            for i in 0..=32 {
4256                let t = if i == 32 {
4257                    d.1
4258                } else {
4259                    d.0 + (d.1 - d.0) * f64::from(i) / 32.0
4260                };
4261                let at = sec.curve.point_at(t, tol)?;
4262                if let Some(p) = prev {
4263                    len += p.distance(at);
4264                }
4265                prev = Some(at);
4266                let near = |f: &GFace| -> OgeomResult<f64> {
4267                    let mut best = f64::INFINITY;
4268                    for e in &f.edges {
4269                        best = best.min(distance_to_edge_curve(&e.curve, e.crange, at, tol)?);
4270                    }
4271                    Ok(best)
4272                };
4273                worst_a = worst_a.max(near(fa)?);
4274                worst_b = worst_b.max(near(fb)?);
4275            }
4276            let pieces = section_pieces.iter().filter(|p| p.section == si).count();
4277            eprintln!(
4278                "SECTION s{si} a{}({}) x b{}({}) tol {:.2e} closed {} len {:.4} pieces {pieces} hug-dist a {:.3e} b {:.3e}",
4279                sec.face_a,
4280                kind(&fa.surface),
4281                sec.face_b,
4282                kind(&fb.surface),
4283                sec.tolerance,
4284                sec.closed,
4285                len,
4286                worst_a,
4287                worst_b
4288            );
4289        }
4290        eprintln!("TANGENTS {}  CONTACTS {}", tangents.len(), contacts.len());
4291    }
4292    // The strict audit: fill again with every pair admitted and demand the
4293    // same kept material. The production result above is always the
4294    // filtered run (under audit too), so the audit compares rather than
4295    // substitutes, and a filtered run *is* replayed exactly. Zero cost with
4296    // the variable unset.
4297    if *AUDIT_BOUNDS {
4298        let (audit_sections, audit_pieces, ..) = fill(&ga, &gb, true, tol)?;
4299        audit_fill_equivalence(
4300            (&sections, &section_pieces),
4301            (&audit_sections, &audit_pieces),
4302            tol,
4303        );
4304    }
4305
4306    ogeom_core::progress::stage("boolean: split");
4307    let mut pieces: Vec<FacePiece> = Vec::new();
4308    // Every face's strands, before any face is arranged: the dust decision
4309    // (a piece shorter than its face's snap collapses to one node) must
4310    // be one decision per shared edge piece, and a sub-piece of one edge
4311    // lies in several charts, each with its own snap and its own metric.
4312    // A band's rail hugging a wedge's cylinder for a quarter of a micron
4313    // is dust in the wall's chart and a strand in the cylinder's, and the
4314    // sew then finds the cylinder's piece used once.
4315    let strands_of = |from_a: bool,
4316                      fi: usize,
4317                      face: &GFace|
4318     -> OgeomResult<(Vec<Strand<Tag>>, f64)> {
4319        let mut strands: Vec<Strand<Tag>> = Vec::new();
4320        // How far apart the paves one junction stands for lie. A section
4321        // meeting the edge all but tangentially (its fitted end sliding
4322        // along it) paves it a few microns from where an exact section
4323        // crosses; the first pave speaks for both, and the other's strand
4324        // must still reach it.
4325        let mut spread = 0.0_f64;
4326        for (ei, e) in face.edges.iter().enumerate() {
4327            let mut stops = vec![e.crange.0];
4328            if let Some(ts) = paves.get(&e.node) {
4329                // Paves the edge itself cannot tell apart are one
4330                // junction, and the cluster's first pave speaks for it.
4331                for c in cluster_paves(&e.curve, e.crange, e.tolerance, ts, tol)? {
4332                    if c.members > 1 {
4333                        spread = spread.max(c.span + c.honesty);
4334                    }
4335                    stops.push(c.t);
4336                }
4337            }
4338            stops.push(e.crange.1);
4339            // A closed boundary edge (a cap's full circle) needs two
4340            // distinct endpoints per strand.
4341            let closed_edge = e
4342                .curve
4343                .point_at(e.crange.0, tol)?
4344                .distance(e.curve.point_at(e.crange.1, tol)?)
4345                <= tol.confusion();
4346            if closed_edge && stops.len() == 2 {
4347                stops.insert(1, f64::midpoint(e.crange.0, e.crange.1));
4348            }
4349            for pair in stops.windows(2) {
4350                let sub = (pair[0], pair[1]);
4351                if sub.1 - sub.0 <= tol.parametric() {
4352                    continue;
4353                }
4354                strands.push(Strand {
4355                    polyline: pcurve_polyline(
4356                        &e.pcurve,
4357                        e.prange,
4358                        e.crange,
4359                        sub,
4360                        &face.surface,
4361                        tol,
4362                    )?,
4363                    tag: Tag::Boundary {
4364                        edge: ei,
4365                        range: sub,
4366                    },
4367                    boundary: true,
4368                });
4369                if let Some((other_pc, orange)) = &e.other_side {
4370                    strands.push(Strand {
4371                        polyline: pcurve_polyline(
4372                            other_pc,
4373                            *orange,
4374                            e.crange,
4375                            sub,
4376                            &face.surface,
4377                            tol,
4378                        )?,
4379                        tag: Tag::Boundary {
4380                            edge: ei,
4381                            range: sub,
4382                        },
4383                        boundary: true,
4384                    });
4385                }
4386            }
4387        }
4388        for sp in &section_pieces {
4389            let section = &sections[sp.section];
4390            let (belongs, pcurve) = if from_a {
4391                (section.face_a == fi && !sp.hugs[0], &section.pc_a)
4392            } else {
4393                (section.face_b == fi && !sp.hugs[1], &section.pc_b)
4394            };
4395            if !belongs {
4396                continue;
4397            }
4398            let domain = section.curve.domain();
4399            let sub = if section.closed {
4400                (
4401                    fold(sp.range.0, domain),
4402                    sp.range.1 - sp.range.0 + fold(sp.range.0, domain),
4403                )
4404            } else {
4405                sp.range
4406            };
4407            // The pcurve shares the curve's parameterization; sampling
4408            // uses folded parameters for periodic curves.
4409            let count = 32;
4410            let mut line = Vec::with_capacity(count + 1);
4411            for i in 0..=count {
4412                #[allow(clippy::cast_precision_loss)]
4413                let t = sub.0 + (sub.1 - sub.0) * i as f64 / count as f64;
4414                let tf = if section.closed { fold(t, domain) } else { t };
4415                line.push(pcurve.point_at(tf, tol)?);
4416            }
4417            // Folding the parameter can tear the sampled polyline at the
4418            // period; unwrap it pointwise, then bring the whole strand
4419            // into the chart with one shift.
4420            unwrap_polyline(&mut line, &face.surface);
4421            {
4422                let trim: Vec<&[Point2]> = strands
4423                    .iter()
4424                    .filter(|st| st.boundary)
4425                    .map(|st| st.polyline.as_slice())
4426                    .collect();
4427                fold_line_inside(&mut line, &face.surface, &trim);
4428            }
4429            strands.push(Strand {
4430                polyline: line,
4431                tag: Tag::Section {
4432                    section: sp.section,
4433                    range: sp.range,
4434                },
4435                boundary: false,
4436            });
4437        }
4438        // The poles, after the sections, because a section can end *on*
4439        // a pole (a plane through a ball's axis cuts it exactly there),
4440        // and the pole has to be cut where that happens or the two meet
4441        // at no shared node and the arrangement sees a dangling section.
4442        for (pi, pole) in face.poles.iter().enumerate() {
4443            let mut stops = vec![pole.prange.0, pole.prange.1];
4444            if let PlanarCurve::Line(line) = &pole.pcurve {
4445                let axis = line.axis();
4446                for strand in &strands {
4447                    if strand.boundary {
4448                        continue;
4449                    }
4450                    for end in [strand.polyline.first(), strand.polyline.last()]
4451                        .into_iter()
4452                        .flatten()
4453                    {
4454                        let along = (*end - axis.location).dot(axis.direction.vector());
4455                        let foot = axis.point_at(along);
4456                        if foot.distance(*end) <= PARAM_SNAP
4457                            && along > pole.prange.0 + PARAM_SNAP
4458                            && along < pole.prange.1 - PARAM_SNAP
4459                        {
4460                            stops.push(along);
4461                        }
4462                    }
4463                }
4464            }
4465            stops.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
4466            stops.dedup_by(|a, b| (*a - *b).abs() <= PARAM_SNAP);
4467            for pair in stops.windows(2) {
4468                let sub = (pair[0], pair[1]);
4469                strands.push(Strand {
4470                    polyline: pcurve_polyline(
4471                        &pole.pcurve,
4472                        pole.prange,
4473                        pole.prange,
4474                        sub,
4475                        &face.surface,
4476                        tol,
4477                    )?,
4478                    tag: Tag::Pole {
4479                        pole: pi,
4480                        range: sub,
4481                    },
4482                    boundary: true,
4483                });
4484            }
4485        }
4486        for (ci, contact) in contacts.iter().enumerate() {
4487            if contact.target_from_a != from_a || contact.target_face != fi {
4488                continue;
4489            }
4490            // The owner's paves split its edge; the contact strands split
4491            // at the same parameters, so the sub-edges rebuilt from both
4492            // sides are the same edges and sew shared.
4493            let mut stops = vec![contact.crange.0];
4494            if let Some(ts) = paves.get(&contact.node) {
4495                stops.extend(
4496                    cluster_paves(&contact.curve, contact.crange, contact.tolerance, ts, tol)?
4497                        .iter()
4498                        .map(|c| c.t),
4499                );
4500            }
4501            stops.push(contact.crange.1);
4502            let closed_contact = contact
4503                .curve
4504                .point_at(contact.crange.0, tol)?
4505                .distance(contact.curve.point_at(contact.crange.1, tol)?)
4506                <= tol.confusion();
4507            if closed_contact && stops.len() == 2 {
4508                stops.insert(1, f64::midpoint(contact.crange.0, contact.crange.1));
4509            }
4510            for pair in stops.windows(2) {
4511                let sub = (pair[0], pair[1]);
4512                if sub.1 - sub.0 <= tol.parametric() {
4513                    continue;
4514                }
4515                let mid_t = f64::midpoint(sub.0, sub.1);
4516                if contact_along[ci]
4517                    .iter()
4518                    .any(|(lo, hi)| mid_t >= *lo && mid_t <= *hi)
4519                {
4520                    // Already boundary on both sides.
4521                    if *DEBUG_STRANDS {
4522                        eprintln!("CONTACT c{ci} {sub:?} on fi={fi}: along boundary, skipped");
4523                    }
4524                    continue;
4525                }
4526                // Keep only what lies inside this face's trim; the rest
4527                // of the owner's boundary splits nothing here.
4528                let mut line = pcurve_polyline(
4529                    &contact.pcurve,
4530                    contact.prange,
4531                    contact.crange,
4532                    sub,
4533                    &face.surface,
4534                    tol,
4535                )?;
4536                unwrap_polyline(&mut line, &face.surface);
4537                let boundary_lines: Vec<&[Point2]> = strands
4538                    .iter()
4539                    .filter(|st| st.boundary)
4540                    .map(|st| st.polyline.as_slice())
4541                    .collect();
4542                fold_line_inside(&mut line, &face.surface, &boundary_lines);
4543                let mid = interior_of(&line);
4544                if !inside_many_slanted(&boundary_lines, mid) {
4545                    if *DEBUG_STRANDS {
4546                        eprintln!(
4547                            "CONTACT c{ci} {sub:?} on fi={fi}: outside the trim at {mid:?}, skipped"
4548                        );
4549                    }
4550                    continue;
4551                }
4552                strands.push(Strand {
4553                    polyline: line,
4554                    tag: Tag::Contact {
4555                        contact: ci,
4556                        range: sub,
4557                    },
4558                    boundary: false,
4559                });
4560            }
4561        }
4562
4563        // A tolerant contact's chart image meets the boundary it paved
4564        // only as closely as its own slop allows; the arrangement's node
4565        // weld reaches that far on this face, or the strand dangles a
4566        // few microns from the junction it belongs to.
4567        // A fitted section that hugs one of this face's edges leaves it
4568        // at the hug's far end by up to the hug's own width; its strand
4569        // must still find the edge's node there.
4570        let doubt_of = |edges: &[BoundaryEdge]| -> f64 {
4571            edges.iter().fold(0.0_f64, |acc, e| {
4572                acc.max(e.tolerance * 2.0)
4573                    .max(e.ends_tolerance + e.tolerance)
4574            })
4575        };
4576        let near = contacts
4577            .iter()
4578            .filter(|c| c.target_from_a == from_a && c.target_face == fi)
4579            .fold(PARAM_SNAP, |acc, c| acc.max(c.tolerance * 2.0))
4580            // An edge's end lands in the chart within its vertex's own
4581            // recorded doubt plus its own image's: a projected pcurve is
4582            // honest to the edge's tolerance, and the vertex it ends at
4583            // was welded to some earlier gap.
4584            .max(doubt_of(&face.edges))
4585            .max(spread)
4586            .max(
4587                if sections.iter().any(|s| {
4588                    s.tolerance > 0.0
4589                        && if from_a {
4590                            s.face_a == fi
4591                        } else {
4592                            s.face_b == fi
4593                        }
4594                }) {
4595                    tol.confusion() * 1e3
4596                } else {
4597                    0.0
4598                },
4599            );
4600        // A section ends where it crosses the other face's boundary,
4601        // and lands there within that boundary's own doubt: two
4602        // sections through neighbouring facets of a converted mesh
4603        // stop on the edge they share a few microns apart, each on its
4604        // own facet's plane, and meet on this face only if the weld
4605        // reaches that far.
4606        let far = |section: usize| -> f64 {
4607            let s = &sections[section];
4608            doubt_of(if from_a {
4609                &gb.faces[s.face_b].edges
4610            } else {
4611                &ga.faces[s.face_a].edges
4612            })
4613        };
4614        let face_snap = near.max(
4615            (0..sections.len())
4616                .filter(|&k| {
4617                    if from_a {
4618                        sections[k].face_a == fi
4619                    } else {
4620                        sections[k].face_b == fi
4621                    }
4622                })
4623                .map(far)
4624                .fold(0.0_f64, f64::max),
4625        );
4626        // The doubts above are lengths in space, and the weld runs in the
4627        // chart. Where the chart stretches every direction (a sphere of a
4628        // few millimetres runs a radian over its whole radius), a gap in
4629        // space is that many times narrower in the chart, and a weld taken
4630        // at the space length would swallow whole edges of a small patch.
4631        // Where some direction shrinks instead (a pole, a thin cylinder's
4632        // turn), the length stands.
4633        let stretch = strands
4634            .iter()
4635            .flat_map(|st| st.polyline.iter())
4636            .step_by(8)
4637            .filter_map(|p| face.surface.d1_at(p.x, p.y, tol).ok())
4638            .map(|(du, dv)| du.magnitude().min(dv.magnitude()))
4639            .fold(f64::INFINITY, f64::min);
4640        let face_snap = if stretch.is_finite() && stretch > 1.0 {
4641            (face_snap / stretch).max(PARAM_SNAP)
4642        } else {
4643            face_snap
4644        };
4645        // Where only one direction stretches (a cylinder's turn against its
4646        // straight length) no single scale holds, and a strand can be short
4647        // in the chart yet long in space. And the weld is the loosest doubt
4648        // of anything on the face: one sloppy edge a section crosses sets it
4649        // for every other section there. A strand longer in space than its
4650        // own doubt is no dust, so the weld stays below its chart length,
4651        // or it collapses and takes its neighbours' ends with it.
4652        let own_doubt = |tag: &Tag| -> f64 {
4653            match tag {
4654                Tag::Section { section, .. } => near.max(far(*section)),
4655                Tag::Pole { .. } => f64::INFINITY,
4656                Tag::Boundary { .. } | Tag::Contact { .. } => near,
4657            }
4658        };
4659        let face_snap = strands
4660            .iter()
4661            .filter(|st| {
4662                st.polyline.len() >= 2
4663                    && st
4664                        .polyline
4665                        .windows(2)
4666                        .map(|w| w[0].distance(w[1]))
4667                        .sum::<f64>()
4668                        <= face_snap
4669            })
4670            .filter_map(|st| {
4671                let points: Vec<Point> = st
4672                    .polyline
4673                    .iter()
4674                    .filter_map(|p| face.surface.point_at(p.x, p.y, tol).ok())
4675                    .collect();
4676                let space: f64 = points.windows(2).map(|w| w[0].distance(w[1])).sum();
4677                (space > own_doubt(&st.tag)).then(|| {
4678                    st.polyline
4679                        .windows(2)
4680                        .map(|w| w[0].distance(w[1]))
4681                        .sum::<f64>()
4682                        / 2.0
4683                })
4684            })
4685            .fold(face_snap, f64::min)
4686            .max(PARAM_SNAP);
4687        // A section that runs along one of this face's edges into its end
4688        // stops within the hug's width of the node the edge was split at,
4689        // and two sections meeting a rail that grazes this face stop on it
4690        // as far apart as the rail stays within tolerance of the face. The
4691        // junction there says each pair is one point. Where a chart unit is
4692        // shorter than a millimetre, or the graze long, that gap is wider in
4693        // the chart than the weld, and the section dangles beside the node it
4694        // ends at. Its end moves onto a boundary node the junction holds, or
4695        // onto the first section end already there.
4696        let mut anchors: Vec<(Option<usize>, Point2, Point)> = strands
4697            .iter()
4698            .filter(|st| st.boundary && st.polyline.len() >= 2)
4699            .flat_map(|st| [st.polyline[0], st.polyline[st.polyline.len() - 1]])
4700            .filter_map(|p| {
4701                face.surface
4702                    .point_at(p.x, p.y, tol)
4703                    .ok()
4704                    .map(|q| (None, p, q))
4705            })
4706            .collect();
4707        for st in strands
4708            .iter_mut()
4709            .filter(|st| matches!(st.tag, Tag::Section { .. }) && st.polyline.len() >= 2)
4710        {
4711            let last = st.polyline.len() - 1;
4712            for at in [0, last] {
4713                let p = st.polyline[at];
4714                if anchors
4715                    .iter()
4716                    .any(|(held, n, _)| held.is_none() && n.distance(p) <= face_snap)
4717                {
4718                    continue;
4719                }
4720                let Ok(q) = face.surface.point_at(p.x, p.y, tol) else {
4721                    continue;
4722                };
4723                let Some((ji, junction)) = junctions
4724                    .iter()
4725                    .enumerate()
4726                    .find(|(_, j)| j.at.distance(q) <= j.reach)
4727                else {
4728                    continue;
4729                };
4730                // The nearest in space the junction also holds, and reached
4731                // across the chart without leaving it: the chart's midpoint
4732                // of the two lies midway in space too, where a node in
4733                // another period's copy would not.
4734                let anchor = anchors
4735                    .iter()
4736                    .filter(|(held, _, s)| {
4737                        held.is_none_or(|h| h == ji) && junction.at.distance(*s) <= junction.reach
4738                    })
4739                    .filter(|(_, n, s)| {
4740                        face.surface
4741                            .point_at((n.x + p.x) / 2.0, (n.y + p.y) / 2.0, tol)
4742                            .is_ok_and(|m| m.distance(q.midpoint(*s)) <= junction.reach)
4743                    })
4744                    .min_by(|a, b| {
4745                        a.2.distance(q)
4746                            .partial_cmp(&b.2.distance(q))
4747                            .unwrap_or(core::cmp::Ordering::Equal)
4748                    })
4749                    .map(|(_, n, _)| *n);
4750                match anchor {
4751                    Some(n) => st.polyline[at] = n,
4752                    None => anchors.push((Some(ji), p, q)),
4753                }
4754            }
4755        }
4756        if *DEBUG_STRANDS {
4757            eprintln!(
4758                "FACE-SNAP from_a={from_a} fi={fi}: snap {face_snap:.3e} edges {:?}",
4759                face.edges
4760                    .iter()
4761                    .map(|e| (
4762                        format!("{:.2e}", e.tolerance),
4763                        format!("{:.2e}", e.ends_tolerance)
4764                    ))
4765                    .collect::<Vec<_>>()
4766            );
4767            for (si, st) in strands.iter().enumerate() {
4768                let tag = match st.tag {
4769                    Tag::Boundary { edge, range } => format!("Boundary e{edge} {range:?}"),
4770                    Tag::Contact { contact, range } => format!("Contact c{contact} {range:?}"),
4771                    Tag::Section { section, range } => format!("Section s{section} {range:?}"),
4772                    Tag::Pole { pole, range } => format!("Pole p{pole} {range:?}"),
4773                };
4774                let (a, b) = (st.polyline[0], st.polyline[st.polyline.len() - 1]);
4775                eprintln!(
4776                    "STRAND from_a={from_a} fi={fi} {si}: boundary={} {tag} pts={} {a:?} .. {b:?}",
4777                    st.boundary,
4778                    st.polyline.len()
4779                );
4780            }
4781        }
4782        Ok((strands, face_snap))
4783    };
4784    let chart_length =
4785        |line: &[Point2]| -> f64 { line.windows(2).map(|w| w[0].distance(w[1])).sum::<f64>() };
4786    // The identity of a piece across faces: a boundary or contact piece by
4787    // its edge node, a section piece by its section: the same section is
4788    // split the same way on both faces it cuts, and a piece that is dust
4789    // in a sphere's chart at the pole must be dust on the plane it also
4790    // lies in.
4791    let node_of = |face: &GFace, tag: &Tag| -> Option<(usize, usize, (f64, f64))> {
4792        match tag {
4793            Tag::Boundary { edge, range } => {
4794                Some((0, face.edges[*edge].node.index() as usize, *range))
4795            }
4796            Tag::Contact { contact, range } => {
4797                Some((1, contacts[*contact].node.index() as usize, *range))
4798            }
4799            Tag::Section { section, range } => Some((2, *section, *range)),
4800            Tag::Pole { .. } => None,
4801        }
4802    };
4803    /// A face's strands and its snap, waiting to be arranged.
4804    type Prepared = Option<(Vec<Strand<Tag>>, f64)>;
4805    let mut prepared: [Vec<Prepared>; 2] = [Vec::new(), Vec::new()];
4806    let mut dust: Vec<(usize, usize, (f64, f64))> = Vec::new();
4807    for (side, solid, from_a) in [(0_usize, &ga, true), (1, &gb, false)] {
4808        for (fi, face) in solid.faces.iter().enumerate() {
4809            ogeom_core::progress::checkpoint()?;
4810            let (strands, snap) = strands_of(from_a, fi, face)?;
4811            for st in &strands {
4812                if st.polyline.len() >= 2
4813                    && chart_length(&st.polyline) <= snap
4814                    && let Some(key) = node_of(face, &st.tag)
4815                {
4816                    dust.push(key);
4817                }
4818            }
4819            prepared[side].push(Some((strands, snap)));
4820        }
4821    }
4822    ogeom_core::progress::stage("boolean: split arrange");
4823    let same_key = |a: &(usize, usize, (f64, f64)), b: &(usize, usize, (f64, f64))| {
4824        a.0 == b.0
4825            && a.1 == b.1
4826            && (a.2.0 - b.2.0).abs() <= tol.parametric()
4827            && (a.2.1 - b.2.1).abs() <= tol.parametric()
4828    };
4829    for (from_a, own, other) in [(true, &ga, &gb.solid), (false, &gb, &ga.solid)] {
4830        // The other solid's boundary, prepared once for the whole side. It is
4831        // asked once per face piece, and what it costs to prepare (every
4832        // face's trimming rings, polylined) does not depend on the point
4833        // being asked about. Rebuilt per question it dwarfed the question:
4834        // 3.5 ms of preparation against 5.6 µs of ray casting.
4835        let boundary = ogeom_algo::SolidBoundary::of(model, other, tol.confusion() * 1e4, tol)?;
4836        for (fi, face) in own.faces.iter().enumerate() {
4837            ogeom_core::progress::checkpoint()?;
4838            let Some((mut strands, face_snap)) = prepared[usize::from(!from_a)][fi].take() else {
4839                ogeom_bail!(Construction, "a face was prepared twice");
4840            };
4841            // A piece some other chart already collapsed collapses here too:
4842            // its ends become one node, every neighbour meeting them moves
4843            // onto it, and one junction owns the span in space so the
4844            // rebuilt vertices agree on every face.
4845            // A strand's ends in space, or none for a pole.
4846            let space_ends = |face: &GFace, tag: &Tag| -> OgeomResult<Option<(Point, Point)>> {
4847                Ok(Some(match tag {
4848                    Tag::Boundary { edge, range } => {
4849                        let e = &face.edges[*edge];
4850                        (
4851                            e.curve.point_at(range.0, tol)?,
4852                            e.curve.point_at(range.1, tol)?,
4853                        )
4854                    }
4855                    Tag::Contact { contact, range } => {
4856                        let c = &contacts[*contact];
4857                        (
4858                            c.curve.point_at(range.0, tol)?,
4859                            c.curve.point_at(range.1, tol)?,
4860                        )
4861                    }
4862                    Tag::Section { section, range } => {
4863                        let sec = &sections[*section];
4864                        let domain = sec.curve.domain();
4865                        (
4866                            sec.curve
4867                                .point_at(at_param(range.0, domain, sec.closed), tol)?,
4868                            sec.curve
4869                                .point_at(at_param(range.1, domain, sec.closed), tol)?,
4870                        )
4871                    }
4872                    Tag::Pole { .. } => return Ok(None),
4873                }))
4874            };
4875            // A strand this face's own weld collapses is one point in space
4876            // as well: its ends become one junction, or the pieces either
4877            // side of it end on two vertices the chart says are one, and a
4878            // loop cut at a seam a few microns from its own start closes in
4879            // the chart and stays open in space.
4880            for st in &strands {
4881                if st.polyline.len() >= 2
4882                    && chart_length(&st.polyline) <= face_snap
4883                    && let Some((from, to)) = space_ends(face, &st.tag)?
4884                    && from.distance(to) > tol.confusion() * 1e2
4885                {
4886                    junctions.push(Junction {
4887                        at: from.midpoint(to),
4888                        reach: from.distance(to) / 2.0 + tol.confusion() * 1e2,
4889                        onto_vertex: false,
4890                    });
4891                }
4892            }
4893            let forced: Vec<usize> = strands
4894                .iter()
4895                .enumerate()
4896                .filter(|(_, st)| {
4897                    st.polyline.len() >= 2
4898                        && chart_length(&st.polyline) > face_snap
4899                        && node_of(face, &st.tag)
4900                            .is_some_and(|k| dust.iter().any(|d| same_key(d, &k)))
4901                })
4902                .map(|(i, _)| i)
4903                .collect();
4904            if !forced.is_empty() {
4905                let mut reps: Vec<(Point2, usize)> = Vec::new();
4906                let canon = |p: Point2, reps: &mut Vec<(Point2, usize)>| -> usize {
4907                    match reps.iter().position(|(q, _)| q.distance(p) <= face_snap) {
4908                        Some(i) => i,
4909                        None => {
4910                            reps.push((p, reps.len()));
4911                            reps.len() - 1
4912                        }
4913                    }
4914                };
4915                fn root(reps: &mut [(Point2, usize)], mut i: usize) -> usize {
4916                    while reps[i].1 != i {
4917                        reps[i].1 = reps[reps[i].1].1;
4918                        i = reps[i].1;
4919                    }
4920                    i
4921                }
4922                for &i in &forced {
4923                    let st = &strands[i];
4924                    let a = canon(st.polyline[0], &mut reps);
4925                    let b = canon(st.polyline[st.polyline.len() - 1], &mut reps);
4926                    let (ra, rb) = (root(&mut reps, a), root(&mut reps, b));
4927                    if ra != rb {
4928                        reps[rb].1 = ra;
4929                    }
4930                    let Some((from, to)) = space_ends(face, &st.tag)? else {
4931                        continue;
4932                    };
4933                    if *DEBUG_STRANDS {
4934                        eprintln!(
4935                            "DUST from_a={from_a} fi={fi}: strand {i} collapses with its partners, {from:?} -> {to:?}"
4936                        );
4937                    }
4938                    junctions.push(Junction {
4939                        at: from.midpoint(to),
4940                        reach: from.distance(to) / 2.0 + tol.confusion() * 1e2,
4941                        onto_vertex: false,
4942                    });
4943                }
4944                for (i, st) in strands.iter_mut().enumerate() {
4945                    if forced.contains(&i) {
4946                        continue;
4947                    }
4948                    let last = st.polyline.len() - 1;
4949                    for end in [0, last] {
4950                        if let Some(k) = reps
4951                            .iter()
4952                            .position(|(q, _)| q.distance(st.polyline[end]) <= face_snap)
4953                        {
4954                            let r = root(&mut reps, k);
4955                            st.polyline[end] = reps[r].0;
4956                        }
4957                    }
4958                }
4959                let mut index = 0_usize;
4960                strands.retain(|_| {
4961                    let keep = !forced.contains(&index);
4962                    index += 1;
4963                    keep
4964                });
4965            }
4966            let split = match arrange_pieces(&strands, face_snap) {
4967                Ok(split) => split,
4968                Err(err) => {
4969                    if *ARRANGE_DEBUG {
4970                        eprintln!("ARRANGE from_a={from_a} fi={fi} failed: {err}");
4971                        for (si, st) in strands.iter().enumerate() {
4972                            let (a, b) = (st.polyline[0], st.polyline[st.polyline.len() - 1]);
4973                            let describe = |curve: &Curve, range: (f64, f64)| -> String {
4974                                let mid = curve
4975                                    .point_at(f64::midpoint(range.0, range.1), tol)
4976                                    .map(|p| format!("({:.4},{:.4},{:.4})", p.x, p.y, p.z))
4977                                    .unwrap_or_default();
4978                                match curve {
4979                                    Curve::Circle(c) => format!(
4980                                        "circle centre {:?} r {:.4} mid {mid}",
4981                                        c.circle().centre(),
4982                                        c.circle().radius()
4983                                    ),
4984                                    other => {
4985                                        format!("{:?} mid {mid}", core::mem::discriminant(other))
4986                                    }
4987                                }
4988                            };
4989                            let geometry = match st.tag {
4990                                Tag::Boundary { edge, range } => {
4991                                    describe(&face.edges[edge].curve, range)
4992                                }
4993                                Tag::Contact { contact, range } => {
4994                                    format!(
4995                                        "{} window {:?} along {:?} node {}",
4996                                        describe(&contacts[contact].curve, range),
4997                                        contacts[contact].crange,
4998                                        contact_along[contact],
4999                                        contacts[contact].node.index()
5000                                    )
5001                                }
5002                                _ => String::new(),
5003                            };
5004                            eprintln!("    {geometry}");
5005                            eprintln!(
5006                                "  strand {si} boundary={} {:?} {a:?} .. {b:?}",
5007                                st.boundary,
5008                                match st.tag {
5009                                    Tag::Boundary { edge, range } =>
5010                                        format!("Boundary e{edge} {range:?}"),
5011                                    Tag::Contact { contact, range } =>
5012                                        format!("Contact c{contact} {range:?}"),
5013                                    Tag::Section { section, range } =>
5014                                        format!("Section s{section} {range:?}"),
5015                                    Tag::Pole { pole, range } => format!("Pole p{pole} {range:?}"),
5016                                }
5017                            );
5018                        }
5019                    }
5020                    return Err(err);
5021                }
5022            };
5023            for piece in split {
5024                // Where a piece stands is asked at its interior probes in
5025                // turn. The first is the roomiest, and usually the only one
5026                // needed; the rest are for the piece that merely *touches*
5027                // the other solid, whose roomiest probe can land on the
5028                // contact and read neither in nor out.
5029                //
5030                // A partner face is asked before the classifier, not after.
5031                // Where a piece sits on a surface the other solid also
5032                // carries, the partner *is* the answer, and getting there
5033                // through the classifier means every ray grazing the shared
5034                // face, its whole fan of directions exhausted, before it
5035                // reports the On the partner list already knew. On a part
5036                // whose bore is refilled by its own cylinder that is the
5037                // difference between a tenth of a second and a minute.
5038                let partners = if from_a { &same_a[fi] } else { &same_b[fi] };
5039                let mut chosen = None;
5040                for candidate in &piece.interiors {
5041                    let at = face.surface.point_at(candidate.x, candidate.y, tol)?;
5042                    let shared = !partners.is_empty()
5043                        && partners.iter().any(|&pi| {
5044                            let partner = if from_a { &gb.faces[pi] } else { &ga.faces[pi] };
5045                            chart_point_of(partner, at, tol).is_some()
5046                        });
5047                    let says = if shared {
5048                        Containment::On
5049                    } else {
5050                        boundary.holds(model, at, tol)?
5051                    };
5052                    if chosen.is_none() || !matches!(says, Containment::On) {
5053                        chosen = Some((*candidate, at, says));
5054                    }
5055                    if !matches!(says, Containment::On) {
5056                        break;
5057                    }
5058                }
5059                let Some((interior, probe, said)) = chosen else {
5060                    ogeom_bail!(
5061                        Construction,
5062                        "a piece of a face has no interior point to classify at"
5063                    );
5064                };
5065                let state = match said {
5066                    Containment::In => PieceState::In,
5067                    Containment::Out => PieceState::Out,
5068                    Containment::On => {
5069                        // On the other boundary: same-domain contact. The
5070                        // partner face on the shared surface decides whether
5071                        // the two materials lie on the same side or oppose.
5072                        let own_normal = outward_normal(face, interior, tol)?;
5073                        let mut resolved = None;
5074                        for &pi in partners {
5075                            let partner = if from_a { &gb.faces[pi] } else { &ga.faces[pi] };
5076                            let Some(uv) = chart_point_of(partner, probe, tol) else {
5077                                continue;
5078                            };
5079                            let theirs = outward_normal(partner, uv, tol)?;
5080                            resolved = Some(if own_normal.dot(theirs) > 0.0 {
5081                                PieceState::OnAligned
5082                            } else {
5083                                PieceState::OnOpposed
5084                            });
5085                            break;
5086                        }
5087                        let Some(state) = resolved else {
5088                            // On with no partner containing the probe: the
5089                            // band's generosity read proximity as
5090                            // coincidence. Ask again at a width where it
5091                            // cannot, and only a genuine edge contact
5092                            // remains refused.
5093                            match ogeom_algo::classify_in_solid_exact_banded(
5094                                model,
5095                                other,
5096                                probe,
5097                                tol.confusion() * 10.0,
5098                                tol,
5099                            )? {
5100                                Containment::In => {
5101                                    pieces.push(FacePiece {
5102                                        from_a,
5103                                        face: fi,
5104                                        rings: piece.rings,
5105                                        outlines: piece.outlines,
5106                                        probe,
5107                                        state: PieceState::In,
5108                                        covered: false,
5109                                    });
5110                                    continue;
5111                                }
5112                                Containment::Out => {
5113                                    pieces.push(FacePiece {
5114                                        from_a,
5115                                        face: fi,
5116                                        rings: piece.rings,
5117                                        outlines: piece.outlines,
5118                                        probe,
5119                                        state: PieceState::Out,
5120                                        covered: false,
5121                                    });
5122                                    continue;
5123                                }
5124                                Containment::On => {}
5125                            }
5126                            if *DEBUG_WIRE {
5127                                eprintln!(
5128                                    "ON-CONTACT piece of {} face {fi} probe {probe:?} partners {partners:?}",
5129                                    if from_a { "A" } else { "B" }
5130                                );
5131                            }
5132                            ogeom_bail!(
5133                                NotDone,
5134                                "a piece lies on the other solid's boundary \
5135                                 with no coincident partner face to compare \
5136                                 sides against; edge or vertex contact is \
5137                                 refused rather than resolved; see the \
5138                                 remaining work in docs/PLAN.md"
5139                            );
5140                        };
5141                        state
5142                    }
5143                };
5144                pieces.push(FacePiece {
5145                    from_a,
5146                    face: fi,
5147                    rings: piece.rings,
5148                    outlines: piece.outlines,
5149                    probe,
5150                    state,
5151                    covered: false,
5152                });
5153            }
5154        }
5155    }
5156    mark_covered_coincidences(&ga, &mut pieces, tol);
5157    if *ARRANGE_DEBUG {
5158        for (fi, partners) in same_a.iter().enumerate() {
5159            if !partners.is_empty() {
5160                eprintln!("SAME a{fi} with b{partners:?}");
5161            }
5162        }
5163        for (ci, contact) in contacts.iter().enumerate() {
5164            eprintln!(
5165                "CONTACT c{ci} onto {}{} over {:?} prange {:?} node {} pcurve {:?}",
5166                if contact.target_from_a { "a" } else { "b" },
5167                contact.target_face,
5168                contact.crange,
5169                contact.prange,
5170                contact.node.index(),
5171                contact.pcurve
5172            );
5173        }
5174        for (i, p) in pieces.iter().enumerate() {
5175            let own = if p.from_a {
5176                &ga.faces[p.face]
5177            } else {
5178                &gb.faces[p.face]
5179            };
5180            let kind = match &own.surface {
5181                SurfaceGeometry::Plane(_) => "plane",
5182                SurfaceGeometry::Cylinder(_) => "cyl",
5183                SurfaceGeometry::Torus(_) => "torus",
5184                SurfaceGeometry::BSpline(_) => "bspline",
5185                _ => "other",
5186            };
5187            eprintln!(
5188                "PIECE {i} from_a={} face={} {kind} state={:?} covered={} probe={:?} rings {:?}",
5189                p.from_a,
5190                p.face,
5191                p.state,
5192                p.covered,
5193                p.probe,
5194                p.rings.iter().map(Vec::len).collect::<Vec<_>>()
5195            );
5196        }
5197    }
5198    ogeom_core::progress::stage("boolean: classified");
5199    // Merged again: the arrangement adds junctions of its own (a strand
5200    // that collapsed with its partners becomes one) after the first
5201    // merge, and two of those at one triple point, each within the other's
5202    // reach, welded the ends of one band to two vertices with a hairline
5203    // between them.
5204    let junctions = merge_junctions(junctions);
5205    Ok(GeneralFused {
5206        a: ga,
5207        b: gb,
5208        sections,
5209        contacts,
5210        tangents,
5211        pieces,
5212        junctions,
5213    })
5214}
5215
5216// --- rebuilding --------------------------------------------------------------
5217
5218/// Everything the rebuild shares across pieces.
5219struct Rebuild<'m> {
5220    model: &'m mut Model,
5221    /// World surface ids, minted once per source face.
5222    surfaces_a: Vec<Option<ogeom_topo::SurfaceId>>,
5223    surfaces_b: Vec<Option<ogeom_topo::SurfaceId>>,
5224    /// Vertices shared by position: a wire's connectivity is checked by node
5225    /// identity, so two sub-edges meeting at a point must *name* the same
5226    /// vertex, not merely coincide there.
5227    vertices: Vec<(Point, Shape)>,
5228    /// `vertices` binned by position.
5229    vertex_bins: bins::Bins,
5230    /// The widest tolerance any vertex in `vertices` holds: every widening
5231    /// of one goes through [`Rebuild::vertex`] or [`Rebuild::widen`], so an
5232    /// end is compared only with the vertices this far and a weld from it.
5233    widest: f64,
5234    /// How far two honest descriptions of one junction may sit apart: a
5235    /// hundred confusions as the floor, widened to three times the loosest
5236    /// contact edge's or fitted section's own tolerance when one took part:
5237    /// the strand's own, the crossing's, and the edge's it stopped on.
5238    weld: f64,
5239    /// The paving's junctions, each minted as a vertex the first time a
5240    /// strand end lands inside it.
5241    junctions: Vec<(Junction, Option<Shape>)>,
5242    /// `junctions` binned by position.
5243    junction_bins: bins::Bins,
5244    /// The widest reach of any junction.
5245    junction_reach: f64,
5246    /// Vertices minted from junctions at an edge's own end vertex, which a
5247    /// tangent section's chart image is bent onto.
5248    onto_vertex: std::collections::HashSet<ogeom_topo::TShapeId>,
5249}
5250
5251impl Rebuild<'_> {
5252    fn vertex(&mut self, p: Point, tol: Tolerances) -> Shape {
5253        // A junction several paves described is one vertex owning the span
5254        // they disagree by, and every strand end inside it names that
5255        // vertex, whichever pave its own trim stopped at.
5256        // The rim is as generous as the positional weld below: two strand
5257        // ends a weld apart must not fall on opposite sides of it.
5258        let slack = self.weld.max(tol.confusion() * 1e2);
5259        if *DEBUG_WIRE {
5260            eprintln!("VERTEX ask {p:?}");
5261        }
5262        let reach = self.junction_reach;
5263        let inside = |slot: &usize| {
5264            let j = &self.junctions[*slot].0;
5265            j.at.distance(p) <= j.reach + slack
5266        };
5267        let first = match self.junction_bins.near(p, reach + slack) {
5268            Some(near) => near.into_iter().find(inside),
5269            None => (0..self.junctions.len()).find(inside),
5270        };
5271        if let Some(slot) = first {
5272            let junction = self.junctions[slot].0;
5273            let shape = match &self.junctions[slot].1 {
5274                Some(shape) => shape.clone(),
5275                None => {
5276                    let shape = make_vertex(self.model, junction.at).shape;
5277                    self.junctions[slot].1 = Some(shape.clone());
5278                    self.remember(junction.at, &shape);
5279                    shape
5280                }
5281            };
5282            if junction.onto_vertex {
5283                self.onto_vertex.insert(shape.node());
5284            }
5285            // The junction owns its span, and an end welded in from the rim
5286            // widens it by what it actually sat off by, as any weld does.
5287            let gap = junction.at.distance(p);
5288            if let Some(node) = self.model.node_mut(&shape)
5289                && let ogeom_topo::NodeData::Vertex(data) = node.data_mut()
5290            {
5291                // A confusion of slack past the measured gap: an edge built
5292                // against this vertex measures the same gap through its own
5293                // rounding, and a tolerance equal to it fails by an ulp.
5294                data.tolerance = data
5295                    .tolerance
5296                    .widen_to(junction.reach.max(gap) + tol.confusion());
5297            }
5298            self.note_tolerance(&shape);
5299            return shape;
5300        }
5301        // The weld reach covers what the inputs may honestly disagree by: a
5302        // boundary curve that is itself a fitted intersection from an
5303        // earlier boolean carries a couple of microns of slop, and two
5304        // descriptions of one junction arrive that far apart. A hundred
5305        // confusions at millimetre tolerances is ten microns, below any
5306        // feature this pipeline can resolve, and every weld wider than
5307        // confusion is recorded on the vertex, not papered over.
5308        // And as far again as the vertex already owns: a vertex widened by
5309        // the descriptions it has taken in claims its point within that
5310        // tolerance, an end arriving within that and the weld of it may be
5311        // the same point, and a boolean cannot show it is not. Three
5312        // descriptions of one triple point, where a third band met two
5313        // bands at an apex, arrived a tenth of a micron apart in turn, each
5314        // within the last's reach and none within the first's, and the
5315        // wire round the band's end had two vertices where it needed one.
5316        // Every end taken in is remembered where it arrived, so the next
5317        // end is measured from the description nearest it.
5318        let floor = self.weld.max(tol.confusion() * 1e2);
5319        let near = self
5320            .vertex_bins
5321            .near(p, floor + self.widest)
5322            .unwrap_or_else(|| (0..self.vertices.len()).collect());
5323        let found = near
5324            .into_iter()
5325            .map(|index| &self.vertices[index])
5326            .filter_map(|(q, shape)| {
5327                let own = self
5328                    .model
5329                    .node(shape)
5330                    .and_then(|n| n.data().as_vertex())
5331                    .map_or(0.0, |d| d.tolerance.get());
5332                let gap = q.distance(p);
5333                (gap <= floor.max(own + floor)).then_some((gap, shape.clone()))
5334            })
5335            .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
5336        if let Some((gap, shape)) = found {
5337            // Two descriptions of one junction may disagree by a general
5338            // crossing's residual; the vertex's tolerance is where that
5339            // disagreement is recorded, so the sub-edges built against
5340            // either description still reach it honestly.
5341            let at = self
5342                .model
5343                .node(&shape)
5344                .and_then(|n| n.data().as_vertex())
5345                .map_or(p, |d| d.point);
5346            let off = at.distance(p);
5347            if off > tol.confusion()
5348                && let Some(node) = self.model.node_mut(&shape)
5349                && let ogeom_topo::NodeData::Vertex(data) = node.data_mut()
5350            {
5351                data.tolerance = data.tolerance.widen_to(off + tol.confusion());
5352            }
5353            self.note_tolerance(&shape);
5354            if gap > tol.confusion() {
5355                self.remember(p, &shape);
5356            }
5357            return shape;
5358        }
5359        let shape = make_vertex(self.model, p).shape;
5360        self.remember(p, &shape);
5361        shape
5362    }
5363
5364    /// Take in `shape` as described at `p`.
5365    fn remember(&mut self, p: Point, shape: &Shape) {
5366        self.vertex_bins.insert(p, self.vertices.len());
5367        self.vertices.push((p, shape.clone()));
5368        self.note_tolerance(shape);
5369    }
5370
5371    /// Keep `widest` covering `shape`'s tolerance.
5372    fn note_tolerance(&mut self, shape: &Shape) {
5373        if let Some(data) = self.model.node(shape).and_then(|n| n.data().as_vertex()) {
5374            self.widest = self.widest.max(data.tolerance.get());
5375        }
5376    }
5377
5378    /// Widen a vertex this rebuild handed out.
5379    fn widen(&mut self, vertex: &Shape, to: f64) -> OgeomResult<()> {
5380        self.model.widen(vertex, ogeom_core::Tolerance::new(to)?)?;
5381        self.note_tolerance(vertex);
5382        Ok(())
5383    }
5384
5385    fn surface_id(
5386        &mut self,
5387        fused: &GeneralFused,
5388        from_a: bool,
5389        face: usize,
5390    ) -> ogeom_topo::SurfaceId {
5391        let slot = if from_a {
5392            &mut self.surfaces_a[face]
5393        } else {
5394            &mut self.surfaces_b[face]
5395        };
5396        if let Some(id) = slot {
5397            return *id;
5398        }
5399        let surface = if from_a {
5400            fused.a.faces[face].surface.clone()
5401        } else {
5402            fused.b.faces[face].surface.clone()
5403        };
5404        let id = self.model.geometry_mut().add_surface(surface);
5405        *slot = Some(id);
5406        id
5407    }
5408}
5409
5410/// Build one piece as a face, orientation matching its source face's side.
5411fn build_piece(
5412    rebuild: &mut Rebuild,
5413    fused: &GeneralFused,
5414    piece: &FacePiece,
5415    tol: Tolerances,
5416) -> OgeomResult<Shape> {
5417    let own = if piece.from_a { &fused.a } else { &fused.b };
5418    let face = &own.faces[piece.face];
5419    let surface_id = rebuild.surface_id(fused, piece.from_a, piece.face);
5420
5421    // Sub-edges cached within the piece, so a seam used from both sides is
5422    // one edge appearing twice.
5423    let mut cache: Vec<(usize, u8, (f64, f64), Shape)> = Vec::new();
5424    let mut wires = Vec::new();
5425    for ring in &piece.rings {
5426        let mut edges = Vec::with_capacity(ring.len());
5427        for traversal in ring {
5428            let (key_edge, key_kind, range) = match &traversal.tag {
5429                Tag::Boundary { edge, range } => (*edge, 0_u8, *range),
5430                Tag::Section { section, range } => (*section, 1, *range),
5431                Tag::Contact { contact, range } => (*contact, 2, *range),
5432                Tag::Pole { pole, range } => (*pole, 3, *range),
5433            };
5434            let near = |a: (f64, f64), b: (f64, f64)| {
5435                (a.0 - b.0).abs() <= tol.parametric() && (a.1 - b.1).abs() <= tol.parametric()
5436            };
5437            if *DEBUG_WIRE && (range.1 - range.0).abs() < 1e-4 {
5438                let kind = match &traversal.tag {
5439                    Tag::Boundary { .. } => "boundary",
5440                    Tag::Section { .. } => "section",
5441                    Tag::Contact { .. } => "contact",
5442                    Tag::Pole { .. } => "pole",
5443                };
5444                eprintln!(
5445                    "DUST {kind} {key_edge} range {range:?} in piece from_a={} face={}",
5446                    piece.from_a, piece.face
5447                );
5448            }
5449            let built = if let Some((.., shape)) = cache
5450                .iter()
5451                .find(|(k, s, r, _)| *k == key_edge && *s == key_kind && near(*r, range))
5452            {
5453                shape.clone()
5454            } else {
5455                let shape = build_sub_edge(
5456                    rebuild,
5457                    fused,
5458                    piece.from_a,
5459                    face,
5460                    surface_id,
5461                    &traversal.tag,
5462                    tol,
5463                )?;
5464                cache.push((key_edge, key_kind, range, shape.clone()));
5465                shape
5466            };
5467            // A piece whose two ends welded to one vertex and whose whole
5468            // length lies within the vertex's reach is that vertex's dust
5469            // (a rim's last fraction of a micron before a pole corner) and
5470            // no edge of the wire; a closed edge on one vertex, a full
5471            // circle, reaches far from it and stays.
5472            if !matches!(traversal.tag, Tag::Pole { .. })
5473                && let Some((v0, v1)) = ogeom_algo::edge_vertices(rebuild.model, &built)?
5474                && v0.node() == v1.node()
5475                && let Some(vd) = rebuild
5476                    .model
5477                    .node(&v0)
5478                    .and_then(|n| n.data().as_vertex())
5479                    .map(|d| (d.point, d.tolerance.get()))
5480                && let Some(ed) = rebuild.model.node(&built).and_then(|n| n.data().as_edge())
5481                && let Some(ogeom_topo::EdgeRepr::Curve3d {
5482                    curve, range: r3, ..
5483                }) = ed.curve3d()
5484                && let Some(g) = rebuild.model.geometry().curve(*curve)
5485                && g.point_at(f64::midpoint(r3.0, r3.1), tol)?.distance(vd.0)
5486                    <= vd.1.max(tol.confusion() * 1e4)
5487            {
5488                if *DEBUG_WIRE {
5489                    eprintln!(
5490                        "DUST piece closing on one vertex at {:?} dropped from piece from_a={} face={}",
5491                        vd.0, piece.from_a, piece.face
5492                    );
5493                }
5494                continue;
5495            }
5496            edges.push(if traversal.reversed {
5497                built.reversed()
5498            } else {
5499                built
5500            });
5501        }
5502        let wire = match make_wire(rebuild.model, &edges, tol) {
5503            Ok(w) => w.shape,
5504            Err(e) => {
5505                if *DEBUG_WIRE {
5506                    eprintln!(
5507                        "WIRE FAIL piece from_a={} face={}",
5508                        piece.from_a, piece.face
5509                    );
5510                    for built in &edges {
5511                        if let Some((a, b)) = ogeom_algo::edge_vertices(rebuild.model, built)? {
5512                            let at = |v: &Shape| {
5513                                rebuild
5514                                    .model
5515                                    .node(v)
5516                                    .and_then(|n| n.data().as_vertex())
5517                                    .map(|d| (d.point, d.tolerance.get()))
5518                            };
5519                            eprintln!(
5520                                "   built {:?}{}: v{} {:?} -> v{} {:?}",
5521                                built.node(),
5522                                if built.orientation() == ogeom_topo::Orientation::Reversed {
5523                                    " rev"
5524                                } else {
5525                                    ""
5526                                },
5527                                a.node().index(),
5528                                at(&a),
5529                                b.node().index(),
5530                                at(&b)
5531                            );
5532                        }
5533                    }
5534                    for t in ring {
5535                        let tag = match &t.tag {
5536                            Tag::Boundary { edge, range } => format!(
5537                                "Boundary e{edge} {range:?} tol {:.2e}",
5538                                face.edges[*edge].tolerance
5539                            ),
5540                            Tag::Contact { contact, range } => format!(
5541                                "Contact c{contact} {range:?} tol {:.2e}",
5542                                fused.contacts[*contact].tolerance
5543                            ),
5544                            Tag::Section { section, range } => format!(
5545                                "Section s{section} {range:?} tol {:.2e}",
5546                                fused.sections[*section].tolerance
5547                            ),
5548                            Tag::Pole { pole, range } => format!("Pole p{pole} {range:?}"),
5549                        };
5550                        eprintln!("   {tag} reversed={}", t.reversed);
5551                    }
5552                }
5553                return Err(e);
5554            }
5555        };
5556        wires.push(wire);
5557    }
5558    let built = match make_face_on(rebuild.model, surface_id, &wires, tol) {
5559        Ok(b) => b.shape,
5560        Err(e) => {
5561            if *DEBUG_WIRE {
5562                eprintln!(
5563                    "FACE FAIL piece from_a={} face={}: {e}",
5564                    piece.from_a, piece.face
5565                );
5566                for (wi, wire) in wires.iter().enumerate() {
5567                    for edge in rebuild.model.ordered_children_of(wire)? {
5568                        if let Some((a, b)) = ogeom_algo::edge_vertices(rebuild.model, &edge)? {
5569                            let at = |v: &Shape| {
5570                                rebuild
5571                                    .model
5572                                    .node(v)
5573                                    .and_then(|n| n.data().as_vertex())
5574                                    .map(|d| d.point)
5575                            };
5576                            eprintln!(
5577                                "   wire {wi} edge {:?}{}: v{} {:?} -> v{} {:?}",
5578                                edge.node(),
5579                                if edge.orientation() == ogeom_topo::Orientation::Reversed {
5580                                    " rev"
5581                                } else {
5582                                    ""
5583                                },
5584                                a.node().index(),
5585                                at(&a),
5586                                b.node().index(),
5587                                at(&b)
5588                            );
5589                        }
5590                    }
5591                }
5592            }
5593            return Err(e);
5594        }
5595    };
5596    Ok(
5597        if face.face.orientation() == ogeom_topo::Orientation::Reversed {
5598            built.reversed()
5599        } else {
5600            built
5601        },
5602    )
5603}
5604
5605/// Build the exact sub-edge a tag names, pcurves attached.
5606fn build_sub_edge(
5607    rebuild: &mut Rebuild,
5608    fused: &GeneralFused,
5609    from_a: bool,
5610    face: &GFace,
5611    surface_id: ogeom_topo::SurfaceId,
5612    tag: &Tag,
5613    tol: Tolerances,
5614) -> OgeomResult<Shape> {
5615    match tag {
5616        Tag::Pole { pole, range } => {
5617            // A pole rebuilds as what it was: one vertex, an edge with no
5618            // curve bounded by it twice, and the chart line that says where
5619            // it runs in this face's parameters.
5620            let p = &face.poles[*pole];
5621            let at = rebuild.vertex(p.point, tol);
5622            let model = &mut *rebuild.model;
5623            let mut data = ogeom_topo::EdgeData::new();
5624            data.degenerate = true;
5625            let built = model.add_edge(data, &[at.clone(), at])?;
5626            ogeom_algo::attach_pcurve(
5627                model,
5628                &built,
5629                p.pcurve.clone(),
5630                surface_id,
5631                Location::identity(),
5632                *range,
5633            )?;
5634            Ok(built)
5635        }
5636        Tag::Boundary { edge, range } => {
5637            let e = &face.edges[*edge];
5638            let from = e.curve.point_at(range.0, tol)?;
5639            let to = e.curve.point_at(range.1, tol)?;
5640            let v0 = rebuild.vertex(from, tol);
5641            let v1 = rebuild.vertex(to, tol);
5642            let model = &mut *rebuild.model;
5643            let built = make_edge_between(model, e.curve.clone(), *range, &v0, &v1, tol)?.shape;
5644            // A piece of a tolerant edge is the same curve with the same
5645            // honest radius: a fitted rail's stated slop must survive the
5646            // split, or the next boolean over this solid measures the rail
5647            // against a tolerance it never had. The ends own it too: a
5648            // junction on a tolerant curve is a junction to the curve's own
5649            // resolution, and the wires rebuilt through it meet within that.
5650            if e.tolerance > tol.confusion() {
5651                if let Some(node) = model.node_mut(&built)
5652                    && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
5653                {
5654                    data.tolerance = data.tolerance.widen_to(e.tolerance);
5655                }
5656                for v in [&v0, &v1] {
5657                    rebuild.widen(v, e.tolerance)?;
5658                }
5659            }
5660            let model = &mut *rebuild.model;
5661            let sub_p = (
5662                rescale(range.0, e.crange, e.prange),
5663                rescale(range.1, e.crange, e.prange),
5664            );
5665            match &e.other_side {
5666                None => ogeom_algo::attach_pcurve(
5667                    model,
5668                    &built,
5669                    e.pcurve.clone(),
5670                    surface_id,
5671                    Location::identity(),
5672                    sub_p,
5673                )?,
5674                Some((other, orange)) => {
5675                    // A seam: both sides attach, and an occurrence picks its
5676                    // side by its orientation.
5677                    let sub_o = (
5678                        rescale(range.0, e.crange, *orange),
5679                        rescale(range.1, e.crange, *orange),
5680                    );
5681                    let _ = sub_o;
5682                    ogeom_algo::attach_seam(
5683                        model,
5684                        &built,
5685                        e.pcurve.clone(),
5686                        other.clone(),
5687                        surface_id,
5688                        Location::identity(),
5689                        sub_p,
5690                    )?;
5691                }
5692            }
5693            Ok(built)
5694        }
5695        Tag::Contact { contact, range } => {
5696            let c = &fused.contacts[*contact];
5697            let from = c.curve.point_at(range.0, tol)?;
5698            let to = c.curve.point_at(range.1, tol)?;
5699            let v0 = rebuild.vertex(from, tol);
5700            let v1 = rebuild.vertex(to, tol);
5701            let model = &mut *rebuild.model;
5702            let built = make_edge_between(model, c.curve.clone(), *range, &v0, &v1, tol)?.shape;
5703            // A tolerant contact's pieces and ends own its stated slop, as a
5704            // boundary's and a section's do: three kinds of strand close one
5705            // ring, and every junction meets at the strands' own honesty.
5706            if c.tolerance > tol.confusion() {
5707                if let Some(node) = model.node_mut(&built)
5708                    && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
5709                {
5710                    data.tolerance = data.tolerance.widen_to(c.tolerance);
5711                }
5712                for v in [&v0, &v1] {
5713                    rebuild.widen(v, c.tolerance)?;
5714                }
5715            }
5716            let model = &mut *rebuild.model;
5717            // The stored image keeps its own window; the attached copy names
5718            // the sub-window this piece covers under the proportional map.
5719            let sub_p = (
5720                rescale(range.0, c.crange, c.prange),
5721                rescale(range.1, c.crange, c.prange),
5722            );
5723            let mid = c.pcurve.point_at(f64::midpoint(sub_p.0, sub_p.1), tol)?;
5724            let trim = face_trim_lines(face, tol);
5725            let trim: Vec<&[Point2]> = trim.iter().map(Vec::as_slice).collect();
5726            let folded = fold_inside(mid, &face.surface, &trim);
5727            let shifted = c
5728                .pcurve
5729                .transformed(&ogeom_math::Transform2::translation(folded - mid), tol)?;
5730            ogeom_algo::attach_pcurve(
5731                model,
5732                &built,
5733                shifted,
5734                surface_id,
5735                Location::identity(),
5736                sub_p,
5737            )?;
5738            Ok(built)
5739        }
5740        Tag::Section { section, range } => {
5741            let s = &fused.sections[*section];
5742            let domain = s.curve.domain();
5743            let (f0, f1) = folded_range(*range, domain, s.closed);
5744            let from = s.curve.point_at(at_param(f0, domain, s.closed), tol)?;
5745            let to = s.curve.point_at(at_param(f1, domain, s.closed), tol)?;
5746            let v0 = rebuild.vertex(from, tol);
5747            let v1 = rebuild.vertex(to, tol);
5748            let model = &mut *rebuild.model;
5749            let built = make_edge_between(model, s.curve.clone(), (f0, f1), &v0, &v1, tol)?.shape;
5750            // A piece welded into a junction some way off its own end (a
5751            // section tangent to an edge at the edge's vertex, stopped
5752            // where it began to hug the edge) owns that gap as the vertex
5753            // does: the face it bounds meets its neighbour there only that
5754            // closely, in its chart as in space.
5755            let at = |v: &Shape| {
5756                model
5757                    .node(v)
5758                    .and_then(|n| n.data().as_vertex())
5759                    .map(|d| d.point)
5760            };
5761            let ends = [at(&v0), at(&v1)];
5762            let welded = [(ends[0], from), (ends[1], to)]
5763                .iter()
5764                .filter_map(|(v, p)| v.map(|v| v.distance(*p)))
5765                .fold(0.0, f64::max);
5766            let own = s.tolerance.max(welded);
5767            // A fitted section's pieces and their ends own the section's
5768            // stated slop, exactly as a tolerant boundary's do: the ring
5769            // they close alternates between the two, and both sides must
5770            // meet at the junction's own resolution.
5771            if own > tol.confusion() {
5772                if let Some(node) = model.node_mut(&built)
5773                    && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
5774                {
5775                    data.tolerance = data.tolerance.widen_to(own);
5776                }
5777                for v in [&v0, &v1] {
5778                    rebuild.widen(v, own)?;
5779                }
5780            }
5781            let model = &mut *rebuild.model;
5782            // The section's pcurve is unwrapped across any seam; the face's
5783            // triangulator lives in one chart, so the attached copy is folded
5784            // home by the same period shift the arrangement gave this
5785            // strand's polyline (to the side of the face's seam its trim is
5786            // on), decided by the sub-range's midpoint, so an
5787            // endpoint sitting exactly on the chart's edge stays on the side
5788            // the arc's body is.
5789            let pcurve = if from_a { &s.pc_a } else { &s.pc_b };
5790            let mid = pcurve.point_at(f64::midpoint(f0, f1), tol)?;
5791            let trim = face_trim_lines(face, tol);
5792            let trim: Vec<&[Point2]> = trim.iter().map(Vec::as_slice).collect();
5793            let folded = fold_inside(mid, &face.surface, &trim);
5794            let shifted =
5795                pcurve.transformed(&ogeom_math::Transform2::translation(folded - mid), tol)?;
5796            // The chart image ends where the vertex is, as the edge does in
5797            // space to its tolerance: a welded end left where the section
5798            // stopped leaves the face's outline open by the weld in its
5799            // chart, wider there than any later arrangement's snap.
5800            let loose = s.tolerance.max(tol.confusion() * 1e2);
5801            let bent = [
5802                rebuild.onto_vertex.contains(&v0.node()),
5803                rebuild.onto_vertex.contains(&v1.node()),
5804            ];
5805            let targets = [
5806                ends[0].filter(|v| bent[0] && v.distance(from) > loose),
5807                ends[1].filter(|v| bent[1] && v.distance(to) > loose),
5808            ];
5809            let shifted = if targets.iter().any(Option::is_some) {
5810                pcurve_onto_ends(&shifted, (f0, f1), &face.surface, targets, tol)?
5811            } else {
5812                shifted
5813            };
5814            ogeom_algo::attach_pcurve(
5815                model,
5816                &built,
5817                shifted,
5818                surface_id,
5819                Location::identity(),
5820                (f0, f1),
5821            )?;
5822            Ok(built)
5823        }
5824    }
5825}
5826
5827/// A pcurve over `range` bent at its ends onto the chart points of
5828/// `targets`, where given: each end's correction fades linearly to nothing
5829/// at the other end, and the result is refitted at the same parameters, so
5830/// the image stays same-parameter with its edge to the correction's size.
5831fn pcurve_onto_ends(
5832    pcurve: &PlanarCurve,
5833    range: (f64, f64),
5834    surface: &SurfaceGeometry,
5835    targets: [Option<Point>; 2],
5836    tol: Tolerances,
5837) -> OgeomResult<PlanarCurve> {
5838    const SAMPLES: u32 = 32;
5839    let ts: Vec<f64> = (0..=SAMPLES)
5840        .map(|i| range.0 + (range.1 - range.0) * f64::from(i) / f64::from(SAMPLES))
5841        .collect();
5842    let mut points = ts
5843        .iter()
5844        .map(|t| pcurve.point_at(*t, tol))
5845        .collect::<OgeomResult<Vec<Point2>>>()?;
5846    let last = points.len() - 1;
5847    for (k, target) in targets.iter().enumerate() {
5848        let Some(target) = target else {
5849            continue;
5850        };
5851        let end = points[if k == 0 { 0 } else { last }];
5852        let foot = ogeom_algo::project_on_surface_from(surface, *target, (end.x, end.y), tol)?;
5853        let raw = Point2::new(foot.parameters.0, foot.parameters.1);
5854        // The foot may land a turn away from the end on a periodic chart,
5855        // whether or not the surface's wrapper says it is periodic; a
5856        // shifted candidate counts where the surface agrees it is the foot.
5857        let turn = core::f64::consts::TAU;
5858        let mut shifts = period_shifts(surface);
5859        for du in [-turn, 0.0, turn] {
5860            for dv in [-turn, 0.0, turn] {
5861                shifts.push((du, dv));
5862            }
5863        }
5864        let onto = shifts
5865            .into_iter()
5866            .map(|(du, dv)| Point2::new(raw.x + du, raw.y + dv))
5867            .filter(|c| {
5868                surface
5869                    .point_at(c.x, c.y, tol)
5870                    .is_ok_and(|q| q.distance(foot.point) <= tol.confusion() * 1e2)
5871            })
5872            .min_by(|a, b| {
5873                a.distance(end)
5874                    .partial_cmp(&b.distance(end))
5875                    .unwrap_or(core::cmp::Ordering::Equal)
5876            })
5877            .unwrap_or(raw);
5878        let delta = onto - end;
5879        for (i, p) in points.iter_mut().enumerate() {
5880            #[allow(clippy::cast_precision_loss)]
5881            let along = i as f64 / last as f64;
5882            let weight = if k == 0 { 1.0 - along } else { along };
5883            *p += delta * weight;
5884        }
5885    }
5886    let fitted = ogeom_geom::fit::fit_points_2d_at(&ts, &points, 3, tol.confusion() * 10.0, tol)?;
5887    Ok(PlanarCurve::BSpline(fitted.curve))
5888}
5889
5890/// Sew kept pieces, demand closure, and nest shells into solids and voids.
5891fn assemble_result(
5892    model: &mut Model,
5893    fused: &GeneralFused,
5894    kept: &[(usize, bool)],
5895    a: &Shape,
5896    b: &Shape,
5897    tol: Tolerances,
5898) -> OgeomResult<Built> {
5899    ogeom_core::progress::stage("boolean: assemble");
5900    let mut history = History::new();
5901    let source_face = |piece: &FacePiece| -> Shape {
5902        if piece.from_a {
5903            fused.a.faces[piece.face].face.clone()
5904        } else {
5905            fused.b.faces[piece.face].face.clone()
5906        }
5907    };
5908
5909    if kept.is_empty() {
5910        // A legitimate answer: cutting a solid away entirely leaves nothing.
5911        let empty = model.add_compound(&[])?;
5912        for piece in &fused.pieces {
5913            history.delete(&source_face(piece));
5914        }
5915        history.modify(a, empty.clone());
5916        history.modify(b, empty.clone());
5917        return Ok(Built::new(empty, history));
5918    }
5919
5920    let weld = fused
5921        .contacts
5922        .iter()
5923        .map(|c| c.tolerance)
5924        .chain(fused.sections.iter().map(|s| s.tolerance))
5925        .fold(0.0_f64, |acc, t| acc.max(honest(t, tol) * 3.0));
5926    let floor = weld.max(tol.confusion() * 1e2);
5927    let junction_reach = fused
5928        .junctions
5929        .iter()
5930        .fold(0.0_f64, |acc, j| acc.max(j.reach));
5931    let mut junction_bins = bins::Bins::new(junction_reach + floor);
5932    for (index, j) in fused.junctions.iter().enumerate() {
5933        junction_bins.insert(j.at, index);
5934    }
5935    // The vertices' cells are as wide as the loosest tolerance a strand
5936    // brings to its ends, so an end is compared across a few cells, not
5937    // every vertex.
5938    let loosest = fused
5939        .a
5940        .faces
5941        .iter()
5942        .chain(fused.b.faces.iter())
5943        .flat_map(|f| f.edges.iter().map(|e| e.tolerance))
5944        .chain(fused.contacts.iter().map(|c| c.tolerance))
5945        .chain(fused.sections.iter().map(|s| s.tolerance))
5946        .fold(junction_reach, f64::max);
5947    let mut rebuild = Rebuild {
5948        model,
5949        surfaces_a: vec![None; fused.a.faces.len()],
5950        surfaces_b: vec![None; fused.b.faces.len()],
5951        vertices: Vec::new(),
5952        vertex_bins: bins::Bins::new(floor + loosest),
5953        widest: 0.0,
5954        weld,
5955        junctions: fused.junctions.iter().map(|j| (*j, None)).collect(),
5956        junction_bins,
5957        junction_reach,
5958        onto_vertex: std::collections::HashSet::new(),
5959    };
5960    let mut faces = Vec::new();
5961    let mut kept_sources: std::collections::HashSet<Shape> = std::collections::HashSet::new();
5962    for &(index, flip) in kept {
5963        let piece = &fused.pieces[index];
5964        let mut built = build_piece(&mut rebuild, fused, piece, tol)?;
5965        if flip {
5966            built = built.reversed();
5967        }
5968        history.modify(&source_face(piece), built.clone());
5969        kept_sources.insert(source_face(piece));
5970        faces.push(built);
5971    }
5972    for piece in &fused.pieces {
5973        let source = source_face(piece);
5974        if !kept_sources.contains(&source) {
5975            history.delete(&source);
5976        }
5977    }
5978
5979    let model = rebuild.model;
5980    let sewn = sew(model, &faces, tol)?;
5981    for shell in &sewn.shells {
5982        if !is_shell_closed(model, shell)? {
5983            // Env-gated forensics: the open shell's unshared edges, the
5984            // question every failure here starts from.
5985            if *ARRANGE_DEBUG {
5986                use ogeom_geom::Curve3d as _;
5987                for edge in ogeom_topo::explore_unique(model, shell, ShapeType::Edge)? {
5988                    let users = ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))?
5989                        .iter()
5990                        .filter(|f| {
5991                            ogeom_topo::explore_unique(model, f, ShapeType::Edge)
5992                                .map(|es| es.iter().any(|e2| e2.node() == edge.node()))
5993                                .unwrap_or(false)
5994                        })
5995                        .count();
5996                    let mut occurrences = 0_usize;
5997                    for f in ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))? {
5998                        for wire in model.children_of(&f)? {
5999                            for e2 in model.children_of(&wire)? {
6000                                if e2.node() == edge.node() {
6001                                    occurrences += 1;
6002                                }
6003                            }
6004                        }
6005                    }
6006                    if occurrences % 2 == 1
6007                        && let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge())
6008                        && let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) =
6009                            data.curve3d()
6010                        && let Some(g) = model.geometry().curve(*curve)
6011                    {
6012                        let a = g.point_at(range.0, tol)?;
6013                        let b = g.point_at(range.1, tol)?;
6014                        eprintln!(
6015                            "  open edge uses={occurrences} faces={users} {:?} range={range:?}: {a:?} -> {b:?}",
6016                            core::mem::discriminant(g)
6017                        );
6018                        // Twins: other edges of the shell with the same ends,
6019                        // which the sew should have found to be this edge.
6020                        for other in ogeom_topo::explore_unique(model, shell, ShapeType::Edge)? {
6021                            if other.node() == edge.node() {
6022                                continue;
6023                            }
6024                            let Some(od) = model.node(&other).and_then(|n| n.data().as_edge())
6025                            else {
6026                                continue;
6027                            };
6028                            let Some(ogeom_topo::EdgeRepr::Curve3d {
6029                                curve: oc,
6030                                range: orange,
6031                                ..
6032                            }) = od.curve3d()
6033                            else {
6034                                continue;
6035                            };
6036                            let Some(og) = model.geometry().curve(*oc) else {
6037                                continue;
6038                            };
6039                            let oa = og.point_at(orange.0, tol)?;
6040                            let ob = og.point_at(orange.1, tol)?;
6041                            let same = (oa.distance(a) <= 1e-3 && ob.distance(b) <= 1e-3)
6042                                || (oa.distance(b) <= 1e-3 && ob.distance(a) <= 1e-3);
6043                            if same {
6044                                let om = og.point_at(f64::midpoint(orange.0, orange.1), tol)?;
6045                                let m = g.point_at(f64::midpoint(range.0, range.1), tol)?;
6046                                eprintln!(
6047                                    "    twin {:?} range={orange:?} mid gap {:.2e} tol {:.2e} vs {:.2e}",
6048                                    core::mem::discriminant(og),
6049                                    om.distance(m),
6050                                    od.tolerance.get(),
6051                                    data.tolerance.get()
6052                                );
6053                            }
6054                        }
6055                        for f in ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))?
6056                        {
6057                            let uses_it = ogeom_topo::explore_unique(model, &f, ShapeType::Edge)
6058                                .map(|es| es.iter().any(|e2| e2.node() == edge.node()))
6059                                .unwrap_or(false);
6060                            if !uses_it {
6061                                continue;
6062                            }
6063                            if let Some(ogeom_topo::NodeData::Face(fd)) =
6064                                model.node(&f).map(|n| n.data())
6065                                && let Some(sg) = model.geometry().surface(fd.surface)
6066                            {
6067                                let bound = shape_bounds(model, &f, tol)?;
6068                                eprintln!(
6069                                    "    used by face on {:?} bound {:?}",
6070                                    core::mem::discriminant(sg),
6071                                    bound
6072                                );
6073                            }
6074                        }
6075                    }
6076                }
6077            }
6078            if *ARRANGE_DEBUG {
6079                use ogeom_geom::Curve3d as _;
6080                for f in ogeom_topo::explore(model, shell, Filter::OfType(ShapeType::Face))? {
6081                    let Some(ogeom_topo::NodeData::Face(fd)) = model.node(&f).map(|n| n.data())
6082                    else {
6083                        continue;
6084                    };
6085                    let Some(sg) = model.geometry().surface(fd.surface) else {
6086                        continue;
6087                    };
6088                    let edges = ogeom_topo::explore_unique(model, &f, ShapeType::Edge)?;
6089                    eprintln!(
6090                        "  face {:?} with {} edges",
6091                        core::mem::discriminant(sg),
6092                        edges.len()
6093                    );
6094                    if matches!(sg, SurfaceGeometry::Sphere(_)) {
6095                        for e in &edges {
6096                            if let Some(ed) = model.node(e).and_then(|n| n.data().as_edge())
6097                                && let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) =
6098                                    ed.curve3d()
6099                                && let Some(g) = model.geometry().curve(*curve)
6100                            {
6101                                let a = g.point_at(range.0, tol)?;
6102                                let b = g.point_at(range.1, tol)?;
6103                                eprintln!(
6104                                    "    sphere edge {:?} ({:.4},{:.4},{:.4}) -> ({:.4},{:.4},{:.4})",
6105                                    core::mem::discriminant(g),
6106                                    a.x,
6107                                    a.y,
6108                                    a.z,
6109                                    b.x,
6110                                    b.y,
6111                                    b.z
6112                                );
6113                            }
6114                        }
6115                    }
6116                }
6117            }
6118            ogeom_bail!(
6119                NotDone,
6120                "the kept pieces did not close into a shell; the configuration \
6121                 is beyond what the boolean currently resolves"
6122            );
6123        }
6124    }
6125
6126    // Nest: a shell whose bound sits inside another's is that solid's void.
6127    let mut bounds = Vec::new();
6128    for shell in &sewn.shells {
6129        bounds.push(shape_bounds(model, shell, tol)?);
6130    }
6131    let mut solids = Vec::new();
6132    for (i, shell) in sewn.shells.iter().enumerate() {
6133        let contained = bounds
6134            .iter()
6135            .enumerate()
6136            .any(|(j, other)| j != i && other.contains_box(&bounds[i]));
6137        if contained {
6138            continue;
6139        }
6140        let mut group = vec![shell.clone()];
6141        for (j, candidate) in sewn.shells.iter().enumerate() {
6142            if j != i && bounds[i].contains_box(&bounds[j]) {
6143                group.push(candidate.clone());
6144            }
6145        }
6146        solids.push(model.add_solid(&group)?);
6147    }
6148    let result = if solids.len() == 1 {
6149        solids.remove(0)
6150    } else {
6151        model.add_compound(&solids)?
6152    };
6153    history.modify(a, result.clone());
6154    history.modify(b, result.clone());
6155    Ok(Built::new(result, history))
6156}
6157
6158/// Solids from an unordered soup of faces: sew, demand closure, nest
6159/// shells into solids and voids: the pipeline's own final stages offered
6160/// as a builder.
6161///
6162/// # Errors
6163///
6164/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a
6165/// shell fails to close; an open soup encloses no volume, and saying so
6166/// beats guessing.
6167pub fn make_volume(model: &mut Model, faces: &[Shape], tol: Tolerances) -> OgeomResult<Built> {
6168    let sewn = ogeom_algo::sew(model, faces, tol)?;
6169    for shell in &sewn.shells {
6170        if !ogeom_algo::is_shell_closed(model, shell)? {
6171            ogeom_bail!(
6172                Construction,
6173                "the faces do not close into shells; an open soup encloses no volume"
6174            );
6175        }
6176    }
6177    let mut bounds = Vec::new();
6178    for shell in &sewn.shells {
6179        bounds.push(ogeom_algo::shape_bounds(model, shell, tol)?);
6180    }
6181    let mut solids = Vec::new();
6182    for (i, shell) in sewn.shells.iter().enumerate() {
6183        let contained = bounds
6184            .iter()
6185            .enumerate()
6186            .any(|(j, other)| j != i && other.contains_box(&bounds[i]));
6187        if contained {
6188            continue;
6189        }
6190        let mut group = vec![shell.clone()];
6191        for (j, candidate) in sewn.shells.iter().enumerate() {
6192            if j != i && bounds[i].contains_box(&bounds[j]) {
6193                // A void bounds its solid from inside: material lies outside
6194                // it, so the sewn outward orientation reverses.
6195                group.push(candidate.reversed());
6196            }
6197        }
6198        solids.push(model.add_solid(&group)?);
6199    }
6200    let mut history = History::new();
6201    let result = if solids.len() == 1 {
6202        solids.remove(0)
6203    } else {
6204        model.add_compound(&solids)?
6205    };
6206    for face in faces {
6207        history.modify(face, result.clone());
6208    }
6209    Ok(Built::new(result, history))
6210}
6211
6212/// The three cells two solids cut space into, each one boolean's answer:
6213/// what is only in `a`, what is only in `b`, and what is in both. Arbitrary
6214/// set expressions compose by fusing a selection of these.
6215#[derive(Debug)]
6216pub struct Cells {
6217    /// `a` with `b` removed.
6218    pub a_not_b: Built,
6219    /// `b` with `a` removed.
6220    pub b_not_a: Built,
6221    /// The overlap.
6222    pub common: Built,
6223}
6224
6225/// Split two solids into their three cells.
6226///
6227/// # Errors
6228///
6229/// As the operations themselves.
6230pub fn cells(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Cells> {
6231    Ok(Cells {
6232        a_not_b: cut(model, a, b, tol)?,
6233        b_not_a: cut(model, b, a, tol)?,
6234        common: common(model, a, b, tol)?,
6235    })
6236}
6237
6238/// A tolerance whose confusion *is* the stated fuzz: every gap, pave and
6239/// weld decision inherits it coherently, which is what a fuzzy boolean
6240/// means.
6241fn fuzzed(fuzz: f64, tol: Tolerances) -> OgeomResult<Tolerances> {
6242    if !fuzz.is_finite() || fuzz <= 0.0 {
6243        ogeom_bail!(Construction, "a fuzz of {fuzz} is not a distance");
6244    }
6245    if fuzz <= tol.confusion() {
6246        return Ok(tol);
6247    }
6248    Tolerances::with_scale(ogeom_core::tolerance::CONFUSION / fuzz)
6249}
6250
6251/// [`fuse`] with geometry within `fuzz` of touching counted as touching.
6252///
6253/// # Errors
6254///
6255/// As [`fuse`], plus a non-positive fuzz.
6256pub fn fuse_fuzzy(
6257    model: &mut Model,
6258    a: &Shape,
6259    b: &Shape,
6260    fuzz: f64,
6261    tol: Tolerances,
6262) -> OgeomResult<Built> {
6263    let loosened = fuzzed(fuzz, tol)?;
6264    fuse(model, a, b, loosened)
6265}
6266
6267/// [`cut`] at a stated fuzz.
6268///
6269/// # Errors
6270///
6271/// As [`fuse_fuzzy`].
6272pub fn cut_fuzzy(
6273    model: &mut Model,
6274    a: &Shape,
6275    b: &Shape,
6276    fuzz: f64,
6277    tol: Tolerances,
6278) -> OgeomResult<Built> {
6279    let loosened = fuzzed(fuzz, tol)?;
6280    cut(model, a, b, loosened)
6281}
6282
6283/// A shape repeated `count` times along a direction at a period and fused
6284/// into one: the periodic pattern as a composition of what exists.
6285///
6286/// # Errors
6287///
6288/// As [`fuse`], plus an unusable count or period.
6289pub fn make_periodic(
6290    model: &mut Model,
6291    shape: &Shape,
6292    step: ogeom_math::Vector,
6293    count: usize,
6294    tol: Tolerances,
6295) -> OgeomResult<Built> {
6296    if count == 0 {
6297        ogeom_bail!(Construction, "a pattern of zero copies is nothing");
6298    }
6299    if step.magnitude() <= tol.confusion() {
6300        ogeom_bail!(Construction, "a zero period stacks every copy on the first");
6301    }
6302    let mut history = History::new();
6303    let mut result = shape.clone();
6304    for i in 1..count {
6305        #[allow(clippy::cast_precision_loss, reason = "pattern counts are small")]
6306        let offset = step * i as f64;
6307        let moved =
6308            ogeom_algo::transformed(model, shape, ogeom_math::Transform::translation(offset))?
6309                .shape;
6310        let joined = fuse(model, &result, &moved, tol)?;
6311        history.modify(&moved, joined.shape.clone());
6312        result = joined.shape;
6313    }
6314    history.generate(shape, result.clone());
6315    Ok(Built::new(result, history))
6316}
6317
6318// --- the operations ----------------------------------------------------------
6319
6320/// A shape whose placements carry scale, rebuilt with the scale baked
6321/// into its geometry before the pipeline runs.
6322///
6323/// A scale changes a surface's parameterization out from under its
6324/// pcurves (the old refusal), so the bake is the whole-shape conversion:
6325/// surfaces restated in world space, edges moved exactly, pcurves
6326/// re-derived against the new parameterizations. Unscaled shapes pass
6327/// through untouched.
6328fn baked_if_scaled(model: &mut Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Shape> {
6329    let mut restate = false;
6330    for face in ogeom_topo::explore(model, shape, Filter::OfType(ShapeType::Face))? {
6331        let placement = face.transform(model.datums())?;
6332        // A scale changes lengths the melt compares; a reflection flips
6333        // every chart's natural normal against its face's flag. Either way
6334        // the operand is restated in world coordinates first, where both
6335        // effects are already folded in.
6336        if (placement.scale_factor().abs() - 1.0).abs() > 1e-9 || !placement.preserves_handedness()
6337        {
6338            restate = true;
6339            break;
6340        }
6341    }
6342    if !restate {
6343        return Ok(shape.clone());
6344    }
6345    Ok(ogeom_algo::baked_shape(model, shape, tol)?.shape)
6346}
6347
6348/// Whether a shape is a half space: one shell of one face, open by
6349/// construction or closed inside out.
6350fn is_half_space(model: &Model, shape: &Shape) -> OgeomResult<bool> {
6351    Ok(half_space::half_space_face(model, shape, Tolerances::millimetres())?.is_some())
6352}
6353
6354/// A half space resolved into the solid the operation can act on.
6355///
6356/// A planar boundary becomes a box filling the material side, sized past
6357/// the other argument's whole reach. The box's plane-side face is
6358/// *coplanar with the boundary itself*, so the cut the caller sees is the
6359/// exact plane; its far faces stand outside everything the other shape
6360/// reaches and never appear in the result. A curved boundary is resolved
6361/// by [`half_space::resolved`]. A shape that is not a half space passes
6362/// through untouched.
6363fn resolved_half_space(
6364    model: &mut Model,
6365    shape: &Shape,
6366    other: &Shape,
6367    tol: Tolerances,
6368) -> OgeomResult<Shape> {
6369    let Some(face) = half_space::half_space_face(model, shape, tol)? else {
6370        return Ok(shape.clone());
6371    };
6372    let planar = model
6373        .node(&face)
6374        .and_then(|n| n.data().as_face())
6375        .and_then(|d| model.geometry().surface(d.surface))
6376        .is_some_and(|s| matches!(s, SurfaceGeometry::Plane(_)));
6377    if !planar {
6378        return half_space::resolved(model, &face, other, tol);
6379    }
6380    // The boundary's outward normal points away from the material.
6381    let (at, outward) = ogeom_algo::face_normal(model, &face, tol)?;
6382    let bound = ogeom_algo::shape_bounds(model, other, tol)?;
6383    let Some(centre) = bound.centre() else {
6384        ogeom_bail!(
6385            Construction,
6386            "the other argument has no bound to fill against"
6387        );
6388    };
6389    let reach = bound.diagonal().max(tol.confusion() * 1e3) * 2.0;
6390    let into = ogeom_math::Direction::new(-outward, tol)?;
6391    let foot = centre - outward * outward.dot(centre - at);
6392    let seed = if into.vector().x.abs() < 0.9 {
6393        ogeom_math::Vector::new(1.0, 0.0, 0.0)
6394    } else {
6395        ogeom_math::Vector::new(0.0, 1.0, 0.0)
6396    };
6397    let frame_x = ogeom_math::Direction::from_cross(into.vector(), seed, tol)?;
6398    let oriented = ogeom_math::Frame::new(foot, into, frame_x, tol)?;
6399    let corner =
6400        foot - oriented.x().vector() * (reach / 2.0) - oriented.y().vector() * (reach / 2.0);
6401    let placed = ogeom_math::Frame::new(corner, into, frame_x, tol)?;
6402    Ok(ogeom_algo::make_box(model, placed, (reach, reach, reach), tol)?.shape)
6403}
6404
6405/// The union of two solids.
6406///
6407/// # Errors
6408///
6409/// [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone) for configurations the
6410/// boolean refuses: tangential or same-domain contact, scaled placements;
6411/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) for arguments
6412/// that are not closed solids.
6413pub fn fuse(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
6414    if is_half_space(model, a)? || is_half_space(model, b)? {
6415        ogeom_bail!(
6416            Construction,
6417            "the union with a half space is unbounded; a half space serves cut, \
6418             common and section"
6419        );
6420    }
6421    let (a, b) = (
6422        &baked_if_scaled(model, a, tol)?,
6423        &baked_if_scaled(model, b, tol)?,
6424    );
6425    let fused = general_fuse(model, a, b, tol)?;
6426    // Outward pieces bound the union; a same-domain pair with aligned
6427    // material keeps one copy, and one with opposed material is interior to
6428    // the union and vanishes.
6429    let kept: Vec<(usize, bool)> = fused
6430        .pieces
6431        .iter()
6432        .enumerate()
6433        .filter(|(_, p)| {
6434            p.state == PieceState::Out || (p.state == PieceState::OnAligned && !p.covered)
6435        })
6436        .map(|(i, _)| (i, false))
6437        .collect();
6438    assemble_result(model, &fused, &kept, a, b, tol)
6439}
6440
6441/// The intersection of two solids.
6442///
6443/// # Errors
6444///
6445/// As [`fuse`].
6446pub fn common(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
6447    let (a, b) = (
6448        &resolved_half_space(model, a, b, tol)?,
6449        &resolved_half_space(model, b, a, tol)?,
6450    );
6451    let (a, b) = (
6452        &baked_if_scaled(model, a, tol)?,
6453        &baked_if_scaled(model, b, tol)?,
6454    );
6455    let fused = general_fuse(model, a, b, tol)?;
6456    // Inward pieces bound the intersection; an aligned same-domain pair
6457    // bounds it too, once. An opposed pair encloses no volume between them.
6458    let kept: Vec<(usize, bool)> = fused
6459        .pieces
6460        .iter()
6461        .enumerate()
6462        .filter(|(_, p)| {
6463            p.state == PieceState::In || (p.state == PieceState::OnAligned && !p.covered)
6464        })
6465        .map(|(i, _)| (i, false))
6466        .collect();
6467    assemble_result(model, &fused, &kept, a, b, tol)
6468}
6469
6470/// The first solid with the second removed.
6471///
6472/// The pieces of `b` that close the cut into `a`'s material bound the removed
6473/// volume from `b`'s side, so they join the result with their material side
6474/// flipped.
6475///
6476/// # Errors
6477///
6478/// As [`fuse`].
6479pub fn cut(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
6480    let (a, b) = (
6481        &resolved_half_space(model, a, b, tol)?,
6482        &resolved_half_space(model, b, a, tol)?,
6483    );
6484    let (a, b) = (
6485        &baked_if_scaled(model, a, tol)?,
6486        &baked_if_scaled(model, b, tol)?,
6487    );
6488    let fused = general_fuse(model, a, b, tol)?;
6489    // The first argument's outward pieces stay; the tool's inward pieces
6490    // close the cut with their material side flipped. On the shared surface:
6491    // an opposed pair means the tool's material is entirely on the other
6492    // side, so the first argument's face survives untouched; an aligned pair
6493    // means the tool's material backs the same wall, which the cut removes.
6494    let kept: Vec<(usize, bool)> = fused
6495        .pieces
6496        .iter()
6497        .enumerate()
6498        .filter_map(|(i, p)| match (p.from_a, p.state) {
6499            (true, PieceState::Out) => Some((i, false)),
6500            (true, PieceState::OnOpposed) => Some((i, false)),
6501            (false, PieceState::In) => Some((i, true)),
6502            _ => None,
6503        })
6504        .collect();
6505    assemble_result(model, &fused, &kept, a, b, tol)
6506}
6507
6508/// The edges where the two solids' boundaries cross.
6509///
6510/// # Errors
6511///
6512/// As [`fuse`].
6513pub fn section(model: &mut Model, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<Built> {
6514    let (a, b) = (
6515        &resolved_half_space(model, a, b, tol)?,
6516        &resolved_half_space(model, b, a, tol)?,
6517    );
6518    let (a, b) = (
6519        &baked_if_scaled(model, a, tol)?,
6520        &baked_if_scaled(model, b, tol)?,
6521    );
6522    let fused = general_fuse(model, a, b, tol)?;
6523    // Every section sub-edge that survived into some piece's ring, built
6524    // once per distinct sub-range.
6525    let mut wanted: Vec<(usize, (f64, f64))> = Vec::new();
6526    for piece in &fused.pieces {
6527        for ring in &piece.rings {
6528            for traversal in ring {
6529                if let Tag::Section { section, range } = &traversal.tag {
6530                    let near = |a: (f64, f64), b: (f64, f64)| {
6531                        (a.0 - b.0).abs() <= tol.parametric()
6532                            && (a.1 - b.1).abs() <= tol.parametric()
6533                    };
6534                    if !wanted.iter().any(|(s, r)| s == section && near(*r, *range)) {
6535                        wanted.push((*section, *range));
6536                    }
6537                }
6538            }
6539        }
6540    }
6541    let mut history = History::new();
6542    let mut edges = Vec::new();
6543    for (si, range) in wanted {
6544        let s = &fused.sections[si];
6545        let domain = s.curve.domain();
6546        let (f0, f1) = folded_range(range, domain, s.closed);
6547        let from = s.curve.point_at(at_param(f0, domain, s.closed), tol)?;
6548        let to = s.curve.point_at(at_param(f1, domain, s.closed), tol)?;
6549        let v0 = make_vertex(model, from).shape;
6550        let v1 = make_vertex(model, to).shape;
6551        edges.push(make_edge_between(model, s.curve.clone(), (f0, f1), &v0, &v1, tol)?.shape);
6552    }
6553    // Contacts are not crossings, so no piece's ring carries them and the
6554    // loop above cannot see them, but a section through a tangency has a
6555    // curve in it, and this is where it comes from.
6556    for contact in &fused.tangents {
6557        for (lo, hi) in contact_intervals(&fused, contact, tol)? {
6558            let from = contact.curve.point_at(lo, tol)?;
6559            let to = contact.curve.point_at(hi, tol)?;
6560            let v0 = make_vertex(model, from).shape;
6561            let v1 = if from.distance(to) <= tol.confusion() {
6562                v0.clone()
6563            } else {
6564                make_vertex(model, to).shape
6565            };
6566            edges.push(
6567                make_edge_between(model, contact.curve.clone(), (lo, hi), &v0, &v1, tol)?.shape,
6568            );
6569        }
6570    }
6571    let result = model.add_compound(&edges)?;
6572    history.modify(a, result.clone());
6573    history.modify(b, result.clone());
6574    Ok(Built::new(result, history))
6575}
6576
6577#[cfg(test)]
6578#[allow(clippy::unwrap_used)]
6579mod tests {
6580    use super::*;
6581    use ogeom_algo::{check, make_box, make_cylinder, volume_properties};
6582    use ogeom_math::{Direction, Frame};
6583    use ogeom_mesh::Deflection;
6584
6585    const T: Tolerances = Tolerances::millimetres();
6586    const PI: f64 = core::f64::consts::PI;
6587
6588    #[test]
6589    fn coincidence_is_measured_over_the_overlap_and_nowhere_else() {
6590        // Patches restated from planes: the geometry no longer says "plane",
6591        // which is the whole reason this measurement exists.
6592        let patch = |plane: ogeom_math::Plane, u: (f64, f64), v: (f64, f64)| {
6593            let surface: SurfaceGeometry =
6594                ogeom_geom::PlaneSurface::over(plane, u, v).unwrap().into();
6595            SurfaceGeometry::from(surface.to_bspline(T).unwrap())
6596        };
6597        let reach = T.confusion() * 1e2;
6598
6599        // Two windows on one plane, overlapping over a quarter of each. They
6600        // are the same surface exactly where they meet, which is the claim.
6601        let here = patch(ogeom_math::Plane::XY, (0.0, 10.0), (0.0, 10.0));
6602        let over = patch(ogeom_math::Plane::XY, (5.0, 15.0), (5.0, 15.0));
6603        assert!(surfaces_coincide(&here, &over, reach, T));
6604
6605        // The same plane lifted clear of itself is not the same surface, and
6606        // a plane square to it crosses rather than coincides: the case that
6607        // must keep marching, since a crossing has a section to find.
6608        let above = patch(
6609            ogeom_math::Plane::new(
6610                Frame::new(Point::new(0.0, 0.0, 1.0), Direction::Z, Direction::X, T).unwrap(),
6611            ),
6612            (0.0, 10.0),
6613            (0.0, 10.0),
6614        );
6615        assert!(!surfaces_coincide(&here, &above, reach, T));
6616        let across = patch(
6617            ogeom_math::Plane::new(
6618                Frame::new(Point::new(5.0, 0.0, 0.0), Direction::X, Direction::Y, T).unwrap(),
6619            ),
6620            (0.0, 10.0),
6621            (0.0, 10.0),
6622        );
6623        assert!(!surfaces_coincide(&here, &across, reach, T));
6624    }
6625
6626    fn frame_at(origin: Point) -> Frame {
6627        Frame::new(origin, Direction::Z, Direction::X, T).unwrap()
6628    }
6629
6630    fn boxes(model: &mut Model) -> (Shape, Shape) {
6631        // Overlapping in the corner cube [1,2]^3.
6632        let a = make_box(model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
6633        let b = make_box(
6634            model,
6635            frame_at(Point::new(1.0, 1.0, 1.0)),
6636            (2.0, 2.0, 2.0),
6637            T,
6638        )
6639        .unwrap();
6640        (a.shape, b.shape)
6641    }
6642
6643    fn volume(model: &Model, shape: &Shape) -> f64 {
6644        let fine = Deflection {
6645            chord: 1e-3,
6646            ..Deflection::default()
6647        };
6648        volume_properties(model, shape, fine, T).unwrap().mass
6649    }
6650
6651    fn assert_valid(model: &Model, shape: &Shape) {
6652        let diagnosis = check(model, shape, T).unwrap();
6653        assert!(
6654            diagnosis.is_valid(),
6655            "the result fails validity: {:?}",
6656            diagnosis.problems
6657        );
6658    }
6659
6660    #[test]
6661    fn fuse_of_overlapping_boxes_has_the_inclusion_exclusion_volume() {
6662        let mut model = Model::new();
6663        let (a, b) = boxes(&mut model);
6664        let fused = fuse(&mut model, &a, &b, T).unwrap();
6665        assert_valid(&model, &fused.shape);
6666        assert!((volume(&model, &fused.shape) - 15.0).abs() < 1e-9);
6667        assert_eq!(
6668            fused.history.modified(&a),
6669            std::slice::from_ref(&fused.shape)
6670        );
6671    }
6672
6673    #[test]
6674    fn common_of_overlapping_boxes_is_the_overlap_cube() {
6675        let mut model = Model::new();
6676        let (a, b) = boxes(&mut model);
6677        let result = common(&mut model, &a, &b, T).unwrap();
6678        assert_valid(&model, &result.shape);
6679        assert!((volume(&model, &result.shape) - 1.0).abs() < 1e-9);
6680    }
6681
6682    #[test]
6683    fn cut_removes_the_overlap_from_the_first_argument() {
6684        let mut model = Model::new();
6685        let (a, b) = boxes(&mut model);
6686        let result = cut(&mut model, &a, &b, T).unwrap();
6687        assert_valid(&model, &result.shape);
6688        assert!((volume(&model, &result.shape) - 7.0).abs() < 1e-9);
6689    }
6690
6691    #[test]
6692    fn section_of_overlapping_boxes_is_the_six_segment_seam() {
6693        let mut model = Model::new();
6694        let (a, b) = boxes(&mut model);
6695        let result = section(&mut model, &a, &b, T).unwrap();
6696        let edges = explore(&model, &result.shape, Filter::OfType(ShapeType::Edge)).unwrap();
6697        assert_eq!(edges.len(), 6);
6698    }
6699
6700    #[test]
6701    fn fuse_of_disjoint_boxes_is_a_compound_of_both() {
6702        let mut model = Model::new();
6703        let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
6704        let b = make_box(
6705            &mut model,
6706            frame_at(Point::new(5.0, 0.0, 0.0)),
6707            (2.0, 2.0, 2.0),
6708            T,
6709        )
6710        .unwrap();
6711        let fused = fuse(&mut model, &a.shape, &b.shape, T).unwrap();
6712        assert_eq!(model.kind_of(&fused.shape).unwrap(), ShapeType::Compound);
6713        assert!((volume(&model, &fused.shape) - 16.0).abs() < 1e-9);
6714    }
6715
6716    #[test]
6717    fn cutting_a_through_post_leaves_a_slab_with_a_hole() {
6718        let mut model = Model::new();
6719        let slab = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
6720        let post = make_box(
6721            &mut model,
6722            frame_at(Point::new(1.5, 1.5, -1.0)),
6723            (1.0, 1.0, 3.0),
6724            T,
6725        )
6726        .unwrap();
6727        let result = cut(&mut model, &slab.shape, &post.shape, T).unwrap();
6728        assert_valid(&model, &result.shape);
6729        assert!((volume(&model, &result.shape) - 15.0).abs() < 1e-9);
6730    }
6731
6732    #[test]
6733    fn common_with_a_contained_box_is_that_box() {
6734        let mut model = Model::new();
6735        let outer = make_box(&mut model, Frame::WORLD, (6.0, 6.0, 6.0), T).unwrap();
6736        let inner = make_box(
6737            &mut model,
6738            frame_at(Point::new(2.0, 2.0, 2.0)),
6739            (2.0, 2.0, 2.0),
6740            T,
6741        )
6742        .unwrap();
6743        let result = common(&mut model, &outer.shape, &inner.shape, T).unwrap();
6744        assert_valid(&model, &result.shape);
6745        assert!((volume(&model, &result.shape) - 8.0).abs() < 1e-9);
6746    }
6747
6748    #[test]
6749    fn cutting_everything_away_leaves_an_empty_compound() {
6750        let mut model = Model::new();
6751        let outer = make_box(&mut model, Frame::WORLD, (6.0, 6.0, 6.0), T).unwrap();
6752        let inner = make_box(
6753            &mut model,
6754            frame_at(Point::new(2.0, 2.0, 2.0)),
6755            (2.0, 2.0, 2.0),
6756            T,
6757        )
6758        .unwrap();
6759        let result = cut(&mut model, &inner.shape, &outer.shape, T).unwrap();
6760        assert_eq!(model.kind_of(&result.shape).unwrap(), ShapeType::Compound);
6761        assert!(
6762            explore(&model, &result.shape, Filter::OfType(ShapeType::Face))
6763                .unwrap()
6764                .is_empty()
6765        );
6766    }
6767
6768    #[test]
6769    fn drilling_a_box_leaves_a_cylindrical_hole() {
6770        // The curved milestone: box minus a through-post cylinder. The box's
6771        // top and bottom faces come back with *circular* holes, the hole's
6772        // wall is the cylinder's own surface with its material side flipped,
6773        // and the cylinder's seam and both section circles all had to split
6774        // and sew for the shell to close.
6775        let mut model = Model::new();
6776        let block = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
6777        let drill = make_cylinder(
6778            &mut model,
6779            frame_at(Point::new(2.0, 2.0, -1.0)),
6780            0.5,
6781            3.0,
6782            T,
6783        )
6784        .unwrap();
6785        let result = cut(&mut model, &block.shape, &drill.shape, T).unwrap();
6786        assert_valid(&model, &result.shape);
6787        let exact = 16.0 - PI * 0.25;
6788        let got = volume(&model, &result.shape);
6789        assert!(
6790            (got - exact).abs() / exact < 2e-3,
6791            "volume {got} against {exact}"
6792        );
6793    }
6794
6795    #[test]
6796    fn common_of_a_box_and_a_cylinder_is_the_post_inside_it() {
6797        let mut model = Model::new();
6798        let block = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
6799        let post = make_cylinder(
6800            &mut model,
6801            frame_at(Point::new(2.0, 2.0, -1.0)),
6802            0.5,
6803            3.0,
6804            T,
6805        )
6806        .unwrap();
6807        let result = common(&mut model, &block.shape, &post.shape, T).unwrap();
6808        assert_valid(&model, &result.shape);
6809        let exact = PI * 0.25;
6810        let got = volume(&model, &result.shape);
6811        assert!(
6812            (got - exact).abs() / exact < 2e-3,
6813            "volume {got} against {exact}"
6814        );
6815    }
6816
6817    #[test]
6818    fn fusing_a_post_onto_a_slab_adds_what_stands_proud() {
6819        let mut model = Model::new();
6820        let slab = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
6821        let post = make_cylinder(
6822            &mut model,
6823            frame_at(Point::new(2.0, 2.0, -1.0)),
6824            0.5,
6825            3.0,
6826            T,
6827        )
6828        .unwrap();
6829        let result = fuse(&mut model, &slab.shape, &post.shape, T).unwrap();
6830        assert_valid(&model, &result.shape);
6831        let exact = PI.mul_add(0.25 * 3.0, 16.0) - PI * 0.25;
6832        let got = volume(&model, &result.shape);
6833        assert!(
6834            (got - exact).abs() / exact < 2e-3,
6835            "volume {got} against {exact}"
6836        );
6837    }
6838
6839    #[test]
6840    fn crossed_cylinders_run_through_the_marched_sections() {
6841        // No closed form exists for cylinder/cylinder: the sections are
6842        // marched and fitted, and every stage downstream (paves against
6843        // seams on both charts, splitting, classification, rebuilding with
6844        // fitted pcurves, sewing, meshing) has to work within the fit's
6845        // stated budget. Unequal radii keep the tangential branch points
6846        // away. The volumes have no easy closed form, so the operations are
6847        // held to each other: fuse = A + B - common and cut = A - common are
6848        // identities whatever the shapes.
6849        let mut model = Model::new();
6850        let upright = make_cylinder(&mut model, Frame::WORLD, 1.0, 4.0, T).unwrap();
6851        let across_frame =
6852            Frame::new(Point::new(-2.0, 0.0, 2.0), Direction::X, Direction::Y, T).unwrap();
6853        let across = make_cylinder(&mut model, across_frame, 0.6, 4.0, T).unwrap();
6854
6855        let both = fuse(&mut model, &upright.shape, &across.shape, T).unwrap();
6856        assert_valid(&model, &both.shape);
6857        let shared = common(&mut model, &upright.shape, &across.shape, T).unwrap();
6858        assert_valid(&model, &shared.shape);
6859        let pierced = cut(&mut model, &upright.shape, &across.shape, T).unwrap();
6860        assert_valid(&model, &pierced.shape);
6861
6862        let va = volume(&model, &upright.shape);
6863        let vb = volume(&model, &across.shape);
6864        let vf = volume(&model, &both.shape);
6865        let vc = volume(&model, &shared.shape);
6866        let vx = volume(&model, &pierced.shape);
6867
6868        assert!(vc > 0.0 && vc < vb, "the overlap is real and partial: {vc}");
6869        assert!(
6870            (vf - (va + vb - vc)).abs() / vf < 2e-3,
6871            "fuse {vf} against A + B - common {}",
6872            va + vb - vc
6873        );
6874        assert!(
6875            (vx - (va - vc)).abs() / vx < 2e-3,
6876            "cut {vx} against A - common {}",
6877            va - vc
6878        );
6879    }
6880
6881    #[test]
6882    fn stacked_boxes_fuse_into_one_solid_and_the_contact_vanishes() {
6883        // Same-domain contact with opposed materials: the shared rectangle is
6884        // interior to the union and no face of the result may carry it.
6885        let mut model = Model::new();
6886        let lower = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 1.0), T).unwrap();
6887        let upper = make_box(
6888            &mut model,
6889            frame_at(Point::new(0.0, 0.0, 1.0)),
6890            (2.0, 2.0, 1.0),
6891            T,
6892        )
6893        .unwrap();
6894        let fused = fuse(&mut model, &lower.shape, &upper.shape, T).unwrap();
6895        assert_valid(&model, &fused.shape);
6896        assert_eq!(model.kind_of(&fused.shape).unwrap(), ShapeType::Solid);
6897        assert!((volume(&model, &fused.shape) - 8.0).abs() < 1e-9);
6898    }
6899
6900    #[test]
6901    fn a_small_box_on_a_big_one_fuses_with_a_partial_contact() {
6902        // The big top face splits into the contact rectangle (swallowed)
6903        // and the surround, which stays and must sew to the small box's
6904        // walls along the contact's edges.
6905        let mut model = Model::new();
6906        let big = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
6907        let small = make_box(
6908            &mut model,
6909            frame_at(Point::new(1.0, 1.0, 1.0)),
6910            (2.0, 2.0, 1.0),
6911            T,
6912        )
6913        .unwrap();
6914        let fused = fuse(&mut model, &big.shape, &small.shape, T).unwrap();
6915        assert_valid(&model, &fused.shape);
6916        assert!((volume(&model, &fused.shape) - 20.0).abs() < 1e-9);
6917
6918        // Cutting the same pair removes nothing but the measure-zero contact:
6919        // the big box survives whole, its top face's contact piece intact.
6920        let mut model = Model::new();
6921        let big = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
6922        let small = make_box(
6923            &mut model,
6924            frame_at(Point::new(1.0, 1.0, 1.0)),
6925            (2.0, 2.0, 1.0),
6926            T,
6927        )
6928        .unwrap();
6929        let result = cut(&mut model, &big.shape, &small.shape, T).unwrap();
6930        assert_valid(&model, &result.shape);
6931        assert!((volume(&model, &result.shape) - 16.0).abs() < 1e-9);
6932    }
6933
6934    #[test]
6935    fn flush_walls_fuse_cut_and_meet_with_aligned_contact() {
6936        // Overlapping boxes sharing flush walls: same-domain contact with
6937        // *aligned* materials. A = [0,2]^3, B = [1,3]x[0,2]x[0,2]: the y and
6938        // z walls of the overlap are coplanar with aligned outward normals.
6939        let mut model = Model::new();
6940        let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
6941        let b = make_box(
6942            &mut model,
6943            frame_at(Point::new(1.0, 0.0, 0.0)),
6944            (2.0, 2.0, 2.0),
6945            T,
6946        )
6947        .unwrap();
6948        let fused = fuse(&mut model, &a.shape, &b.shape, T).unwrap();
6949        assert_valid(&model, &fused.shape);
6950        assert!((volume(&model, &fused.shape) - 12.0).abs() < 1e-9);
6951
6952        let mut model = Model::new();
6953        let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
6954        let b = make_box(
6955            &mut model,
6956            frame_at(Point::new(1.0, 0.0, 0.0)),
6957            (2.0, 2.0, 2.0),
6958            T,
6959        )
6960        .unwrap();
6961        let shared = common(&mut model, &a.shape, &b.shape, T).unwrap();
6962        assert_valid(&model, &shared.shape);
6963        assert!((volume(&model, &shared.shape) - 4.0).abs() < 1e-9);
6964
6965        let mut model = Model::new();
6966        let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
6967        let b = make_box(
6968            &mut model,
6969            frame_at(Point::new(1.0, 0.0, 0.0)),
6970            (2.0, 2.0, 2.0),
6971            T,
6972        )
6973        .unwrap();
6974        let cut_result = cut(&mut model, &a.shape, &b.shape, T).unwrap();
6975        assert_valid(&model, &cut_result.shape);
6976        assert!((volume(&model, &cut_result.shape) - 4.0).abs() < 1e-9);
6977    }
6978
6979    #[test]
6980    fn every_source_face_is_accounted_for_in_the_history() {
6981        let mut model = Model::new();
6982        let (a, b) = boxes(&mut model);
6983        let result = cut(&mut model, &a, &b, T).unwrap();
6984        for face in explore(&model, &a, Filter::OfType(ShapeType::Face)).unwrap() {
6985            assert!(
6986                result.history.is_affected(&face),
6987                "a face of the first argument vanished from the history"
6988            );
6989        }
6990    }
6991}