Skip to main content

brep_kernel/brep/
soundness.rs

1//! # Structural soundness — the checks `validate()` deliberately does NOT make
2//!
3//! [`BrepSolid::validate`](crate::BrepSolid::validate) is an
4//! **incidence** test: ids resolve, loops close, an edge is used twice with
5//! opposite senses, a pcurve tracks its edge, Euler closes. A great deal of
6//! code reads "validate passed" as exactly that claim, so this module does not
7//! widen it. What `validate()` never asks:
8//!
9//! * **Is the thing connected?** Nothing in the incidence test notices that a
10//!   shell's faces fall into two mutually unreachable pieces. Every local
11//!   incidence can be perfect while the "solid" is two solids in one record.
12//! * **Does it pass through itself?** `validate()`'s one string carrying the
13//!   words "self-intersects" is a *uv-wire* warning — one loop's sampled
14//!   pcurve polygon crossing itself in parameter space. It is not, and never
15//!   was, a face-versus-face test.
16//! * **Does a single face's own BOUNDARY pass through itself?** That is what
17//!   the uv-wire warning above is about — and `validate()` does not return it.
18//!   It goes to [`ValidationReport::wire_warnings`](crate::ValidationReport),
19//!   and `validate()` hands back `issues`, so every caller in the kernel that
20//!   reads `validate().is_empty()` has never seen that verdict. It was also
21//!   measured blind to the symmetric case (see [`loop_self_crossings`]).
22//!
23//! Both live here as SEPARATE detectors, following the precedent set by
24//! `csg/oracle.rs`: an independent wrongness detector that a caller asks for,
25//! never a silent widening of an existing verdict. Nothing in the kernel's own
26//! build path calls either of them; what a CALLER does with the verdict — gate
27//! or record — is the table below, and it is the case gate that acts on it.
28//!
29//! ## Gate or diagnostic — the standing decision
30//!
31//! | check | cost | verdict |
32//! |---|---|---|
33//! | [`solid_connectivity`] — per-shell edge-connected components, cross-shell edges | pure topology, one pass over the coedges, no geometry | **hard gate.** A split shell is never acceptable and the test cannot be noisy: it reads ids, not numbers. [`ConnectivityReport::is_disconnected`] is the gate predicate. |
34//! | `ConnectivityReport::pinch_vertices` — vertices whose incident faces form more than one fan | same pass | **diagnostic.** A pinch is a real non-manifold defect, but pole/seam vertices are false-positive bait, so it is reported beside the gate rather than inside it. Vertices with an incident degenerate edge are skipped outright and counted in `pinch_skipped`. |
35//! | [`loop_self_crossings`] — one face loop crossing ITSELF | samples each trim at the density the containment scan uses and sweeps the chords; no tessellation, no surface evaluation until a hit | **hard gate.** [`LoopCrossingReport::is_flagged`] is the predicate. A bowtie is not a tolerance question: a face's boundary either passes through itself in its own domain or it does not, and no valid face has one. The whole case population reads zero, the blend acceptance refuses a result that does not, and the case gate diffs the count per solid. |
36//! | [`face_self_intersections`] — ONE face folded through itself in 3D | the SAME tessellation and BVH sweep as the row below — it is that scan's same-face half, confirmed rather than counted | **hard gate.** A face whose surface carries two distinct trimmed parameter feet to one 3D point is wrong however its volume reads, and no valid face has one. Added 2026-09-13; the case gate diffs the count per solid, and the offset-shell, thicken, face-offset, sweep, loft and boolean acceptances resolve or refuse a result that has one. |
37//! | [`shell_vector_areas`] — a CLOSED shell's TRIMS enclosing the zero vector area they owe | one pass over the trims and the edge curves, at the quadrature the mass integrals use; no tessellation, no projection, no surface-surface work | **gate for a NEW crossing of the bar,** and a measurement otherwise. `Σ_faces ∫ n dA = 0` is exact on a closed shell — every edge is walked twice, once each way — so the residual is the sum of the trims' departures from the edges they claim, which is the accuracy floor under every mass property and which nothing in the kernel read before 2026-09-16. The bar is derived, not chosen: `PCURVE_REFINEMENT_TOLERANCE` times the shell's edge length. The case gate records the residual per solid and fails a solid that goes over its bar when its baseline was inside it. |
38//! | [`solid_self_intersections`] — face-versus-face crossing | tessellates the solid and walks a triangle BVH | **gate for NEW occurrences.** [`SelfIntersectionReport::is_flagged`] is the predicate. It shipped opt-in because it costs a full tessellation plus a BVH sweep; the decision was reversed once the first corpus run found a case labelled `correct` whose faces cross four orders of magnitude past the band, because a gate that fires only when someone remembers a flag is a diagnostic with extra steps. The case gate now runs it by default, reports a crossing its baseline already holds as a note, and fails a crossing on any other face pair. |
39//!
40//! ## How the self-intersection test avoids being noisy
41//!
42//! A mesh-only test flags every tangent neighbour: two faces meeting G1 along
43//! a shared edge chord-cross each other one triangle row in, and a fillet
44//! corpus is nothing but tangent neighbours. Two rules keep this one quiet:
45//!
46//! 1. **Welded-vertex skip.** The watertight tessellator pins shared-edge
47//!    samples to bit-identical positions, so triangles that meet along a
48//!    shared edge share a welded vertex and are never compared.
49//! 2. **Exact-surface confirmation.** A mesh crossing is only a *candidate*.
50//!    It is confirmed by projecting each triangle's corners onto the OTHER
51//!    face's carrier surface and requiring the signed distances to straddle
52//!    zero by more than [`SelfIntersectionOptions::straddle_band`]. Every
53//!    tessellation vertex lies exactly on its own surface, so chord error
54//!    cannot manufacture a straddle: a tangent neighbour's corners all sit on
55//!    one side of the other carrier, while a genuine piercing has corners on
56//!    both. The confirmation runs only on BVH+tri-tri survivors, so it is
57//!    paid a handful of times per solid, not per triangle.
58//!
59//! ## The same-face case, and why it is a different question
60//!
61//! Only DISTINCT face pairs can be confirmed that way. When both triangles lie
62//! on ONE face the straddle test is degenerate by construction — every corner
63//! of both triangles is on that carrier at distance zero — so for a long time
64//! a single face folding through itself was only COUNTED, in
65//! [`SelfIntersectionReport::same_face_candidates`].
66//!
67//! [`face_self_intersections`] closes that, by asking the question the fold
68//! actually is, in parameter space: the surface carries two DISTINCT trimmed
69//! feet to ONE point in space. Each triangle is seeded from its own three
70//! corners, and then every branch of the surface that comes near that point is
71//! ENUMERATED from a sample grid over the face's own domain and refined
72//! through the branch-preserving Newton `project_point_to_surface_seeded` —
73//! the same instrument the pcurve tracer uses to stay on one branch of a
74//! surface that folds back. A global projection cannot do this job and is not
75//! asked to: measured on the ribbon fixture, `project_point_to_surface` put a
76//! point lying EXACTLY on the surface 2.766 away from its own foot. A crossing
77//! is confirmed only when two DISTINCT feet reach the point, both inside the
78//! trim, with non-parallel normals. The last two rules are what keep it quiet:
79//! chord error on a tight curve leaves ONE foot however many seeds are
80//! refined, and a degenerate parameterization (a sphere's pole, a seam)
81//! carries distinct uv to one point with one tangent plane. Two triangles of
82//! one face that merely TOUCH — a
83//! carved offset image pinching to the apex of a cone, a torus sheet meeting
84//! its own axis point — never reach the confirmation at all: they either share
85//! a welded vertex or their crossing segment has zero length.
86//!
87//! What it does NOT ask: a face that lies ON itself over an area (a coplanar
88//! double cover) rather than crossing, since a coplanar triangle pair returns
89//! no crossing segment; and a fold whose whole crossing region is finer than
90//! the tessellation's chord tolerance, which this — like every check keyed to
91//! a mesh — resolves nothing below.
92//!
93//! ## The one that is not a shape question at all
94//!
95//! Every check above asks about a PLACE — a face, a pair of faces, a loop.
96//! [`shell_vector_areas`] asks about a sum: a closed shell's faces enclose
97//! zero vector area, whatever shape they are, so any residual is error and
98//! the only question is whose. It is the check that notices a trim lying off
99//! the edge it claims while every other verdict here is silent — the
100//! incidence is perfect, the topology is untouched, no face crosses anything,
101//! and the volume is an integral over exactly those trims, so it moves
102//! without anything to compare it against. Three such defects on one user
103//! solid (the notched cap, 2026-09-16) sat between 5e-7 and 1.9e-5 while
104//! `validate()`'s own pcurve-versus-edge bar was 4e-3, and the solid's volume
105//! matched its closed form because two of them cancelled.
106//!
107//! [`loop_self_crossings`] is that blind spot covered, in parameter space
108//! where the question is exact. It was added on 2026-09-13 after the
109//! rib-spine partition composed a solid whose two rib side faces each had a
110//! loop crossing itself twice and EVERY check above passed it: `validate()`
111//! silent, one connected component, no pinch, χ = 2, and the crossing scan 0
112//! confirmed / 0 rejected / 0 undecided / **0 same-face candidates** over 3900
113//! triangles — because both inverted lobes lie in the face's own plane, so
114//! there is no surface crossing to find at all. The face's AREA was the only
115//! other tell.
116//!
117//! What that detector in turn does not ask: whether two DIFFERENT loops of one
118//! face cross each other (a hole loop escaping its outer loop), and whether a
119//! loop runs back along itself without crossing (collinear overlap — which is
120//! what a periodic face's seam looks like, traversed once each way, so it
121//! cannot be convicted here).
122
123use crate::arrangement::Vec2;
124use crate::classification::{parameter_point_in_face, PolygonClass};
125use crate::projection::{project_point_to_surface, project_point_to_surface_seeded};
126use crate::spatial::{Aabb, Bvh};
127use crate::mass_properties::{split_at_surface_breaks, surface_breaks, GAUSS_W, GAUSS_X};
128use crate::topology::{BrepSolid, FaceRecord, LoopRecord};
129use crate::{NurbsCurve, NurbsSurface, Vec3};
130use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
131use serde::Serialize;
132use web_time::Instant;
133
134#[path = "soundness/sphere_certificate.rs"]
135mod sphere_certificate;
136
137#[path = "soundness/nurbs_certificate.rs"]
138mod nurbs_certificate;
139
140#[path = "soundness/census.rs"]
141pub(crate) mod census;
142#[path = "soundness/pair_certificate.rs"]
143mod pair_certificate;
144#[path = "soundness/pair_memo.rs"]
145pub(crate) mod pair_memo;
146#[path = "soundness/fold_memo.rs"]
147pub(crate) mod fold_memo;
148#[path = "soundness/analytic_body.rs"]
149mod analytic_body;
150#[path = "soundness/tube_grid.rs"]
151mod tube_grid;
152#[path = "soundness/resolve.rs"]
153pub(crate) mod resolve;
154pub use resolve::{CrossingUndecidedCause, UndecidedCrossing};
155#[path = "soundness/blend_patch.rs"]
156mod blend_patch;
157#[path = "soundness/blend_curved.rs"]
158mod blend_curved;
159
160// ---------------------------------------------------------------------------
161// Disjoint set — shared by the shell and the vertex-fan scans
162// ---------------------------------------------------------------------------
163
164struct DisjointSet {
165    parent: Vec<usize>,
166}
167
168impl DisjointSet {
169    fn new(count: usize) -> Self {
170        Self {
171            parent: (0..count).collect(),
172        }
173    }
174
175    fn find(&mut self, mut index: usize) -> usize {
176        while self.parent[index] != index {
177            self.parent[index] = self.parent[self.parent[index]];
178            index = self.parent[index];
179        }
180        index
181    }
182
183    fn union(&mut self, first: usize, second: usize) {
184        let (a, b) = (self.find(first), self.find(second));
185        if a != b {
186            self.parent[b] = a;
187        }
188    }
189}
190
191// ---------------------------------------------------------------------------
192// Connectivity
193// ---------------------------------------------------------------------------
194
195/// One shell's connectivity: how many edge-connected pieces its faces form.
196#[derive(Clone, Debug, Serialize)]
197pub struct ShellConnectivity {
198    pub shell_id: u64,
199    pub faces: usize,
200    /// Edge-connected components of this shell's faces. Anything but 1 means
201    /// the record holds several surfaces under one shell.
202    pub components: usize,
203    /// Face ids of every component EXCEPT the largest, smallest first — the
204    /// pieces that would each have had to be their own shell. Empty when
205    /// `components == 1`.
206    pub detached: Vec<Vec<u64>>,
207}
208
209/// Outcome of [`solid_connectivity`].
210#[derive(Clone, Debug, Serialize)]
211pub struct ConnectivityReport {
212    pub shells: Vec<ShellConnectivity>,
213    /// Edges used by faces belonging to two DIFFERENT shells. A shell is a
214    /// closed surface in its own right; sharing an edge across shells means
215    /// the shell split is fiction.
216    pub cross_shell_edges: Vec<u64>,
217    /// Vertices where the incident face corners form more than one fan — two
218    /// pieces of surface touching at a point and nowhere else. Edge
219    /// connectivity cannot see this: both pieces are edge-connected, just not
220    /// through this vertex. Diagnostic, not part of [`Self::is_disconnected`].
221    pub pinch_vertices: Vec<u64>,
222    /// Vertices the fan scan skipped because a degenerate (pole) edge meets
223    /// there — a collapsed parameter boundary is not an ordinary fan and the
224    /// scan would read it as a pinch.
225    pub pinch_skipped: usize,
226}
227
228impl ConnectivityReport {
229    /// The HARD-GATE predicate: the record claims to be one closed surface per
230    /// shell and it is not. Pinch vertices are deliberately excluded — see the
231    /// module table.
232    pub fn is_disconnected(&self) -> bool {
233        self.shells.iter().any(|shell| shell.components > 1)
234            || !self.cross_shell_edges.is_empty()
235    }
236
237    /// One line naming what is wrong, or `None` when the gate predicate is
238    /// clear. Pinch vertices are appended when present so a caller printing
239    /// the summary still sees them.
240    pub fn summary(&self) -> Option<String> {
241        let mut parts = Vec::new();
242        for shell in &self.shells {
243            if shell.components > 1 {
244                let sizes: Vec<usize> = shell.detached.iter().map(Vec::len).collect();
245                parts.push(format!(
246                    "shell {} splits into {} edge-connected components ({} face(s), detached sizes {sizes:?}, first detached faces {:?})",
247                    shell.shell_id,
248                    shell.components,
249                    shell.faces,
250                    shell.detached.first().map(Vec::as_slice).unwrap_or(&[]),
251                ));
252            }
253        }
254        if !self.cross_shell_edges.is_empty() {
255            parts.push(format!(
256                "{} edge(s) shared across shells ({:?})",
257                self.cross_shell_edges.len(),
258                &self.cross_shell_edges[..self.cross_shell_edges.len().min(8)]
259            ));
260        }
261        if !self.pinch_vertices.is_empty() {
262            parts.push(format!(
263                "{} pinch vertex/vertices ({:?})",
264                self.pinch_vertices.len(),
265                &self.pinch_vertices[..self.pinch_vertices.len().min(8)]
266            ));
267        }
268        if parts.is_empty() {
269            None
270        } else {
271            Some(parts.join("; "))
272        }
273    }
274}
275
276/// Connectivity of a solid's topology: are each shell's faces reachable from
277/// one another across shared edges, do any two shells share an edge, and does
278/// any vertex pinch two otherwise-separate fans together.
279///
280/// Pure topology — ids and use counts, never a coordinate — so it is
281/// tolerance-free and cannot be noisy. Degenerate (pole) edges DO count as
282/// connections: a collapsed boundary shared by two faces genuinely joins them.
283pub fn solid_connectivity(solid: &BrepSolid) -> ConnectivityReport {
284    // Global face indexing over (shell, face) in record order.
285    let mut face_shell: Vec<usize> = Vec::new();
286    let mut face_ids: Vec<u64> = Vec::new();
287    for (shell_index, shell) in solid.shells.iter().enumerate() {
288        for face in &shell.faces {
289            face_shell.push(shell_index);
290            face_ids.push(face.id);
291        }
292    }
293
294    // edge id -> the global face indices that use it.
295    let mut edge_faces: HashMap<u64, Vec<usize>> = HashMap::default();
296    let mut global_index = 0usize;
297    for shell in &solid.shells {
298        for face in &shell.faces {
299            let _ = face;
300            for loop_record in &face.loops {
301                for coedge in &loop_record.coedges {
302                    let users = edge_faces.entry(coedge.edge_id).or_default();
303                    if users.last() != Some(&global_index) {
304                        users.push(global_index);
305                    }
306                }
307            }
308            global_index += 1;
309        }
310    }
311
312    let mut sets = DisjointSet::new(face_ids.len());
313    let mut cross_shell_edges = Vec::new();
314    for (edge_id, users) in &edge_faces {
315        for pair in users.windows(2) {
316            sets.union(pair[0], pair[1]);
317        }
318        let shells_touched: HashSet<usize> =
319            users.iter().map(|index| face_shell[*index]).collect();
320        if shells_touched.len() > 1 {
321            cross_shell_edges.push(*edge_id);
322        }
323    }
324    cross_shell_edges.sort_unstable();
325
326    let mut shells = Vec::with_capacity(solid.shells.len());
327    let mut base = 0usize;
328    for shell in &solid.shells {
329        let count = shell.faces.len();
330        let mut buckets: HashMap<usize, Vec<u64>> = HashMap::default();
331        for offset in 0..count {
332            let root = sets.find(base + offset);
333            buckets.entry(root).or_default().push(face_ids[base + offset]);
334        }
335        let mut groups: Vec<Vec<u64>> = buckets.into_values().collect();
336        // Largest first so `detached` is "everything but the main piece", and
337        // deterministic: ties break on the smallest face id.
338        groups.sort_by(|a, b| b.len().cmp(&a.len()).then(a.first().cmp(&b.first())));
339        let components = groups.len();
340        let mut detached: Vec<Vec<u64>> = groups.into_iter().skip(1).collect();
341        detached.sort_by_key(Vec::len);
342        for group in &mut detached {
343            group.sort_unstable();
344        }
345        shells.push(ShellConnectivity {
346            shell_id: shell.id,
347            faces: count,
348            components,
349            detached,
350        });
351        base += count;
352    }
353
354    let (pinch_vertices, pinch_skipped) = pinch_vertices(solid);
355
356    ConnectivityReport {
357        shells,
358        cross_shell_edges,
359        pinch_vertices,
360        pinch_skipped,
361    }
362}
363
364/// Vertices whose incident face corners form more than one fan.
365///
366/// A *corner* is one consecutive coedge pair of a loop meeting at the vertex.
367/// Each non-degenerate edge at the vertex is used by exactly two coedges, each
368/// belonging to exactly one corner there, so the edge links those two corners.
369/// One fan = one component. Two cones touching at their apex are edge-
370/// connected everywhere else and split into two fans exactly here.
371///
372/// Any vertex with an incident DEGENERATE edge is skipped: a collapsed pole
373/// boundary is not an ordinary fan and would read as a pinch.
374fn pinch_vertices(solid: &BrepSolid) -> (Vec<u64>, usize) {
375    let mut degenerate_at: HashSet<u64> = HashSet::default();
376    for edge in &solid.edges {
377        if edge.degenerate {
378            degenerate_at.insert(edge.start_vertex_id);
379            degenerate_at.insert(edge.end_vertex_id);
380        }
381    }
382    let edges: HashMap<u64, &crate::topology::EdgeRecord> =
383        solid.edges.iter().map(|edge| (edge.id, edge)).collect();
384
385    // vertex id -> corners; a corner is the pair of edge ids meeting there.
386    let mut corners: HashMap<u64, Vec<[u64; 2]>> = HashMap::default();
387    for shell in &solid.shells {
388        for face in &shell.faces {
389            for loop_record in &face.loops {
390                let count = loop_record.coedges.len();
391                if count == 0 {
392                    continue;
393                }
394                for index in 0..count {
395                    let current = &loop_record.coedges[index];
396                    let next = &loop_record.coedges[(index + 1) % count];
397                    let Some(current_edge) = edges.get(&current.edge_id) else {
398                        continue;
399                    };
400                    let vertex = if current.forward {
401                        current_edge.end_vertex_id
402                    } else {
403                        current_edge.start_vertex_id
404                    };
405                    corners
406                        .entry(vertex)
407                        .or_default()
408                        .push([current.edge_id, next.edge_id]);
409                }
410            }
411        }
412    }
413
414    let mut pinched = Vec::new();
415    let mut skipped = 0usize;
416    for (vertex, list) in &corners {
417        if degenerate_at.contains(vertex) {
418            skipped += 1;
419            continue;
420        }
421        if list.len() < 2 {
422            continue;
423        }
424        let mut sets = DisjointSet::new(list.len());
425        let mut by_edge: HashMap<u64, usize> = HashMap::default();
426        for (index, corner) in list.iter().enumerate() {
427            for edge_id in corner {
428                match by_edge.entry(*edge_id) {
429                    std::collections::hash_map::Entry::Occupied(slot) => {
430                        sets.union(*slot.get(), index);
431                    }
432                    std::collections::hash_map::Entry::Vacant(slot) => {
433                        slot.insert(index);
434                    }
435                }
436            }
437        }
438        let fans: HashSet<usize> = (0..list.len()).map(|index| sets.find(index)).collect();
439        if fans.len() > 1 {
440            pinched.push(*vertex);
441        }
442    }
443    pinched.sort_unstable();
444    (pinched, skipped)
445}
446
447// ---------------------------------------------------------------------------
448// Face-versus-face self-intersection
449// ---------------------------------------------------------------------------
450
451/// Fraction of the solid's bounding diagonal a confirmed crossing must
452/// straddle the other carrier by. Tessellation vertices are exact on their own
453/// surface, so this band only has to clear projection/Newton residue, not
454/// chord error; it is deliberately far below any real penetration.
455const STRADDLE_FRACTION: f64 = 1e-6;
456
457/// Fraction of the diagonal a mesh crossing segment must be longer than. A
458/// crossing shorter than this is a corner graze, not a penetration.
459const CROSSING_FRACTION: f64 = 1e-7;
460
461/// Fraction of the diagonal used to weld coincident tessellation vertices.
462/// Shared-edge samples are pinned to identical values by the watertight
463/// tessellator, so this only has to survive being read back out of the mesh
464/// buffers.
465const WELD_FRACTION: f64 = 1e-9;
466
467/// How far from PARALLEL two surface normals must be for a same-face crossing
468/// to be a fold. A degenerate parameterization — a sphere's pole, a seam the
469/// period reduction did not fold away — carries distinct uv to one point with
470/// one tangent plane, and this is what separates that from a surface genuinely
471/// passing through itself. The two mesh triangles already crossed
472/// TRANSVERSALLY (a coplanar pair returns no crossing), so a fold clears this
473/// by orders of magnitude and the constant is a floor, not a threshold.
474const FOLD_TRANSVERSAL_SINE: f64 = 1e-6;
475
476/// `BREP_FOLD_TRACE=1` prints why each same-face candidate was confirmed,
477/// rejected or left undecided, with the numbers behind the verdict. The
478/// attribution a population sweep needs — "which rule turned this hit away,
479/// and by how much" — cannot be reconstructed from the counts.
480fn fold_trace_enabled() -> bool {
481    std::env::var("BREP_FOLD_TRACE").is_ok_and(|value| !value.is_empty() && value != "0")
482}
483
484macro_rules! fold_trace {
485    ($($arg:tt)*) => {
486        if fold_trace_enabled() {
487            eprintln!($($arg)*);
488        }
489    };
490}
491
492/// `BREP_PIERCE_TRACE=1` prints every face-PAIR candidate the corner lane
493/// declined and each step of the pierce lane's answer for it: which edge
494/// pierced which triangle, the two bracket distances, and where the two
495/// samples across the crossing landed and what they read. The fold trace's
496/// counterpart for the other half of the scan. It is asked several times for
497/// every pair the corner lane declines, so it is one `OnceLock` read rather
498/// than an environment lookup each time.
499fn pierce_trace_enabled() -> bool {
500    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
501    *ON.get_or_init(|| {
502        std::env::var("BREP_PIERCE_TRACE").is_ok_and(|value| !value.is_empty() && value != "0")
503    })
504}
505
506macro_rules! pierce_trace {
507    ($($arg:tt)*) => {
508        if pierce_trace_enabled() {
509            eprintln!($($arg)*);
510        }
511    };
512}
513
514/// `BREP_PROFILE` (the switch the boolean's `boolean.*_ms` and the mass
515/// integrator's `mass.profile` read) prints one `soundness.profile` line per
516/// scan with its stage split — tessellation, weld, BVH build, the pair sweep,
517/// the fold pass — and the counts behind them, so a scan's cost can be named
518/// per stage rather than as one `timing.soundness_ms` ("Rules every slice
519/// follows", measure before changing).
520fn scan_profile_enabled() -> bool {
521    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
522    *ON.get_or_init(|| std::env::var("BREP_PROFILE").is_ok())
523}
524
525/// Knobs for [`solid_self_intersections`]. [`SelfIntersectionOptions::for_solid`]
526/// derives every one from the solid's own size.
527#[derive(Clone, Copy, Debug, Serialize)]
528pub struct SelfIntersectionOptions {
529    /// Tessellation density. The detector resolves nothing finer than this.
530    pub chord_tolerance: f64,
531    /// Signed-distance margin a confirmed crossing must straddle the other
532    /// carrier surface by, on BOTH sides.
533    pub straddle_band: f64,
534    /// Shortest mesh crossing segment that counts as a candidate.
535    pub min_crossing_length: f64,
536    /// Coincidence radius for welding tessellation vertices.
537    pub weld: f64,
538    /// Refuse to scan a mesh larger than this (the report comes back
539    /// `truncated`), so a pathological solid cannot hang a gate.
540    pub max_triangles: usize,
541}
542
543impl SelfIntersectionOptions {
544    /// Every band derived from the solid's own extent: the display chord
545    /// tolerance the app meshes at, and diagonal-relative bands.
546    pub fn for_solid(solid: &BrepSolid) -> Self {
547        let diagonal = solid_diagonal(solid).max(1e-9);
548        Self {
549            chord_tolerance: crate::display_chord_tolerance(solid, 1.0),
550            straddle_band: diagonal * STRADDLE_FRACTION,
551            min_crossing_length: diagonal * CROSSING_FRACTION,
552            weld: diagonal * WELD_FRACTION,
553            max_triangles: 400_000,
554        }
555    }
556}
557
558/// One confirmed face-versus-face crossing.
559#[derive(Clone, Debug, Serialize)]
560pub struct FaceCrossing {
561    pub face_a: u64,
562    pub face_b: u64,
563    pub name_a: Option<String>,
564    pub name_b: Option<String>,
565    /// Midpoint of the mesh crossing segment.
566    pub point: Vec3,
567    /// Length of the mesh crossing segment.
568    pub crossing_length: f64,
569    /// How far the confirmation straddled the other carrier: the smaller of
570    /// the two one-sided margins, over both directions of the pair.
571    pub straddle: f64,
572    /// WHICH confirmation convicted this pair. Recorded so a sweep can
573    /// attribute every hit to the question that caught it — the two ask the
574    /// same thing of different sample points, and a hit only the pierce lane
575    /// sees is a hit the corner lane structurally could not.
576    pub confirmation: CrossingConfirmation,
577    /// How deep one face's own SURFACE reaches past the other's carrier, at
578    /// the crossing — the pierce lane's second margin, and the one that says
579    /// whether the crossing is the geometry's or the mesh's.
580    ///
581    /// The lane's first margin brackets the pierce along a mesh EDGE, so on a
582    /// regularly tessellated carrier it reads the same number for every pair
583    /// in a ring and says more about the tessellation's stride than about the
584    /// shapes. This one is measured between exact surface points and is the
585    /// discriminator. `straddle` is the smaller of the two; for a `Corners`
586    /// confirmation this is 0.0 and means nothing.
587    pub penetration: f64,
588}
589
590/// One confirmed place a face FOLDS THROUGH ITSELF in 3D: its own surface
591/// carries two DISTINCT trimmed parameter feet to ONE point in space.
592///
593/// This is the defect a [`FaceCrossing`] structurally cannot hold. A crossing
594/// is between two faces and is confirmed by a signed straddle of the OTHER
595/// carrier; with both triangles on ONE carrier every corner sits at distance
596/// zero from it, so that test is degenerate by construction and the pair was
597/// only ever counted. A fold is confirmed by a different question, asked in
598/// parameter space where it is exact: are the two feet DISTINCT, are both
599/// inside the face's own trim, and do they land on the same 3D point?
600#[derive(Clone, Debug, Serialize)]
601pub struct FaceFold {
602    pub face: u64,
603    pub face_name: Option<String>,
604    /// The two parameter feet, each inside the face's own trim.
605    pub uv_a: [f64; 2],
606    pub uv_b: [f64; 2],
607    /// Where they meet — the midpoint of the two surface images.
608    pub point: Vec3,
609    /// Parameter distance between the feet, measured the SHORT way round every
610    /// closed direction, so a seam is never mistaken for a separation.
611    pub uv_separation: f64,
612    /// The parameter distance at which two feet count as ONE: half a cell of
613    /// the face's own sample grid, which is built at the tessellation's own
614    /// resolution. Two seeds closer than this refine to the same foot — which
615    /// is what a chord-error crossing on a sharply curved face produces — and
616    /// a face that ends with one foot is rejected.
617    pub uv_band: f64,
618    /// `|S(uv_a) - S(uv_b)|`: how coincident in 3D the two feet actually are.
619    /// Bounded by the mesh's own chord tolerance, and recorded so a reader can
620    /// see the margin rather than trust the verdict.
621    pub residual: f64,
622    /// Angle between the two surface normals, in radians. A genuine fold
623    /// crosses TRANSVERSALLY. A degenerate parameterization — a pole, a seam
624    /// the period reduction did not fold away — also carries distinct uv to
625    /// one point, but with ONE tangent plane; that is rejected here rather
626    /// than reported as a fold.
627    pub normal_angle: f64,
628}
629
630/// Separating support of one omitted distinct-face pair. Bounds are signed
631/// coordinates on the named world axis; no adjacency inference is involved.
632#[derive(Clone, Debug, Serialize)]
633pub struct CertifiedFacePair {
634    pub faces: [u64; 2],
635    pub rule: &'static str,
636    pub axis: usize,
637    pub bounds: [[f64; 2]; 2],
638}
639
640/// Explicit scope of an opt-in analytic pair scan. Original counters describe
641/// only examined pairs. Same-face fold work is retained in this first row.
642#[derive(Clone, Debug, Serialize)]
643pub struct CertificateScope {
644    pub examined_face_pairs: usize,
645    pub certified_face_pairs: usize,
646    pub omitted_faces: usize,
647    pub skipped_candidate_pairs: usize,
648    pub retained_fold_faces: usize,
649    pub faces_in_uncertified_pairs: usize,
650    pub triangles_in_uncertified_pairs: usize,
651    pub retained_triangles: usize,
652}
653
654/// Outcome of [`solid_self_intersections`].
655#[derive(Clone, Debug, Serialize)]
656pub struct SelfIntersectionReport {
657    #[serde(skip_serializing_if = "Vec::is_empty")]
658    pub certified_pairs: Vec<CertifiedFacePair>,
659    #[serde(skip_serializing_if = "Option::is_none")]
660    pub skipped_by_certificate: Option<CertificateScope>,
661    /// The whole-body proof that stood in for the scan, when one did: the
662    /// counters below then read zero because no mesh was built, and this names
663    /// why, so a caller can still tell a proved-clean body from a scan that
664    /// never ran.
665    #[serde(skip_serializing_if = "Option::is_none")]
666    pub certificate: Option<&'static str>,
667    pub triangles: usize,
668    /// Triangle pairs whose boxes overlapped and that survived the welded-
669    /// vertex skip — the work the exact confirmation was offered.
670    pub candidate_pairs: usize,
671    /// Mesh crossings between DISTINCT faces that the exact-surface straddle
672    /// test confirmed, one entry per face pair (deepest kept).
673    pub confirmed: Vec<FaceCrossing>,
674    /// Mesh crossings between distinct faces the straddle test REJECTED —
675    /// tangent neighbours and chord error. A large number here beside zero
676    /// confirmations is the detector working, not failing.
677    pub rejected: usize,
678    /// Mesh crossings whose confirmation could not be evaluated (the witness
679    /// is not a point both faces own, or a projection or a normal failed) and
680    /// that the carriers did not settle either when asked again on their exact
681    /// geometry (`resolve.rs`). Never counted as either verdict; a caller that
682    /// accepts a body must not read zero `confirmed` beside a nonzero count
683    /// here as clean.
684    pub undecided: usize,
685    /// The first [`MAX_LISTED_UNDECIDED`] of those, with their faces and cause.
686    #[serde(skip_serializing_if = "Vec::is_empty")]
687    pub undecided_pairs: Vec<UndecidedCrossing>,
688    /// Candidates the mesh confirmation left undecided that the carriers then
689    /// CLEARED: every point of the carriers' common curve near the crossing
690    /// lies on an edge both faces share, or outside a trim.
691    #[serde(skip_serializing_if = "is_zero")]
692    pub resolved_clear: usize,
693    /// Mesh crossings between two triangles of ONE face: the candidates the
694    /// fold confirmation was offered. A candidate is not a defect — a sharply
695    /// curved face's chords cross each other without the face folding.
696    pub same_face_candidates: usize,
697    /// Faces confirmed to FOLD THROUGH THEMSELVES in 3D, one entry per FACE
698    /// (the widest-separated hit kept). The face is the stable unit: a fold
699    /// region produces as many triangle pairs as the mesh happens to have
700    /// there, and a denser mesh would rewrite that number without the face
701    /// being any more or less folded.
702    pub folds: Vec<FaceFold>,
703    /// Same-face candidates the fold confirmation REJECTED — the two feet came
704    /// back within one tessellation cell of each other (chord error on a tight
705    /// curve), or their normals were parallel (a pole). A large number here
706    /// beside zero folds is the confirmation working, not failing.
707    pub fold_rejected: usize,
708    /// Same-face candidates whose confirmation could not be evaluated: a
709    /// projection, a normal or a containment query failed, or a corner did not
710    /// come back onto its own carrier. Never counted as either verdict, and a
711    /// caller must not read an empty `folds` beside a large one of these as
712    /// "no folds".
713    pub fold_undecided: usize,
714    /// The mesh exceeded `max_triangles` and was not scanned.
715    pub truncated: bool,
716    pub options: SelfIntersectionOptions,
717}
718
719impl SelfIntersectionReport {
720    /// The predicate for a caller that has opted into this check: the solid
721    /// passes through itself — two faces crossing, or ONE face folded through
722    /// itself. Both are the same defect to a caller deciding whether to accept
723    /// a result, so both are in the predicate.
724    pub fn is_flagged(&self) -> bool {
725        !self.confirmed.is_empty() || !self.folds.is_empty()
726    }
727
728    /// One line naming the worst crossing or fold, or `None` when nothing was
729    /// confirmed.
730    pub fn summary(&self) -> Option<String> {
731        if self.confirmed.is_empty() {
732            return self.fold_summary();
733        }
734        let worst = self
735            .confirmed
736            .iter()
737            .max_by(|a, b| a.straddle.total_cmp(&b.straddle))?;
738        Some(format!(
739            "{} face pair(s) self-intersect; worst faces {} and {} near ({:.4}, {:.4}, {:.4}) (crossing {:.3e}, straddle {:.3e})",
740            self.confirmed.len(),
741            worst.face_a,
742            worst.face_b,
743            worst.point.x,
744            worst.point.y,
745            worst.point.z,
746            worst.crossing_length,
747            worst.straddle
748        ))
749    }
750
751    /// One line naming the worst FOLD, or `None` when no face folded.
752    pub fn fold_summary(&self) -> Option<String> {
753        let worst = self
754            .folds
755            .iter()
756            .max_by(|a, b| a.uv_separation.total_cmp(&b.uv_separation))?;
757        Some(format!(
758            "{} face(s) fold through themselves; worst face {}{} carries uv ({:.6}, {:.6}) and \
759             ({:.6}, {:.6}) to ({:.4}, {:.4}, {:.4}) (separation {:.3e} vs band {:.3e}, \
760             residual {:.3e}, normals {:.1}°)",
761            self.folds.len(),
762            worst.face,
763            worst
764                .face_name
765                .as_ref()
766                .map(|name| format!(" '{name}'"))
767                .unwrap_or_default(),
768            worst.uv_a[0],
769            worst.uv_a[1],
770            worst.uv_b[0],
771            worst.uv_b[1],
772            worst.point.x,
773            worst.point.y,
774            worst.point.z,
775            worst.uv_separation,
776            worst.uv_band,
777            worst.residual,
778            worst.normal_angle.to_degrees()
779        ))
780    }
781}
782
783fn solid_diagonal(solid: &BrepSolid) -> f64 {
784    let mut bounds = Aabb::empty();
785    for vertex in &solid.vertices {
786        bounds.include_point(vertex.point);
787    }
788    let diagonal = bounds.diagonal();
789    if diagonal.is_finite() && diagonal > 0.0 {
790        return diagonal;
791    }
792    // Vertex-free solids (a full sphere, a full torus) fall back to the
793    // control hull, exactly as the display tolerance does.
794    let mut hull = Aabb::empty();
795    for shell in &solid.shells {
796        for face in &shell.faces {
797            if let Ok(box_of) = Aabb::from_surface_controls(&face.surface) {
798                hull.include(box_of);
799            }
800        }
801    }
802    let diagonal = hull.diagonal();
803    if diagonal.is_finite() {
804        diagonal
805    } else {
806        0.0
807    }
808}
809
810/// Does this solid pass through itself? Tessellates every face once, walks a
811/// triangle BVH for mesh crossings, and confirms each candidate against the
812/// two carrier SURFACES so tangent neighbours and chord error cannot
813/// manufacture a finding. See the module docs for the design and its one
814/// documented blind spot.
815///
816/// Nothing in the kernel's build path calls this — a caller asks for it and
817/// reads [`SelfIntersectionReport::is_flagged`]. The case gate is that caller
818/// and treats a flag as a failure unless its baseline already holds the same
819/// face pair; see the module table.
820pub fn solid_self_intersections(
821    solid: &BrepSolid,
822    options: SelfIntersectionOptions,
823) -> Result<SelfIntersectionReport, String> {
824    let started = scan_profile_enabled().then(Instant::now);
825    // First, because it declines in microseconds where the NURBS certificate
826    // validates the whole body. Every certificate returns this same empty
827    // report, so the order changes only what a scan costs.
828    if analytic_body::enabled() && analytic_body::certifies(solid, options) {
829        if let Some(started) = started {
830            eprintln!("soundness.profile ms={:.2} faces={} certificate=analytic_plane_cylinder triangles=0 confirmed=0 folds=0", started.elapsed().as_secs_f64()*1000.0, solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>());
831        }
832        return Ok(SelfIntersectionReport {
833            certificate: Some("analytic_plane_cylinder"),
834            ..empty_intersection_report(options)
835        });
836    }
837    if tube_grid::enabled() && tube_grid::certifies(solid, options) {
838        if let Some(started) = started {
839            eprintln!("soundness.profile ms={:.2} faces={} certificate=tube_grid triangles=0 confirmed=0 folds=0", started.elapsed().as_secs_f64()*1000.0, solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>());
840        }
841        return Ok(SelfIntersectionReport {
842            certificate: Some("tube_grid"),
843            ..empty_intersection_report(options)
844        });
845    }
846    if blend_patch::enabled() && blend_patch::certifies(solid, options) {
847        if let Some(started) = started {
848            eprintln!("soundness.profile ms={:.2} faces={} certificate=analytic_blend triangles=0 confirmed=0 folds=0", started.elapsed().as_secs_f64()*1000.0, solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>());
849        }
850        return Ok(SelfIntersectionReport {
851            certificate: Some("analytic_blend"),
852            ..empty_intersection_report(options)
853        });
854    }
855    if sphere_certificate::enabled() && sphere_certificate::certifies(solid, options) {
856        if let Some(started) = started {
857            eprintln!("soundness.profile ms={:.2} faces={} certificate=native_trimmed_spheres triangles=0 confirmed=0 folds=0", started.elapsed().as_secs_f64()*1000.0, solid.shells[0].faces.len());
858        }
859        return Ok(empty_intersection_report(options));
860    }
861    if nurbs_certificate::enabled() && nurbs_certificate::certifies(solid, options) {
862        if let Some(started) = started {
863            eprintln!("soundness.profile ms={:.2} faces={} certificate=native_nurbs_horizontal_sections triangles=0 confirmed=0 folds=0", started.elapsed().as_secs_f64()*1000.0, solid.shells[0].faces.len());
864        }
865        return Ok(empty_intersection_report(options));
866    }
867    solid_self_intersections_full(solid, options)
868}
869
870fn is_zero(value: &usize) -> bool {
871    *value == 0
872}
873
874/// How many undecided candidates a report lists by name.
875pub const MAX_LISTED_UNDECIDED: usize = 16;
876
877fn empty_intersection_report(options: SelfIntersectionOptions) -> SelfIntersectionReport {
878    SelfIntersectionReport {
879        certified_pairs: Vec::new(),
880        skipped_by_certificate: None,
881        certificate: None,
882        triangles: 0,
883        candidate_pairs: 0,
884        confirmed: Vec::new(),
885        rejected: 0,
886        undecided: 0,
887        undecided_pairs: Vec::new(),
888        resolved_clear: 0,
889        same_face_candidates: 0,
890        folds: Vec::new(),
891        fold_rejected: 0,
892        fold_undecided: 0,
893        truncated: false,
894        options,
895    }
896}
897
898/// Ask the carriers about every candidate the mesh confirmation left
899/// undecided (`resolve.rs`), within its budget. Skipped when a crossing is
900/// already confirmed (the body is flagged either way) or the trim switches are
901/// off; what stays undecided is counted and the first few are named.
902fn resolve_pending(
903    solid: &BrepSolid,
904    faces: &[&crate::topology::FaceRecord],
905    pending: Vec<(usize, usize, CrossingUndecidedCause, Vec3, Vec3)>,
906    options: SelfIntersectionOptions,
907    best: &mut HashMap<(u64, u64), FaceCrossing>,
908    report: &mut SelfIntersectionReport,
909) {
910    let mut attempt = resolve::enabled() && best.is_empty() && trim_samples_enabled() && node_membership_enabled();
911    let curves: HashMap<u64, &NurbsCurve> = solid.edges.iter().map(|edge| (edge.id, &edge.curve)).collect();
912    let ends: HashMap<u64, [u64; 2]> = solid.edges.iter().map(|edge| (edge.id, [edge.start_vertex_id, edge.end_vertex_id])).collect();
913    let points: HashMap<u64, Vec3> = solid.vertices.iter().map(|vertex| (vertex.id, vertex.point)).collect();
914    let edges_of = |face: &crate::topology::FaceRecord| -> HashSet<u64> {
915        face.loops.iter().flat_map(|l| &l.coedges).map(|coedge| coedge.edge_id).collect()
916    };
917    for (index, (first, second, cause, start, end)) in pending.into_iter().enumerate() {
918        let (face_a, face_b) = (faces.get(first), faces.get(second));
919        let id = |face: Option<&&crate::topology::FaceRecord>| face.map_or(u64::MAX, |f| f.id);
920        let resolution = match (face_a, face_b) {
921            (Some(a), Some(b)) if attempt && index < resolve::MAX_RESOLVED => {
922                let shared_ids: Vec<u64> = edges_of(a).intersection(&edges_of(b)).copied().collect();
923                let shared: Vec<&NurbsCurve> = shared_ids.iter().filter_map(|edge| curves.get(edge).copied()).collect();
924                let vertices_of = |edges: &HashSet<u64>| -> HashSet<u64> { edges.iter().filter_map(|e| ends.get(e)).flatten().copied().collect() };
925                let corners: Vec<Vec3> = vertices_of(&edges_of(a)).intersection(&vertices_of(&edges_of(b))).filter_map(|v| points.get(v).copied()).collect();
926                resolve::resolve(a, b, &shared, &corners, (start, end), options)
927            }
928            _ => resolve::Resolution::Undecided,
929        };
930        match resolution {
931            resolve::Resolution::Crossing { point, straddle } => {
932                let (a, b) = (face_a.expect("resolved faces"), face_b.expect("resolved faces"));
933                let (lo, hi) = if a.id <= b.id { (a, b) } else { (b, a) };
934                let crossing = FaceCrossing {
935                    face_a: lo.id,
936                    face_b: hi.id,
937                    name_a: lo.name.clone(),
938                    name_b: hi.name.clone(),
939                    point,
940                    crossing_length: end.sub(start).length(),
941                    straddle,
942                    confirmation: CrossingConfirmation::Resolved,
943                    penetration: 0.0,
944                };
945                let slot = best.entry((lo.id, hi.id)).or_insert_with(|| crossing.clone());
946                if crossing.straddle > slot.straddle {
947                    *slot = crossing;
948                }
949                // Flagged now: the rest need not be asked.
950                attempt = false;
951            }
952            resolve::Resolution::Clear => report.resolved_clear += 1,
953            resolve::Resolution::Undecided => {
954                report.undecided += 1;
955                if report.undecided_pairs.len() < MAX_LISTED_UNDECIDED {
956                    report.undecided_pairs.push(UndecidedCrossing { face_a: id(face_a), face_b: id(face_b), cause, point: start.add(end).scale(0.5) });
957                }
958            }
959        }
960    }
961}
962
963fn solid_self_intersections_full(
964    solid: &BrepSolid,
965    options: SelfIntersectionOptions,
966) -> Result<SelfIntersectionReport, String> {
967    solid_self_intersections_with_census(solid, options, census::enabled())
968}
969
970fn solid_self_intersections_with_census(
971    solid: &BrepSolid,
972    options: SelfIntersectionOptions,
973    collect_census: bool,
974) -> Result<SelfIntersectionReport, String> {
975    solid_self_intersections_with_pair_certificates(solid, options, collect_census, pair_certificate::enabled())
976}
977
978fn solid_self_intersections_with_pair_certificates(
979    solid: &BrepSolid,
980    options: SelfIntersectionOptions,
981    collect_census: bool,
982    certify_pairs: bool,
983) -> Result<SelfIntersectionReport, String> {
984    let mut report = empty_intersection_report(options);
985    if !(options.chord_tolerance > 0.0) || !options.chord_tolerance.is_finite() {
986        return Err("solid_self_intersections: chord tolerance must be positive".into());
987    }
988
989    let profile = scan_profile_enabled().then(Instant::now);
990    let stage_ms = |started: Option<Instant>| -> f64 {
991        started.map(|at| at.elapsed().as_secs_f64() * 1_000.0).unwrap_or(0.0)
992    };
993    // The face-stride entry point rather than `tessellate_brep_watertight`:
994    // the latter runs a coherent-orientation pass and a mesh validation that
995    // can fail on exactly the broken solids this detector exists to inspect,
996    // and winding is irrelevant to a crossing test.
997    let (mesh, face_keys) = crate::watertight_tessellation::tessellate_brep_watertight_face_stride_keyed(
998        solid, options.chord_tolerance, 1, 0,
999    )?;
1000    let tessellate_ms = stage_ms(profile);
1001    let triangle_count = mesh.indices.len() / 3;
1002    report.triangles = triangle_count;
1003    if triangle_count == 0 {
1004        return Ok(report);
1005    }
1006    if triangle_count > options.max_triangles {
1007        report.truncated = true;
1008        return Ok(report);
1009    }
1010
1011    // Faces in the tessellator's sequential order, so a triangle's `face_ids`
1012    // entry indexes straight into this.
1013    let faces: Vec<&crate::topology::FaceRecord> = solid
1014        .shells
1015        .iter()
1016        .flat_map(|shell| shell.faces.iter())
1017        .collect();
1018
1019    let mut census = collect_census.then(|| {
1020        let ids: Vec<_> = mesh.face_ids.iter().map(|&index| faces[index as usize].id).collect();
1021        census::ScanCensus::new(&ids)
1022    });
1023    let point_of = |index: u32| -> Vec3 {
1024        let base = index as usize * 3;
1025        Vec3::new(
1026            mesh.positions[base],
1027            mesh.positions[base + 1],
1028            mesh.positions[base + 2],
1029        )
1030    };
1031
1032    let weld_started = scan_profile_enabled().then(Instant::now);
1033    let welded = weld_positions(&mesh.positions, options.weld);
1034    let weld_ms = stage_ms(weld_started);
1035
1036    let bvh_started = scan_profile_enabled().then(Instant::now);
1037    let mut boxes = Vec::with_capacity(triangle_count);
1038    let mut corners: Vec<[Vec3; 3]> = Vec::with_capacity(triangle_count);
1039    for triangle in 0..triangle_count {
1040        let indices = [
1041            mesh.indices[triangle * 3],
1042            mesh.indices[triangle * 3 + 1],
1043            mesh.indices[triangle * 3 + 2],
1044        ];
1045        let points = [
1046            point_of(indices[0]),
1047            point_of(indices[1]),
1048            point_of(indices[2]),
1049        ];
1050        boxes.push(Aabb::from_points(points));
1051        corners.push(points);
1052    }
1053    let mut certificates = (certify_pairs && trim_samples_enabled() && node_membership_enabled()
1054        && faces.len() <= 256
1055        && options.straddle_band.is_finite() && options.straddle_band > 0.
1056        && options.min_crossing_length.is_finite() && options.min_crossing_length > 0.
1057    ).then(|| pair_certificate::Plan::new(solid, &faces, &corners, &mesh.face_ids));
1058    let bvh = Bvh::build(&boxes);
1059    let bvh_ms = stage_ms(bvh_started);
1060    // The face-pair memo (`pair_memo.rs`): a pair of fragments the sweep
1061    // read clean under these bands skips its triangle crossing tests; every
1062    // count the report carries is kept. Off while the census collects (its
1063    // tallies read every candidate) and when the mesh's face keys are not
1064    // the faces' (a stride other than 1 never reaches here).
1065    let pair_memo = pair_memo::enabled() && !collect_census && face_keys.len() == faces.len();
1066    let pair_options = pair_memo::options_key(options.chord_tolerance, options.weld, options.min_crossing_length);
1067    // Per unordered face pair: 0 untouched, 1 swept clean so far, 2 served
1068    // from the memo, 3 crossed in the mesh.
1069    let face_count = faces.len();
1070    let mut pair_state: Vec<u8> = if pair_memo { vec![0u8; face_count * face_count] } else { Vec::new() };
1071    let sweep_started = scan_profile_enabled().then(Instant::now);
1072    let mut overlaps: u64 = 0;
1073    let mut crossings: u64 = 0;
1074    let mut confirm_ms = 0.0f64;
1075
1076    // Deepest confirmed crossing per unordered face pair, and the
1077    // widest-separated confirmed fold per face.
1078    let mut best: HashMap<(u64, u64), FaceCrossing> = HashMap::default();
1079    let mut folded: HashMap<u64, FaceFold> = HashMap::default();
1080    // Same-face mesh crossings, per face index, as `(crossing length, point)`.
1081    let mut same_face: HashMap<usize, Vec<(f64, Vec3)>> = HashMap::default();
1082    // Candidates the mesh confirmation could not decide, asked of the carriers
1083    // after the sweep: `(face index, face index, cause, crossing segment)`.
1084    let mut pending: Vec<(usize, usize, CrossingUndecidedCause, Vec3, Vec3)> = Vec::new();
1085    let mut hits = Vec::new();
1086    for first in 0..triangle_count {
1087        hits.clear();
1088        bvh.overlapping(boxes[first], 0.0, &mut hits);
1089        for second in hits.iter().copied() {
1090            if second <= first {
1091                continue;
1092            }
1093            overlaps += 1;
1094            if let Some(census) = &mut census { census.pair(first, second, false); }
1095            let shares_vertex = (0..3).any(|a| {
1096                let wa = welded[mesh.indices[first * 3 + a] as usize];
1097                (0..3).any(|b| wa == welded[mesh.indices[second * 3 + b] as usize])
1098            });
1099            if shares_vertex {
1100                continue;
1101            }
1102            if let Some(plan) = &mut certificates {
1103                if plan.skips(mesh.face_ids[first], mesh.face_ids[second]) {
1104                    plan.scope.skipped_candidate_pairs += 1;
1105                    continue;
1106                }
1107            }
1108            report.candidate_pairs += 1;
1109            if let Some(census) = &mut census { census.pair(first, second, true); }
1110            if pair_memo {
1111                let (fa, fb) = (mesh.face_ids[first] as usize, mesh.face_ids[second] as usize);
1112                let slot = fa.min(fb) * face_count + fa.max(fb);
1113                match pair_state[slot] {
1114                    2 => continue,
1115                    0 => {
1116                        let served = pair_memo::contains(pair_memo::pair_key(pair_options, face_keys[fa], face_keys[fb]));
1117                        pair_state[slot] = if served { 2 } else { 1 };
1118                        if served {
1119                            continue;
1120                        }
1121                    }
1122                    _ => {}
1123                }
1124            }
1125            let segment = triangle_crossing(&corners[first], &corners[second]);
1126            let Some((start, end)) = segment else {
1127                continue;
1128            };
1129            let length = end.sub(start).length();
1130            if length <= options.min_crossing_length {
1131                continue;
1132            }
1133            crossings += 1;
1134            let confirm_started = scan_profile_enabled().then(Instant::now);
1135            let face_first = mesh.face_ids[first] as usize;
1136            let face_second = mesh.face_ids[second] as usize;
1137            if pair_memo {
1138                pair_state[face_first.min(face_second) * face_count + face_first.max(face_second)] = 3;
1139            }
1140            if face_first == face_second {
1141                report.same_face_candidates += 1;
1142                // Confirmed in a SECOND pass, per face: the fold test needs a
1143                // sample grid over the face's own domain, and building that
1144                // once per face is the difference between a cheap check and a
1145                // tessellation-sized one.
1146                same_face
1147                    .entry(face_first)
1148                    .or_insert_with(Vec::new)
1149                    .push((length, start.add(end).scale(0.5)));
1150                continue;
1151            }
1152            let (Some(face_a), Some(face_b)) = (faces.get(face_first), faces.get(face_second))
1153            else {
1154                pending.push((face_first, face_second, CrossingUndecidedCause::MissingFace, start, end));
1155                continue;
1156            };
1157            // The mesh crossing's midpoint must be a point BOTH faces own: a
1158            // crossing between two meshes whose witness lies outside either
1159            // trim is a crossing of the tessellation, not of the body
1160            // (2026-10-03, `anotherBooleanFail` ∩ sphere: four vendor
1161            // cone–cone junctions convicted at 1.395e-2 from interior mesh
1162            // nodes planted 0.26° past their own trims on a chart where the
1163            // junction pcurve bows 2.8e-3 in u). The witness is a chord point,
1164            // so only its carrier FOOT is asked of each trim (no reach test:
1165            // the oblique bore-mouth control's genuine crossing sits a chord
1166            // sagitta off the bore). A witness that cannot be classified
1167            // leaves the pair undecided, never clear.
1168            let witness = start.add(end).scale(0.5);
1169            match (footprint_material(face_a, witness), footprint_material(face_b, witness)) {
1170                (Some(true), Some(true)) => {}
1171                (in_a, in_b) => {
1172                    pierce_trace!(
1173                        "pair faces {}|{} triangles {first}|{second}: crossing witness ({:.6}, {:.6}, {:.6}) in face {}: {in_a:?}, in face {}: {in_b:?} — not a point both faces own; undecided",
1174                        face_a.id, face_b.id, witness.x, witness.y, witness.z, face_a.id, face_b.id
1175                    );
1176                    pending.push((face_first, face_second, CrossingUndecidedCause::Witness, start, end));
1177                    continue;
1178                }
1179            }
1180            // TWO confirmations, asked in order, and the first one is
1181            // unchanged in what it asks: a triangle's OWN CORNERS must straddle
1182            // the other carrier, in both directions — read over the corners
1183            // the face OWNS (a tessellation node outside its own trim is not
1184            // the face's material and its side counts for nothing).
1185            let forward = straddle_material(face_a, &face_b.surface, &corners[first], options.straddle_band);
1186            let backward = straddle_material(face_b, &face_a.surface, &corners[second], options.straddle_band);
1187            let corner_margin = match (forward, backward) {
1188                (Some(forward), Some(backward))
1189                    if forward > options.straddle_band && backward > options.straddle_band =>
1190                {
1191                    Some(forward.min(backward))
1192                }
1193                _ => None,
1194            };
1195            // The SECOND runs only when the first did not convict, and it is
1196            // the one a PLANAR face passing through a curved one needs: the
1197            // corner test reads a margin of zero there, because every corner
1198            // of the plane's few triangles is outside the cylinder its
1199            // INTERIOR passes through. See `pierce_straddle`.
1200            if corner_margin.is_none() {
1201                pierce_trace!(
1202                    "pair faces {}|{} triangles {first}|{second}: corners forward {forward:?} backward {backward:?}; crossing ({:.6}, {:.6}, {:.6})-({:.6}, {:.6}, {:.6}); triangle A {:?} triangle B {:?}",
1203                    face_a.id,
1204                    face_b.id,
1205                    start.x,
1206                    start.y,
1207                    start.z,
1208                    end.x,
1209                    end.y,
1210                    end.z,
1211                    corners[first].map(|corner| (corner.x, corner.y, corner.z)),
1212                    corners[second].map(|corner| (corner.x, corner.y, corner.z)),
1213                );
1214            }
1215            let (margin, confirmation, penetration) = match corner_margin {
1216                Some(margin) => (margin, CrossingConfirmation::Corners, 0.0),
1217                None => match pierce_straddle(
1218                    &corners[first],
1219                    &corners[second],
1220                    face_a,
1221                    face_b,
1222                    (start, end),
1223                    options.straddle_band,
1224                ) {
1225                    PierceVerdict::Confirmed(hit) => {
1226                        pierce_trace!(
1227                            " verdict: confirmed, bracket {:.6e} penetration {:.6e}",
1228                            hit.bracket,
1229                            hit.penetration
1230                        );
1231                        (hit.margin(), CrossingConfirmation::Pierce, hit.penetration)
1232                    }
1233                    verdict => {
1234                        // A projection or a normal that could not be evaluated
1235                        // is undecided in EITHER lane, never a rejection.
1236                        if forward.is_none()
1237                            || backward.is_none()
1238                            || matches!(verdict, PierceVerdict::Undecided)
1239                        {
1240                            pierce_trace!(" verdict: undecided");
1241                            pending.push((face_first, face_second, CrossingUndecidedCause::Pierce, start, end));
1242                        } else {
1243                            pierce_trace!(" verdict: rejected");
1244                            report.rejected += 1;
1245                        }
1246                        continue;
1247                    }
1248                },
1249            };
1250            let key = if face_a.id <= face_b.id {
1251                (face_a.id, face_b.id)
1252            } else {
1253                (face_b.id, face_a.id)
1254            };
1255            let crossing = FaceCrossing {
1256                face_a: key.0,
1257                face_b: key.1,
1258                name_a: if face_a.id <= face_b.id {
1259                    face_a.name.clone()
1260                } else {
1261                    face_b.name.clone()
1262                },
1263                name_b: if face_a.id <= face_b.id {
1264                    face_b.name.clone()
1265                } else {
1266                    face_a.name.clone()
1267                },
1268                point: start.add(end).scale(0.5),
1269                crossing_length: length,
1270                straddle: margin,
1271                confirmation,
1272                penetration,
1273            };
1274            let slot = best.entry(key).or_insert_with(|| crossing.clone());
1275            if crossing.straddle > slot.straddle {
1276                *slot = crossing;
1277            }
1278            confirm_ms += stage_ms(confirm_started);
1279        }
1280    }
1281    let sweep_ms = stage_ms(sweep_started);
1282    if pair_memo {
1283        // Every pair that was swept and never crossed is clean under these
1284        // inputs; pairs no triangle box ever brought together cost nothing to
1285        // sweep again and are left out.
1286        let clean = (0..face_count).flat_map(|a| (a..face_count).map(move |b| (a, b)))
1287            .filter(|&(a, b)| pair_state[a * face_count + b] == 1)
1288            .map(|(a, b)| pair_memo::pair_key(pair_options, face_keys[a], face_keys[b]));
1289        pair_memo::store(clean);
1290        if profile.is_some() {
1291            let (hits, stored, entries) = pair_memo::take_counts();
1292            eprintln!("soundness.pair_memo served={hits} stored={stored} entries={entries}");
1293        }
1294    }
1295    let resolve_started = scan_profile_enabled().then(Instant::now);
1296    resolve_pending(solid, &faces, pending, options, &mut best, &mut report);
1297    let resolve_ms = stage_ms(resolve_started);
1298    let fold_started = scan_profile_enabled().then(Instant::now);
1299    let fold_counts = fold_memo::counts();
1300
1301    // ---- the fold pass ---------------------------------------------------
1302    // One grid per face that produced a same-face crossing at all, and at most
1303    // MAX_FOLD_PROBES points per face — the longest crossing segments, which
1304    // are the clearest evidence. A fold region produces as many candidates as
1305    // the mesh happens to have there, and confirming every one of them buys
1306    // nothing beyond the first.
1307    let mut triangles_on_face: HashMap<usize, usize> = HashMap::default();
1308    for face in mesh.face_ids.iter() {
1309        *triangles_on_face.entry(*face as usize).or_insert(0) += 1;
1310    }
1311    let mut face_order: Vec<usize> = same_face.keys().copied().collect();
1312    face_order.sort_unstable();
1313    for face_index in face_order {
1314        let Some(face) = faces.get(face_index) else {
1315            report.fold_undecided += same_face[&face_index].len();
1316            continue;
1317        };
1318        let mut points = same_face.remove(&face_index).unwrap_or_default();
1319        points.sort_by(|a, b| b.0.total_cmp(&a.0));
1320        points.truncate(MAX_FOLD_PROBES);
1321        let triangles = triangles_on_face.get(&face_index).copied().unwrap_or(0);
1322        let verdicts = fold_memo::read(face, triangles, &points, options);
1323        for verdict in verdicts {
1324            match verdict {
1325                FoldVerdict::Confirmed(fold) => {
1326                    let slot = folded.entry(face.id).or_insert_with(|| fold.clone());
1327                    if fold.uv_separation > slot.uv_separation {
1328                        *slot = fold;
1329                    }
1330                }
1331                FoldVerdict::Rejected => report.fold_rejected += 1,
1332                FoldVerdict::Undecided => report.fold_undecided += 1,
1333            }
1334        }
1335    }
1336
1337    if profile.is_some() {
1338        let after = fold_memo::counts();
1339        eprintln!("soundness.fold_memo reused={} read={}", after.0 - fold_counts.0, after.1 - fold_counts.1);
1340    }
1341    report.confirmed = best.into_values().collect();
1342    report
1343        .confirmed
1344        .sort_by(|a, b| b.straddle.total_cmp(&a.straddle).then(a.face_a.cmp(&b.face_a)));
1345    report.folds = folded.into_values().collect();
1346    report
1347        .folds
1348        .sort_by(|a, b| b.uv_separation.total_cmp(&a.uv_separation).then(a.face.cmp(&b.face)));
1349    if profile.is_some() {
1350        eprintln!(
1351            "soundness.profile ms={:.2} faces={} tris={triangle_count} chord={:.3e} | \
1352             tessellate_ms={tessellate_ms:.2} weld_ms={weld_ms:.2} bvh_ms={bvh_ms:.2} \
1353             sweep_ms={sweep_ms:.2} confirm_ms={confirm_ms:.2} resolve_ms={resolve_ms:.2} fold_ms={:.2} | \
1354             overlaps={overlaps} candidate_pairs={} mesh_crossings={crossings} \
1355             confirmed={} rejected={} undecided={} same_face={} folds={}",
1356            stage_ms(profile),
1357            faces.len(),
1358            options.chord_tolerance,
1359            stage_ms(fold_started),
1360            report.candidate_pairs,
1361            report.confirmed.len(),
1362            report.rejected,
1363            report.undecided,
1364            report.same_face_candidates,
1365            report.folds.len(),
1366        );
1367    }
1368    if let Some(plan) = certificates {
1369        if !plan.proofs.is_empty() {
1370            if profile.is_some() {
1371                let mut rules = std::collections::BTreeMap::new();
1372                for proof in &plan.proofs { *rules.entry(proof.rule).or_insert(0usize) += 1; }
1373                eprintln!("soundness.certificates {}", serde_json::json!({"rules": rules, "skipped_by_certificate": plan.scope}));
1374            }
1375            report.certified_pairs = plan.proofs;
1376            report.skipped_by_certificate = Some(plan.scope);
1377        }
1378    }
1379    if let Some(census) = census { census.emit(&boxes); }
1380    Ok(report)
1381}
1382
1383/// Which of this solid's faces FOLD THROUGH THEMSELVES in 3D — a face whose
1384/// surface carries two distinct trimmed parameter feet to one point in space:
1385/// an offset taken past the source's curvature radius, a section swept through
1386/// a bend tighter than its own half-width, a carrier fitted through a
1387/// swallowtail.
1388///
1389/// The brief's named entry point, and one pass: it is
1390/// [`solid_self_intersections`] read for its fold half, because both verdicts
1391/// come off the SAME tessellation and paying for that twice would buy nothing.
1392/// A caller that wants both should call `solid_self_intersections` directly and
1393/// read `confirmed` and `folds` off the one report.
1394pub fn face_self_intersections(solid: &BrepSolid) -> Result<Vec<FaceFold>, String> {
1395    let options = SelfIntersectionOptions::for_solid(solid);
1396    Ok(solid_self_intersections(solid, options)?.folds)
1397}
1398
1399/// Verdict of the fold confirmation for ONE same-face triangle pair.
1400enum FoldVerdict {
1401    Confirmed(FaceFold),
1402    Rejected,
1403    Undecided,
1404}
1405
1406
1407/// A coarse `(u, v) -> 3D` sample of ONE face's surface over its own domain,
1408/// at the resolution the tessellation already resolved the face at.
1409///
1410/// This is the fold test's instrument, and it exists because
1411/// `project_point_to_surface` cannot be one. A global projection onto a
1412/// FOLDED general surface is exactly the case its Newton is weakest at:
1413/// measured on the ribbon fixture, a point lying EXACTLY on the surface came
1414/// back with a foot 2.766 away from itself. The grid replaces that search with
1415/// an enumeration — every branch that comes near the point has a node near it
1416/// — and `project_point_to_surface_seeded` then refines each one WITHOUT
1417/// re-seeding globally, which is the same instrument the pcurve tracer uses to
1418/// stay on one branch of a surface that folds back.
1419struct FoldGrid {
1420    samples: Vec<(f64, f64, Vec3)>,
1421    /// Parameter distance at which two feet are ONE foot: half a cell. Two
1422    /// seeds inside it refine to the same place, which is exactly what chord
1423    /// error on a tightly curved face produces.
1424    merge: f64,
1425    /// The largest 3D step between neighbouring nodes, measured rather than
1426    /// assumed. A branch's nearest node can sit this far from the point, so it
1427    /// is how wide the seed window has to be to miss nothing.
1428    reach: f64,
1429}
1430
1431/// At most this many crossing points are confirmed per face. A fold region
1432/// produces as many mesh candidates as the tessellation happens to have
1433/// there; the verdict is a property of the FACE, so the longest crossing
1434/// segments — the clearest evidence — are enough.
1435const MAX_FOLD_PROBES: usize = 64;
1436
1437/// At most this many seeds are refined for one crossing point. A fold needs
1438/// TWO distinct feet; more than a handful of branches through one point is a
1439/// different shape than this detector claims.
1440const MAX_FOLD_SEEDS: usize = 16;
1441
1442/// Build the sample grid for one face, at twice the tessellation's own linear
1443/// density (`triangles` is how many triangles the mesh gave this face), so the
1444/// grid resolves anything the mesh that produced the candidate could.
1445fn fold_grid(surface: &NurbsSurface, triangles: usize) -> Option<FoldGrid> {
1446    let [u0, u1] = surface.domain_u().ok()?;
1447    let [v0, v1] = surface.domain_v().ok()?;
1448    let steps = (((triangles.max(1) as f64).sqrt().ceil() as usize) * 2).clamp(24, 192);
1449    let node = |index: usize| -> (f64, f64) {
1450        let iu = index / (steps + 1);
1451        let iv = index % (steps + 1);
1452        (
1453            u0 + (u1 - u0) * iu as f64 / steps as f64,
1454            v0 + (v1 - v0) * iv as f64 / steps as f64,
1455        )
1456    };
1457    let mut samples = Vec::with_capacity((steps + 1) * (steps + 1));
1458    for index in 0..(steps + 1) * (steps + 1) {
1459        let (u, v) = node(index);
1460        samples.push((u, v, surface.evaluate(u, v).ok()?));
1461    }
1462    let mut reach = 0.0f64;
1463    for iu in 0..=steps {
1464        for iv in 0..=steps {
1465            let here = samples[iu * (steps + 1) + iv].2;
1466            if iu < steps {
1467                reach = reach.max(samples[(iu + 1) * (steps + 1) + iv].2.sub(here).length());
1468            }
1469            if iv < steps {
1470                reach = reach.max(samples[iu * (steps + 1) + iv + 1].2.sub(here).length());
1471            }
1472        }
1473    }
1474    let cell_u = (u1 - u0) / steps as f64;
1475    let cell_v = (v1 - v0) / steps as f64;
1476    Some(FoldGrid {
1477        samples,
1478        merge: 0.5 * (cell_u * cell_u + cell_v * cell_v).sqrt(),
1479        reach,
1480    })
1481}
1482
1483/// The parameter distance between two feet, measured the SHORT way round every
1484/// closed direction.
1485fn parameter_separation(a: Vec2, b: Vec2, period: [Option<f64>; 2]) -> f64 {
1486    b.add(period_shift(b, a, period)).sub(a).length()
1487}
1488
1489/// Are these two feet ONE locus reached from opposite ends of the domain — a
1490/// seam the surface carries but does not DECLARE?
1491///
1492/// `closed_directions()` is an exact test and an imported carrier can fail it
1493/// while still closing: measured on `abc_00000026.step`, a face whose two
1494/// u-boundary isocurves sit 2.5e-2 apart (on a model whose pcurves are known to
1495/// be ~3.5e-2 off their own edges) put one foot at u = 0 and the other at
1496/// u = 1, a full span apart, on what is one curve. The period reduction cannot
1497/// fold those together because the surface is not closed to the precision that
1498/// test demands.
1499///
1500/// So it is MEASURED: when the two feet sit within `window` of opposite ends of
1501/// a direction, the two boundary isocurves are sampled and compared, and they
1502/// are one locus when every sample agrees within `band`. `window` is the grid's
1503/// own resolution — a foot is "at the boundary" when the grid cannot place it
1504/// anywhere else — and `band` is the same coincidence band the feet themselves
1505/// had to clear.
1506fn boundary_identified(
1507    surface: &NurbsSurface,
1508    a: Vec2,
1509    b: Vec2,
1510    domain_u: [f64; 2],
1511    domain_v: [f64; 2],
1512    window: f64,
1513    band: f64,
1514) -> bool {
1515    const SAMPLES: usize = 8;
1516    let straddles = |first: f64, second: f64, low: f64, high: f64| {
1517        (first - low).abs() <= window && (second - high).abs() <= window
1518            || (first - high).abs() <= window && (second - low).abs() <= window
1519    };
1520    let isocurves_agree = |along_u: bool| {
1521        for index in 0..=SAMPLES {
1522            let fraction = index as f64 / SAMPLES as f64;
1523            let (first, second) = if along_u {
1524                let v = domain_v[0] + (domain_v[1] - domain_v[0]) * fraction;
1525                (
1526                    surface.evaluate(domain_u[0], v),
1527                    surface.evaluate(domain_u[1], v),
1528                )
1529            } else {
1530                let u = domain_u[0] + (domain_u[1] - domain_u[0]) * fraction;
1531                (
1532                    surface.evaluate(u, domain_v[0]),
1533                    surface.evaluate(u, domain_v[1]),
1534                )
1535            };
1536            let (Ok(first), Ok(second)) = (first, second) else {
1537                return false;
1538            };
1539            if first.sub(second).length() > band {
1540                return false;
1541            }
1542        }
1543        true
1544    };
1545    if straddles(a.x, b.x, domain_u[0], domain_u[1]) && isocurves_agree(true) {
1546        return true;
1547    }
1548    straddles(a.y, b.y, domain_v[0], domain_v[1]) && isocurves_agree(false)
1549}
1550
1551/// Is this same-face mesh crossing a genuine 3D FOLD?
1552///
1553/// The face-versus-face straddle test cannot be asked here: both triangles lie
1554/// on one carrier, so every corner of both is at distance zero from the
1555/// surface it would be measured against. The question asked instead is the
1556/// parameter one, and it is the definition of a fold — the surface carries two
1557/// DISTINCT trimmed parameters to ONE point in space:
1558///
1559/// 1. **Enumerate the branches.** Every grid node within one cell's reach of
1560///    the crossing point is a seed ([`FoldGrid`] says why this is an
1561///    enumeration and not a search).
1562/// 2. **Refine each**, through the Newton that does NOT re-seed globally, and
1563///    keep the feet that actually reach the point within the mesh's own chord
1564///    band. Two seeds on ONE branch refine to one foot and are merged, which
1565///    is what chord error on a tightly curved face produces — so a face that
1566///    does not fold ends this step with a single foot and is rejected.
1567/// 3. **Both feet inside the trim.** An untrimmed carrier may fold wherever it
1568///    likes; only a fold the FACE carries is its defect.
1569/// 4. **The feet are TRANSVERSAL.** Distinct parameters carried to one point
1570///    with ONE tangent plane is a degenerate parameterization — a sphere's
1571///    pole, a seam — and not a fold.
1572///
1573/// The widest-separated qualifying pair is reported, with every margin it
1574/// cleared.
1575fn confirm_fold(
1576    face: &crate::topology::FaceRecord,
1577    grid: &FoldGrid,
1578    point: Vec3,
1579    options: SelfIntersectionOptions,
1580) -> FoldVerdict {
1581    let surface = &face.surface;
1582    let (Ok((closed_u, closed_v)), Ok(domain_u), Ok(domain_v)) = (
1583        surface.closed_directions(),
1584        surface.domain_u(),
1585        surface.domain_v(),
1586    ) else {
1587        fold_trace!("fold face {}: domain/closedness unreadable", face.id);
1588        return FoldVerdict::Undecided;
1589    };
1590    let span_u = domain_u[1] - domain_u[0];
1591    let span_v = domain_v[1] - domain_v[0];
1592    let period = [closed_u.then_some(span_u), closed_v.then_some(span_v)];
1593    // A tessellation vertex lies exactly on its surface and the crossing point
1594    // is within the chord tolerance of one, so this is the band every 3D
1595    // residual here is held to: the mesh's own accuracy.
1596    let coincidence = 2.0 * options.chord_tolerance;
1597
1598    let window = grid.reach + coincidence;
1599    let mut seeds: Vec<(f64, usize)> = grid
1600        .samples
1601        .iter()
1602        .enumerate()
1603        .filter_map(|(index, (_, _, sample))| {
1604            let distance = sample.sub(point).length();
1605            (distance <= window).then_some((distance, index))
1606        })
1607        .collect();
1608    if seeds.is_empty() {
1609        fold_trace!(
1610            "fold face {}: no grid node within {window:.3e} of the crossing point",
1611            face.id
1612        );
1613        return FoldVerdict::Undecided;
1614    }
1615    seeds.sort_by(|a, b| a.0.total_cmp(&b.0));
1616
1617    let mut feet: Vec<crate::projection::SurfaceProjection> = Vec::new();
1618    let mut used: Vec<Vec2> = Vec::new();
1619    for (_, index) in seeds {
1620        if feet.len() >= MAX_FOLD_SEEDS {
1621            break;
1622        }
1623        let (u, v, _) = grid.samples[index];
1624        let seed = Vec2 { x: u, y: v };
1625        if used
1626            .iter()
1627            .any(|taken| parameter_separation(*taken, seed, period) <= grid.merge)
1628        {
1629            continue;
1630        }
1631        used.push(seed);
1632        let Ok(foot) = project_point_to_surface_seeded(surface, point, u, v) else {
1633            continue;
1634        };
1635        if !(foot.distance <= coincidence) {
1636            continue;
1637        }
1638        let landed = Vec2 {
1639            x: foot.u,
1640            y: foot.v,
1641        };
1642        if feet.iter().any(|other| {
1643            parameter_separation(Vec2 { x: other.u, y: other.v }, landed, period) <= grid.merge
1644        }) {
1645            continue;
1646        }
1647        feet.push(foot);
1648    }
1649    if feet.len() < 2 {
1650        fold_trace!(
1651            "fold face {}: {} distinct foot/feet at the crossing point (merge {:.3e}) — one foot is not a fold",
1652            face.id,
1653            feet.len(),
1654            grid.merge
1655        );
1656        return FoldVerdict::Rejected;
1657    }
1658
1659    // The uv coincidence band the trim is asked with, keyed to the face's own
1660    // parameter extent exactly as `loop_self_crossings` keys its weld: uv is
1661    // not a length, and a domain is as likely to be [0, 1] as [0, 2*PI]. A foot
1662    // ON the trim boundary counts as inside — a fold that reaches the boundary
1663    // is still a fold.
1664    let trim_band = UV_WELD_FRACTION * (span_u.abs() + span_v.abs()).max(1.0);
1665    // Each kept foot with whether it sits ON the trim boundary. Two feet
1666    // that both sit on the boundary are the TRIM's: a seam the face declares
1667    // by its loops, or the two arms of a slit a carved wall leaves along its
1668    // crease (the closed-edge lane's carve cuts the fold's lens out of the
1669    // trim and keeps one crease edge, used twice, as the hole's boundary —
1670    // the surface's two sheets meet there by construction). Such a pair
1671    // carries one point on two parameters exactly as a fold does, and is not
1672    // one: 2026-10-17, `fillet-mouth-over-wall-bore-5p5-r2p2`, convicted
1673    // under a coarser rim mesh whose triangles crossed the slit. A fold that
1674    // reaches the boundary from INSIDE is still a fold (one foot inside).
1675    let mut inside: Vec<(&crate::projection::SurfaceProjection, Vec3, bool)> = Vec::new();
1676    for foot in &feet {
1677        let uv = Vec2 {
1678            x: foot.u,
1679            y: foot.v,
1680        };
1681        let on_boundary = match parameter_point_in_face(face, uv, trim_band) {
1682            Ok(PolygonClass::Outside) => continue,
1683            Ok(PolygonClass::Boundary) => true,
1684            Ok(PolygonClass::Inside) => false,
1685            Err(message) => {
1686                fold_trace!("fold face {}: containment: {message}", face.id);
1687                return FoldVerdict::Undecided;
1688            }
1689        };
1690        let Ok(normal) = surface.normal(foot.u, foot.v).and_then(|normal| normal.normalized())
1691        else {
1692            fold_trace!("fold face {}: a normal could not be evaluated", face.id);
1693            return FoldVerdict::Undecided;
1694        };
1695        inside.push((foot, normal, on_boundary));
1696    }
1697    if inside.len() < 2 {
1698        fold_trace!(
1699            "fold face {}: {} of {} feet are inside the trim",
1700            face.id,
1701            inside.len(),
1702            feet.len()
1703        );
1704        return FoldVerdict::Rejected;
1705    }
1706
1707    let mut chosen: Option<(f64, usize, usize, f64)> = None;
1708    let mut worst_sine = 0.0f64;
1709    let mut identified = 0usize;
1710    let mut trim_pairs = 0usize;
1711    for first in 0..inside.len() {
1712        for second in (first + 1)..inside.len() {
1713            if inside[first].2 && inside[second].2 {
1714                trim_pairs += 1;
1715                continue;
1716            }
1717            // Two feet with the TRIM between them — the uv segment joining
1718            // them leaves the face — are the two sheets either side of a
1719            // slit or a hole, within the coincidence band of each other
1720            // because they meet along it; a fold's two feet are joined
1721            // through the material that folds. (The carved wall again: a
1722            // mesh triangle spanning the slit puts one foot a few 1e-5 of u
1723            // to each side of the crease, 1.1e-5 apart in 3D.)
1724            {
1725                let (a, b) = (inside[first].0, inside[second].0);
1726                let separated = (1..8).any(|k| {
1727                    let s = k as f64 / 8.0;
1728                    let uv = Vec2 { x: a.u + (b.u - a.u) * s, y: a.v + (b.v - a.v) * s };
1729                    matches!(parameter_point_in_face(face, uv, trim_band), Ok(PolygonClass::Outside))
1730                });
1731                if separated {
1732                    trim_pairs += 1;
1733                    continue;
1734                }
1735            }
1736            let sine = inside[first].1.cross(inside[second].1).length();
1737            worst_sine = worst_sine.max(sine);
1738            if sine <= FOLD_TRANSVERSAL_SINE {
1739                continue;
1740            }
1741            // A SEAM the surface does not declare. `closed_directions()` is an
1742            // exact test, and an imported carrier whose two boundary isocurves
1743            // are 2.5e-2 apart fails it while still being one locus reached
1744            // from both ends of the domain — so the period reduction above did
1745            // not fold these two feet together and they read a full span
1746            // apart. Measured rather than assumed: the two isocurves are
1747            // sampled and compared.
1748            if boundary_identified(
1749                surface,
1750                Vec2 { x: inside[first].0.u, y: inside[first].0.v },
1751                Vec2 { x: inside[second].0.u, y: inside[second].0.v },
1752                domain_u,
1753                domain_v,
1754                6.0 * grid.merge,
1755                coincidence,
1756            ) {
1757                identified += 1;
1758                continue;
1759            }
1760            let separation = parameter_separation(
1761                Vec2 { x: inside[first].0.u, y: inside[first].0.v },
1762                Vec2 { x: inside[second].0.u, y: inside[second].0.v },
1763                period,
1764            );
1765            if chosen.is_none_or(|(best, _, _, _)| separation > best) {
1766                chosen = Some((separation, first, second, sine));
1767            }
1768        }
1769    }
1770    let Some((separation, first, second, sine)) = chosen else {
1771        fold_trace!(
1772            "fold face {}: no pair of feet is a fold (widest sine {worst_sine:.3e}, {identified} pair(s) are one locus across an undeclared seam, {trim_pairs} pair(s) both on the trim's own boundary) — a degenerate parameterization or the trim's own seam, not a fold",
1773            face.id
1774        );
1775        return FoldVerdict::Rejected;
1776    };
1777    let (foot_a, normal_a, _) = inside[first];
1778    let (foot_b, normal_b, _) = inside[second];
1779    let angle = sine.atan2(normal_a.dot(normal_b));
1780    fold_trace!(
1781        "fold face {}: CONFIRMED feet ({:.6}, {:.6}) and ({:.6}, {:.6}) separation {separation:.3e} vs merge {:.3e} residual {:.3e} (feet {:.3e}/{:.3e} from the point, band {coincidence:.3e}) normals {:.2}deg",
1782        face.id,
1783        foot_a.u,
1784        foot_a.v,
1785        foot_b.u,
1786        foot_b.v,
1787        grid.merge,
1788        foot_a.point.sub(foot_b.point).length(),
1789        foot_a.distance,
1790        foot_b.distance,
1791        angle.to_degrees()
1792    );
1793
1794    FoldVerdict::Confirmed(FaceFold {
1795        face: face.id,
1796        face_name: face.name.clone(),
1797        uv_a: [foot_a.u, foot_a.v],
1798        uv_b: [foot_b.u, foot_b.v],
1799        point: foot_a.point.add(foot_b.point).scale(0.5),
1800        uv_separation: separation,
1801        uv_band: grid.merge,
1802        residual: foot_a.point.sub(foot_b.point).length(),
1803        normal_angle: angle,
1804    })
1805}
1806
1807/// Map every mesh vertex to a representative index, welding positions that
1808/// coincide within `weld`. Bucketed on a `weld`-sized lattice with the 27
1809/// neighbouring cells consulted, so a pair straddling a cell boundary still
1810/// welds.
1811fn weld_positions(positions: &[f64], weld: f64) -> Vec<u32> {
1812    let count = positions.len() / 3;
1813    let mut representative = vec![0u32; count];
1814    let cell = weld.max(f64::MIN_POSITIVE);
1815    let mut buckets: HashMap<(i64, i64, i64), Vec<u32>> = HashMap::default();
1816    for index in 0..count {
1817        let base = index * 3;
1818        let point = Vec3::new(positions[base], positions[base + 1], positions[base + 2]);
1819        let key = (
1820            (point.x / cell).floor() as i64,
1821            (point.y / cell).floor() as i64,
1822            (point.z / cell).floor() as i64,
1823        );
1824        let mut found = None;
1825        'search: for dx in -1..=1 {
1826            for dy in -1..=1 {
1827                for dz in -1..=1 {
1828                    let Some(list) = buckets.get(&(key.0 + dx, key.1 + dy, key.2 + dz)) else {
1829                        continue;
1830                    };
1831                    for candidate in list {
1832                        let base = *candidate as usize * 3;
1833                        let other = Vec3::new(
1834                            positions[base],
1835                            positions[base + 1],
1836                            positions[base + 2],
1837                        );
1838                        if other.sub(point).length() <= weld {
1839                            found = Some(representative[*candidate as usize]);
1840                            break 'search;
1841                        }
1842                    }
1843                }
1844            }
1845        }
1846        let value = found.unwrap_or(index as u32);
1847        representative[index] = value;
1848        buckets.entry(key).or_default().push(index as u32);
1849    }
1850    representative
1851}
1852
1853/// Signed distances of a triangle's corners to `surface`, reduced to the
1854/// smaller of the two one-sided margins: positive means the corners genuinely
1855/// sit on both sides of the carrier by at least that much, zero means they do
1856/// not straddle it at all. `None` when a projection or a normal could not be
1857/// evaluated — never a verdict.
1858///
1859/// Three projections, and only for a triangle pair that already survived the
1860/// BVH, the welded-vertex skip and the mesh crossing test.
1861fn straddle(surface: &NurbsSurface, triangle: &[Vec3; 3], band: f64) -> Option<f64> {
1862    let mut above = 0.0f64;
1863    let mut below = 0.0f64;
1864    for corner in triangle {
1865        let projection = project_point_to_surface(surface, *corner).ok()?;
1866        if projection.distance <= band {
1867            continue;
1868        }
1869        let normal = surface.normal(projection.u, projection.v).ok()?;
1870        let normal = normal.normalized().ok()?;
1871        let signed = corner.sub(projection.point).dot(normal);
1872        if signed > 0.0 {
1873            above = above.max(signed);
1874        } else {
1875            below = below.max(-signed);
1876        }
1877    }
1878    Some(above.min(below))
1879}
1880
1881/// `BREP_SCAN_NODE_MEMBERSHIP=0` restores the pre-2026-10-03 verdict, where a
1882/// tessellation node's side of the other carrier counted whether or not the
1883/// node lay inside its own face's trim. Diagnostic A/B hatch, not a policy.
1884fn node_membership_enabled() -> bool {
1885    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1886    *ON.get_or_init(|| std::env::var("BREP_SCAN_NODE_MEMBERSHIP").as_deref() != Ok("0"))
1887}
1888
1889/// Is `point` a point `face` OWNS: on the face's carrier, inside (or on) its
1890/// trim? `Some(false)` when the carrier foot classifies OUTSIDE the trim;
1891/// `None` when the projection or the classification could not be evaluated
1892/// — never a verdict, exactly as [`across_crossing`] treats its samples. A
1893/// foot ON the trim counts as the face's material. The band is
1894/// [`TRIM_SAMPLE_TOLERANCE`], the same the across-samples are asked with.
1895///
1896/// Why this question exists: a watertight tessellation's interior node is an
1897/// exact point of the carrier but need not be a point of the FACE — on a
1898/// highly curved chart the trim boundary between two boundary samples bows
1899/// away from the UV chord, and nodes land past the trim. Their signed
1900/// distances to another carrier then read a straddle the body does not have.
1901fn node_material(face: &FaceRecord, point: Vec3, band: f64) -> Option<bool> {
1902    if !node_membership_enabled() {
1903        return Some(true);
1904    }
1905    let projection = project_point_to_surface(&face.surface, point).ok()?;
1906    // A finite carrier clamps a point past its domain onto its boundary, and
1907    // the trim then reads that boundary foot as the face's: a point the
1908    // carrier does not reach within `band` is not the face's material.
1909    if projection.distance > band {
1910        return Some(false);
1911    }
1912    match parameter_point_in_face(face, Vec2 { x: projection.u, y: projection.v }, TRIM_SAMPLE_TOLERANCE) {
1913        Ok(PolygonClass::Outside) => Some(false),
1914        Ok(_) => Some(true),
1915        Err(_) => None,
1916    }
1917}
1918
1919/// Does `face`'s trim OWN the spot under `point`: the carrier FOOT of the
1920/// point classifies inside or on the trim? No reach test — a mesh crossing's
1921/// midpoint is a point of two chords, off a curved carrier by the chord's
1922/// sagitta even at a genuine crossing (the oblique bore-mouth control read 0
1923/// confirmed with a reach test here, 2026-10-03), so the question for a
1924/// witness is only whether the faces' trims cover where it lands. `None` when
1925/// the projection or the classification cannot be evaluated.
1926fn footprint_material(face: &FaceRecord, point: Vec3) -> Option<bool> {
1927    if !node_membership_enabled() {
1928        return Some(true);
1929    }
1930    let projection = project_point_to_surface(&face.surface, point).ok()?;
1931    let (closed_u, closed_v) = face.surface.closed_directions().ok()?;
1932    let domains = [face.surface.domain_u().ok()?, face.surface.domain_v().ok()?];
1933    let parameters = [projection.u, projection.v];
1934    let closed = [closed_u, closed_v];
1935    // A closest point constrained to a finite chart boundary is not an
1936    // unconstrained carrier foot when the residual has a tangential component.
1937    // Keep that witness undecided. Pure normal displacement (chord sagitta)
1938    // does not disqualify a foot, including a legitimate foot on the boundary.
1939    // The only spatial floor here is the existing trim-sample tolerance.
1940    for axis in 0..2 {
1941        if closed[axis] {
1942            continue;
1943        }
1944        let [first, last] = domains[axis];
1945        let roundoff = 32.0 * f64::EPSILON * first.abs().max(last.abs()).max(last - first);
1946        if parameters[axis] > first + roundoff && parameters[axis] < last - roundoff {
1947            continue;
1948        }
1949        let derivatives = face.surface.derivatives(projection.u, projection.v, 1).ok()?;
1950        let tangent = if axis == 0 { derivatives[1][0] } else { derivatives[0][1] };
1951        let tangent = tangent.normalized().ok()?;
1952        let tangential_residual = point.sub(projection.point).dot(tangent);
1953        if !tangential_residual.is_finite() || tangential_residual.abs() > TRIM_SAMPLE_TOLERANCE {
1954            return None;
1955        }
1956    }
1957    match parameter_point_in_face(face, Vec2 { x: projection.u, y: projection.v }, TRIM_SAMPLE_TOLERANCE) {
1958        Ok(PolygonClass::Outside) => Some(false),
1959        Ok(_) => Some(true),
1960        Err(_) => None,
1961    }
1962}
1963
1964/// [`straddle`] over the corners `own_face` OWNS: a corner outside its own
1965/// trim ([`node_material`] `Some(false)`) contributes no side; a corner whose
1966/// membership cannot be evaluated makes the whole reading `None`. With every
1967/// corner a member this is exactly [`straddle`].
1968fn straddle_material(
1969    own_face: &FaceRecord,
1970    other: &NurbsSurface,
1971    triangle: &[Vec3; 3],
1972    band: f64,
1973) -> Option<f64> {
1974    let mut above = 0.0f64;
1975    let mut below = 0.0f64;
1976    for corner in triangle {
1977        match node_material(own_face, *corner, band) {
1978            Some(true) => {}
1979            Some(false) => {
1980                pierce_trace!(
1981                    "    corner ({:.6}, {:.6}, {:.6}) of face {} lies OUTSIDE its own trim; its side is not read",
1982                    corner.x, corner.y, corner.z, own_face.id
1983                );
1984                continue;
1985            }
1986            None => return None,
1987        }
1988        let projection = project_point_to_surface(other, *corner).ok()?;
1989        if projection.distance <= band {
1990            continue;
1991        }
1992        let normal = other.normal(projection.u, projection.v).ok()?;
1993        let normal = normal.normalized().ok()?;
1994        let signed = corner.sub(projection.point).dot(normal);
1995        if signed > 0.0 {
1996            above = above.max(signed);
1997        } else {
1998            below = below.max(-signed);
1999        }
2000    }
2001    Some(above.min(below))
2002}
2003
2004/// Which confirmation convicted a [`FaceCrossing`].
2005///
2006/// Recorded so a sweep can attribute every hit to the question that caught it.
2007/// The two ask the same thing — does one face's surface sit on BOTH sides of
2008/// the other's carrier — of different sample points, and the sample points are
2009/// the whole difference between them.
2010#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
2011#[non_exhaustive]
2012pub enum CrossingConfirmation {
2013    /// [`straddle`]: the triangles' OWN CORNERS, in both directions.
2014    Corners,
2015    /// [`pierce_straddle`]: an edge of one triangle through the other's
2016    /// interior, with the samples taken at the pierce.
2017    Pierce,
2018    /// Neither of those decided it; the carriers did (`resolve.rs`): a point
2019    /// both faces own on their common curve, crossed transversally, straddled
2020    /// past the band both ways. Added 2026-10-16 (lane K2).
2021    Resolved,
2022}
2023
2024/// Signed distance from `point` to `surface`, positive on the side the
2025/// surface's normal points to. `None` when the projection or the normal could
2026/// not be evaluated — never a verdict.
2027fn signed_distance(surface: &NurbsSurface, point: Vec3) -> Option<f64> {
2028    let projection = project_point_to_surface(surface, point).ok()?;
2029    let normal = surface.normal(projection.u, projection.v).ok()?;
2030    let normal = normal.normalized().ok()?;
2031    Some(point.sub(projection.point).dot(normal))
2032}
2033
2034/// The SECOND confirmation, and the one a PLANAR face passing through a curved
2035/// one needs.
2036///
2037/// [`straddle`] reads a triangle's OWN CORNERS, and a planar face is
2038/// tessellated as the two triangles its rectangle needs. When such a face
2039/// passes through a cylinder every one of those corners is OUTSIDE the
2040/// cylinder, so the corner test reads a one-sided margin of zero and REJECTS a
2041/// body that demonstrably passes through itself: a 20-block whose `-X` wall is
2042/// pushed to `x = 6` through its own `r = 6` bore encloses 1669.026644711,
2043/// which is exactly the closed form that removes the bore's segment TWICE.
2044///
2045/// The same question is asked where the two triangles actually meet instead:
2046///
2047/// 1. An EDGE of one triangle passes through the INTERIOR of the other
2048///    (segment-triangle intersection, with a barycentric margin, so two
2049///    triangles meeting along a shared boundary are never a pierce).
2050/// 2. That edge's two ENDPOINTS straddle the pierced face's carrier — the same
2051///    signed-distance question [`straddle`] asks of a triangle's corners,
2052///    restricted to the two points that bracket the pierce.
2053/// 3. Two points ON the pierced triangle, either side of the two triangles'
2054///    crossing line and inside that triangle, straddle the PIERCING face's
2055///    carrier. This is the half the corner test cannot reach, and it is what
2056///    separates a face passing THROUGH another from one merely touching it: at
2057///    a tangency both samples sit on the SAME side, however deep the chords
2058///    interpenetrate.
2059///
2060/// Returns the smallest margin the confirmation cleared, or `None` when
2061/// neither direction confirmed.
2062fn pierce_straddle(
2063    first: &[Vec3; 3],
2064    second: &[Vec3; 3],
2065    face_first: &FaceRecord,
2066    face_second: &FaceRecord,
2067    crossing: (Vec3, Vec3),
2068    band: f64,
2069) -> PierceVerdict {
2070    pierce_trace!(" forward: an edge of the first triangle through the second");
2071    let forward = pierce_direction(first, second, face_first, face_second, crossing, band);
2072    pierce_trace!(" backward: an edge of the second triangle through the first");
2073    let backward = pierce_direction(second, first, face_second, face_first, crossing, band);
2074    match (forward, backward) {
2075        (PierceVerdict::Confirmed(a), PierceVerdict::Confirmed(b)) => {
2076            PierceVerdict::Confirmed(if a.margin() >= b.margin() { a } else { b })
2077        }
2078        (PierceVerdict::Confirmed(hit), _) | (_, PierceVerdict::Confirmed(hit)) => {
2079            PierceVerdict::Confirmed(hit)
2080        }
2081        // A projection or a normal that could not be evaluated is never a
2082        // verdict — the module's rule, and the same one the corner lane keeps.
2083        (PierceVerdict::Undecided, _) | (_, PierceVerdict::Undecided) => PierceVerdict::Undecided,
2084        _ => PierceVerdict::Rejected,
2085    }
2086}
2087
2088/// The pierce lane's two margins, both measured between EXACT surface points.
2089#[derive(Clone, Copy, Debug)]
2090struct PierceHit {
2091    /// How far the piercing mesh edge's two endpoints sit either side of the
2092    /// pierced face's carrier.
2093    bracket: f64,
2094    /// How far the pierced face's own SURFACE reaches either side of the
2095    /// piercing face's carrier, across the crossing line.
2096    penetration: f64,
2097}
2098
2099impl PierceHit {
2100    fn margin(&self) -> f64 {
2101        self.bracket.min(self.penetration)
2102    }
2103}
2104
2105enum PierceVerdict {
2106    Confirmed(PierceHit),
2107    Rejected,
2108    Undecided,
2109}
2110
2111/// One direction of [`pierce_straddle`]: an edge of `piercing` through
2112/// `pierced`'s interior. The widest margin over the three edges.
2113fn pierce_direction(
2114    piercing: &[Vec3; 3],
2115    pierced: &[Vec3; 3],
2116    face_piercing: &FaceRecord,
2117    face_pierced: &FaceRecord,
2118    crossing: (Vec3, Vec3),
2119    band: f64,
2120) -> PierceVerdict {
2121    let (surface_piercing, surface_pierced) = (&face_piercing.surface, &face_pierced.surface);
2122    let mut best: Option<PierceHit> = None;
2123    let mut undecided = false;
2124    for index in 0..3 {
2125        let p0 = piercing[index];
2126        let p1 = piercing[(index + 1) % 3];
2127        let Some(fraction) = segment_pierces_triangle(p0, p1, pierced) else {
2128            pierce_trace!("  edge {index}: does not pierce the other triangle's interior");
2129            continue;
2130        };
2131        let point = p0.add(p1.sub(p0).scale(fraction));
2132        pierce_trace!(
2133            "  edge {index}: pierces at ({:.6}, {:.6}, {:.6}); bracket distances {:?} {:?}",
2134            point.x,
2135            point.y,
2136            point.z,
2137            signed_distance(surface_pierced, p0),
2138            signed_distance(surface_pierced, p1)
2139        );
2140        // The bracket is read from two tessellation nodes of the piercing
2141        // face; a node outside that face's own trim is not the face's
2142        // material, so its side says nothing about the body (2026-10-03). The
2143        // pair is then UNEVALUATED along this edge — never rejected, the same
2144        // rule the across-samples keep — and the trace records the readings.
2145        match (node_material(face_piercing, p0, band), node_material(face_piercing, p1, band)) {
2146            (Some(true), Some(true)) => {}
2147            (in_p0, in_p1) => {
2148                pierce_trace!(
2149                    "  edge {index}: bracket endpoints in their own face {}: {in_p0:?} {in_p1:?} — not read",
2150                    face_piercing.id
2151                );
2152                undecided = true;
2153                continue;
2154            }
2155        }
2156        let bracket = match bracket_straddle(surface_pierced, p0, p1, band) {
2157            Margin::Cleared(value) => value,
2158            Margin::Failed => continue,
2159            Margin::Unevaluated => {
2160                undecided = true;
2161                continue;
2162            }
2163        };
2164        let penetration = match across_crossing(
2165            surface_piercing,
2166            face_pierced,
2167            pierced,
2168            point,
2169            crossing,
2170            band,
2171        ) {
2172            Margin::Cleared(value) => value,
2173            Margin::Failed => continue,
2174            Margin::Unevaluated => {
2175                undecided = true;
2176                continue;
2177            }
2178        };
2179        let hit = PierceHit {
2180            bracket,
2181            penetration,
2182        };
2183        if best.is_none_or(|value| hit.margin() > value.margin()) {
2184            best = Some(hit);
2185        }
2186    }
2187    match best {
2188        Some(hit) => PierceVerdict::Confirmed(hit),
2189        None if undecided => PierceVerdict::Undecided,
2190        None => PierceVerdict::Rejected,
2191    }
2192}
2193
2194/// One margin the pierce lane asked for: cleared with a value, genuinely not
2195/// cleared, or not evaluable at all. The third is never a verdict.
2196enum Margin {
2197    Cleared(f64),
2198    Failed,
2199    Unevaluated,
2200}
2201
2202/// Where the segment `p0 -> p1` passes through `triangle`'s INTERIOR, as the
2203/// fraction along the segment. `None` when it misses, when it is parallel to
2204/// the triangle's plane, or when it crosses within `INTERIOR_MARGIN` of the
2205/// triangle's boundary — a hit on the boundary is two triangles MEETING, which
2206/// every watertight tessellation does along every edge.
2207fn segment_pierces_triangle(p0: Vec3, p1: Vec3, triangle: &[Vec3; 3]) -> Option<f64> {
2208    /// Barycentric margin, as a fraction of the triangle.
2209    const INTERIOR_MARGIN: f64 = 1e-6;
2210    let edge_one = triangle[1].sub(triangle[0]);
2211    let edge_two = triangle[2].sub(triangle[0]);
2212    let direction = p1.sub(p0);
2213    let cross = direction.cross(edge_two);
2214    let determinant = edge_one.dot(cross);
2215    if determinant.abs() <= 1e-18 {
2216        return None; // parallel to the triangle's plane
2217    }
2218    let inverse = 1.0 / determinant;
2219    let to_corner = p0.sub(triangle[0]);
2220    let u = to_corner.dot(cross) * inverse;
2221    let second_cross = to_corner.cross(edge_one);
2222    let v = direction.dot(second_cross) * inverse;
2223    if u <= INTERIOR_MARGIN || v <= INTERIOR_MARGIN || u + v >= 1.0 - INTERIOR_MARGIN {
2224        return None;
2225    }
2226    let fraction = edge_two.dot(second_cross) * inverse;
2227    if fraction <= 0.0 || fraction >= 1.0 {
2228        return None;
2229    }
2230    Some(fraction)
2231}
2232
2233/// Do `p0` and `p1` sit on OPPOSITE sides of `surface`, each by more than
2234/// `band`? The smaller of the two margins, or `None`.
2235fn bracket_straddle(surface: &NurbsSurface, p0: Vec3, p1: Vec3, band: f64) -> Margin {
2236    let (Some(first), Some(second)) = (
2237        signed_distance(surface, p0),
2238        signed_distance(surface, p1),
2239    ) else {
2240        return Margin::Unevaluated;
2241    };
2242    if first.abs() <= band || second.abs() <= band || (first > 0.0) == (second > 0.0) {
2243        return Margin::Failed;
2244    }
2245    Margin::Cleared(first.abs().min(second.abs()))
2246}
2247
2248/// The boundary band [`parameter_point_in_face`] is asked with here, so that a
2249/// sample sitting ON the trim reads as the face's material rather than outside
2250/// it. The comparand is the same containment band the imprint's own window
2251/// guard uses (`restricted_carrier`'s wrap sample, `1e-9`) — the tightest in
2252/// the kernel, since the question is "is this spot the face's at all", not "how
2253/// far may it stray". The overshoot this test exists to catch is four orders
2254/// wider (2e-5 on the helmet's turning point).
2255const TRIM_SAMPLE_TOLERANCE: f64 = 1e-9;
2256
2257/// `BREP_PIERCE_TRIM_SAMPLES=0` restores the pre-2026-09-16 verdict, where an
2258/// across-sample was taken from the triangle without asking the face whether
2259/// it owns that spot.
2260fn trim_samples_enabled() -> bool {
2261    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2262    *ON.get_or_init(|| std::env::var("BREP_PIERCE_TRIM_SAMPLES").as_deref() != Ok("0"))
2263}
2264
2265/// Does `triangle` sit on BOTH sides of `surface`, either side of the crossing
2266/// line through `point`?
2267///
2268/// The two samples are taken on the triangle, stepping perpendicular to the
2269/// crossing line, half way to the triangle's boundary in each direction, and
2270/// then put back onto the face's OWN carrier so the margin is a property of
2271/// the two surfaces rather than of the mesh. A face passing THROUGH the
2272/// carrier puts them on opposite sides; a face TOUCHING it puts both on the
2273/// same one, which is the rejection this test exists to make.
2274///
2275/// A TRIANGLE IS NOT THE FACE'S MATERIAL. Its corners lie on the trim, but the
2276/// boundary is approximated by chords, and where the boundary turns sharply
2277/// the chord bows to the outside: the triangle then covers a sliver the face
2278/// does not own. Measured on `20_helmet_merge_stage` b − a, at the turning
2279/// point of the helmet `Face_15` × cutter-plane section, a boundary chord of
2280/// 6.4e-4 stood 2e-5 outside its own trim, and a sample there read 1.72e-6
2281/// through the other carrier against a band of 2.2e-7: a confirmed crossing on
2282/// geometry no face carries. So each sample is asked of the pierced face's own
2283/// trim, and a sample outside it leaves the pair UNEVALUATED — never rejected,
2284/// because a genuine crossing whose sample happened to land outside would then
2285/// read clean. Escape hatch `BREP_PIERCE_TRIM_SAMPLES=0`.
2286fn across_crossing(
2287    surface: &NurbsSurface,
2288    own_face: &FaceRecord,
2289    triangle: &[Vec3; 3],
2290    point: Vec3,
2291    crossing: (Vec3, Vec3),
2292    band: f64,
2293) -> Margin {
2294    let own = &own_face.surface;
2295    let along = crossing.1.sub(crossing.0);
2296    let (Ok(along), Some((normal, _))) = (along.normalized(), triangle_plane(triangle)) else {
2297        return Margin::Unevaluated;
2298    };
2299    let Ok(across) = normal.cross(along).normalized() else {
2300        return Margin::Unevaluated;
2301    };
2302    // How far the samples may step before leaving the triangle: the three
2303    // inward half-planes of its own edges, clipped against the ray.
2304    let mut low = f64::NEG_INFINITY;
2305    let mut high = f64::INFINITY;
2306    for index in 0..3 {
2307        let a = triangle[index];
2308        let b = triangle[(index + 1) % 3];
2309        let opposite = triangle[(index + 2) % 3];
2310        let mut inward = normal.cross(b.sub(a));
2311        if inward.dot(opposite.sub(a)) < 0.0 {
2312            inward = inward.scale(-1.0);
2313        }
2314        let slope = inward.dot(across);
2315        let offset = inward.dot(point.sub(a));
2316        if slope.abs() <= 1e-18 {
2317            if offset < 0.0 {
2318                return Margin::Failed;
2319            }
2320            continue;
2321        }
2322        let bound = -offset / slope;
2323        if slope > 0.0 {
2324            low = low.max(bound);
2325        } else {
2326            high = high.min(bound);
2327        }
2328    }
2329    if !(low < 0.0) || !(high > 0.0) || !low.is_finite() || !high.is_finite() {
2330        pierce_trace!("    across: no room either side of the crossing (reach {low:.6}..{high:.6})");
2331        return Margin::Failed;
2332    }
2333    // Both samples sit on the pierced TRIANGLE, which is a chord of its own
2334    // face and up to the tessellation's chord tolerance off the surface it
2335    // approximates — on an imported model that tolerance is 1.5e-3 of the
2336    // model's extent, orders above the band this margin has to clear. Put each
2337    // sample back ON its own carrier before asking the other surface about it,
2338    // so what is measured is a property of the two SURFACES rather than of how
2339    // coarsely one of them was meshed. On a planar face this is the identity.
2340    let hold_to_trim = trim_samples_enabled();
2341    let samples = [low * 0.5, high * 0.5].map(|step| {
2342        let projection = project_point_to_surface(own, point.add(across.scale(step))).ok();
2343        let sample = projection.as_ref().map(|projection| projection.point);
2344        let material = match (&projection, hold_to_trim) {
2345            (Some(projection), true) => !matches!(
2346                parameter_point_in_face(
2347                    own_face,
2348                    Vec2 { x: projection.u, y: projection.v },
2349                    TRIM_SAMPLE_TOLERANCE,
2350                ),
2351                Ok(PolygonClass::Outside)
2352            ),
2353            _ => true,
2354        };
2355        let reading = sample.and_then(|sample| signed_distance(surface, sample));
2356        if let Some(sample) = sample {
2357            pierce_trace!(
2358                "    across step {step:.6} (reach {low:.6}..{high:.6}): sample ({:.6}, {:.6}, {:.6}) reads {reading:?}{}",
2359                sample.x,
2360                sample.y,
2361                sample.z,
2362                if material { "" } else { " OUTSIDE its own trim" }
2363            );
2364        }
2365        material.then_some(reading).flatten()
2366    });
2367    let [Some(below), Some(above)] = samples else {
2368        return Margin::Unevaluated;
2369    };
2370    if below.abs() <= band || above.abs() <= band || (below > 0.0) == (above > 0.0) {
2371        return Margin::Failed;
2372    }
2373    Margin::Cleared(below.abs().min(above.abs()))
2374}
2375
2376/// The segment along which two triangles cross, or `None` when they do not
2377/// cross transversally. Coplanar pairs return `None` — a coplanar overlap is a
2378/// different defect and this detector does not claim it.
2379fn triangle_crossing(first: &[Vec3; 3], second: &[Vec3; 3]) -> Option<(Vec3, Vec3)> {
2380    let plane_first = triangle_plane(first)?;
2381    let plane_second = triangle_plane(second)?;
2382    let direction = plane_first.0.cross(plane_second.0);
2383    let length = direction.length();
2384    if length <= 1e-12 {
2385        return None; // parallel or coplanar
2386    }
2387    let direction = direction.scale(1.0 / length);
2388
2389    let first_span = plane_span(first, plane_second, direction)?;
2390    let second_span = plane_span(second, plane_first, direction)?;
2391    let low = first_span.0.max(second_span.0);
2392    let high = first_span.1.min(second_span.1);
2393    if high < low {
2394        return None;
2395    }
2396    // Rebuild 3D points by interpolating the first triangle's own crossing
2397    // chord, so the returned segment is on the mesh rather than reconstructed
2398    // from a line equation.
2399    let span = first_span.1 - first_span.0;
2400    let lerp = |value: f64| {
2401        if span <= 0.0 {
2402            first_span.2
2403        } else {
2404            let fraction = ((value - first_span.0) / span).clamp(0.0, 1.0);
2405            first_span
2406                .2
2407                .add(first_span.3.sub(first_span.2).scale(fraction))
2408        }
2409    };
2410    Some((lerp(low), lerp(high)))
2411}
2412
2413/// Unit normal and plane offset of a triangle, `None` for a degenerate one.
2414fn triangle_plane(triangle: &[Vec3; 3]) -> Option<(Vec3, f64)> {
2415    let normal = triangle[1]
2416        .sub(triangle[0])
2417        .cross(triangle[2].sub(triangle[0]));
2418    let length = normal.length();
2419    if length <= 1e-18 {
2420        return None;
2421    }
2422    let normal = normal.scale(1.0 / length);
2423    Some((normal, -normal.dot(triangle[0])))
2424}
2425
2426/// Where `triangle` crosses `plane`, as `(low, high, low_point, high_point)`
2427/// parametrized by `point · direction`. `None` when the triangle does not
2428/// reach the plane.
2429fn plane_span(
2430    triangle: &[Vec3; 3],
2431    plane: (Vec3, f64),
2432    direction: Vec3,
2433) -> Option<(f64, f64, Vec3, Vec3)> {
2434    let distance = |point: Vec3| plane.0.dot(point) + plane.1;
2435    let distances = [
2436        distance(triangle[0]),
2437        distance(triangle[1]),
2438        distance(triangle[2]),
2439    ];
2440    if distances.iter().all(|value| *value > 0.0) || distances.iter().all(|value| *value < 0.0) {
2441        return None;
2442    }
2443    let mut points: Vec<Vec3> = Vec::with_capacity(3);
2444    for index in 0..3 {
2445        let next = (index + 1) % 3;
2446        if distances[index] == 0.0 {
2447            points.push(triangle[index]);
2448        }
2449        if (distances[index] < 0.0) != (distances[next] < 0.0)
2450            && distances[index] != 0.0
2451            && distances[next] != 0.0
2452        {
2453            let fraction = distances[index] / (distances[index] - distances[next]);
2454            points.push(
2455                triangle[index].add(triangle[next].sub(triangle[index]).scale(fraction)),
2456            );
2457        }
2458    }
2459    if points.len() < 2 {
2460        return None;
2461    }
2462    let mut low = (f64::INFINITY, Vec3::default());
2463    let mut high = (f64::NEG_INFINITY, Vec3::default());
2464    for point in points {
2465        let value = point.dot(direction);
2466        if value < low.0 {
2467            low = (value, point);
2468        }
2469        if value > high.0 {
2470            high = (value, point);
2471        }
2472    }
2473    Some((low.0, high.0, low.1, high.1))
2474}
2475
2476
2477// ---------------------------------------------------------------------------
2478// Euler parity
2479// ---------------------------------------------------------------------------
2480
2481/// Outcome of [`solid_euler`].
2482#[derive(Clone, Debug, Serialize)]
2483pub struct EulerReport {
2484    /// `V − E + F − H` over the reduced complex — the same count
2485    /// [`BrepSolid::validate`] makes, with degenerate (collapsed) cells left
2486    /// out of all four terms.
2487    pub characteristic: i64,
2488    pub shells: usize,
2489    /// `S − χ/2`, and `None` when `characteristic` is odd — there is no such
2490    /// surface, so there is no genus to report rather than a halved one.
2491    pub genus: Option<i64>,
2492    /// What the solid carries in its `genus` field.
2493    pub recorded_genus: i64,
2494    /// Non-degenerate edges used by FEWER than two coedges — a boundary edge,
2495    /// or an edge record no loop references at all. A closed surface has
2496    /// neither; a sheet or an in-progress shell legitimately has the first, and
2497    /// its characteristic is then unconstrained. Both are counted here because
2498    /// both mean the same thing for parity: these counts are not those of a
2499    /// closed surface, so decline to judge rather than convict.
2500    pub boundary_edges: usize,
2501    /// Non-degenerate edges used by three or more coedges — not a 2-manifold,
2502    /// so the characteristic means nothing either.
2503    pub non_manifold_edges: usize,
2504}
2505
2506impl EulerReport {
2507    /// Whether the counts describe a closed 2-manifold at all. Parity is only
2508    /// an invariant for one of those.
2509    pub fn is_closed_manifold(&self) -> bool {
2510        self.boundary_edges == 0 && self.non_manifold_edges == 0
2511    }
2512
2513    /// The HARD predicate: a CLOSED manifold whose Euler characteristic is odd.
2514    ///
2515    /// No closed orientable surface has one — χ = 2(S − G) is even by
2516    /// construction — so this is a statement about the record, not about a
2517    /// tolerance, and it cannot be tuned away. It is the one thing the Euler
2518    /// count can establish on its own: the genus itself has no second
2519    /// derivation from the cell complex to check against.
2520    pub fn is_unsound(&self) -> bool {
2521        self.is_closed_manifold() && self.characteristic % 2 != 0
2522    }
2523
2524    /// One line naming what is wrong, or `None`.
2525    pub fn summary(&self) -> Option<String> {
2526        if self.is_unsound() {
2527            return Some(format!(
2528                "Euler characteristic {} is ODD over {} shell(s); no closed orientable \
2529                 surface has one, so these counts do not describe a solid \
2530                 (recorded genus {})",
2531                self.characteristic, self.shells, self.recorded_genus
2532            ));
2533        }
2534        None
2535    }
2536}
2537
2538/// Closedness and Euler parity: `V − E + F − H` over the reduced complex, and
2539/// whether the counts can belong to a closed orientable surface at all.
2540///
2541/// Pure topology — ids and use counts, never a coordinate — so like
2542/// [`solid_connectivity`] it is tolerance-free and cannot be noisy.
2543///
2544/// This does NOT verify the genus. A genus has no second derivation from the
2545/// same cell complex: `genus = S − χ/2` IS the definition, so any recomputation
2546/// re-runs the same arithmetic. (The only independent route is geometric —
2547/// Gauss–Bonnet, `∮K dA` plus the edge and vertex terms — which is a different
2548/// project.) What parity establishes without a second source is that a body
2549/// whose characteristic is odd is not a closed orientable surface at all.
2550///
2551/// The characteristic is a TOTAL over shells, which is necessary but not
2552/// sufficient: two shells each odd sum to even and pass. Per-shell parity needs
2553/// edges attributed to shells, which `cross_shell_edges` makes ambiguous
2554/// exactly when it would matter.
2555pub fn solid_euler(solid: &BrepSolid) -> EulerReport {
2556    let degenerate: HashSet<u64> = solid
2557        .edges
2558        .iter()
2559        .filter(|edge| edge.degenerate)
2560        .map(|edge| edge.id)
2561        .collect();
2562    let mut uses: HashMap<u64, usize> = HashMap::default();
2563    for coedge in solid
2564        .shells
2565        .iter()
2566        .flat_map(|shell| &shell.faces)
2567        .flat_map(|face| &face.loops)
2568        .flat_map(|record| &record.coedges)
2569    {
2570        if !degenerate.contains(&coedge.edge_id) {
2571            *uses.entry(coedge.edge_id).or_insert(0) += 1;
2572        }
2573    }
2574    let mut boundary_edges = 0usize;
2575    let mut non_manifold_edges = 0usize;
2576    for edge in solid.edges.iter().filter(|edge| !edge.degenerate) {
2577        match uses.get(&edge.id).copied().unwrap_or(0) {
2578            2 => {}
2579            0 | 1 => boundary_edges += 1,
2580            _ => non_manifold_edges += 1,
2581        }
2582    }
2583
2584    let live: HashSet<u64> = solid
2585        .edges
2586        .iter()
2587        .filter(|edge| !edge.degenerate)
2588        .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
2589        .collect();
2590    let vertices = solid
2591        .vertices
2592        .iter()
2593        .filter(|vertex| live.contains(&vertex.id))
2594        .count() as i64;
2595    let edges = solid.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
2596    let faces: i64 = solid.shells.iter().map(|shell| shell.faces.len() as i64).sum();
2597    let characteristic = vertices - edges + faces - solid.bounding_hole_count();
2598    let shells = solid.shells.len();
2599    EulerReport {
2600        characteristic,
2601        shells,
2602        genus: (characteristic % 2 == 0).then(|| shells as i64 - characteristic / 2),
2603        recorded_genus: solid.genus,
2604        boundary_edges,
2605        non_manifold_edges,
2606    }
2607}
2608
2609// ---------------------------------------------------------------------------
2610// Face-loop self-crossing
2611// ---------------------------------------------------------------------------
2612
2613/// Parametric exclusion at each chord's ends. A crossing this close to a
2614/// chord end is the shared vertex of two consecutive chords, not a crossing.
2615const CHORD_END_EXCLUSION: f64 = 1e-9;
2616
2617/// Sine of the angle below which two chords are treated as PARALLEL and no
2618/// crossing is computed. Scale-free: it is compared against the cross product
2619/// divided by both lengths, so a long chord pair and a short one are held to
2620/// the same angle. Collinear overlap — the two seam coedges of a periodic
2621/// face, traversed once each way — lands here and is not a crossing.
2622const CHORD_PARALLEL_SINE: f64 = 1e-12;
2623
2624/// Coincidence radius for chord ENDPOINTS, as a fraction of the face's own
2625/// parameter extent. Two chords whose ends meet share a vertex of the loop;
2626/// where that is a loop touching itself at a point it is a PINCH, which is
2627/// `ConnectivityReport::pinch_vertices`' question, not this one.
2628const UV_WELD_FRACTION: f64 = 1e-9;
2629
2630/// One place a face's trim loop crosses ITSELF in the face's parameter domain.
2631///
2632/// The two coedges are the loop's own — a bowtie between two different trims,
2633/// or one pcurve crossing itself, in which case they are the same coedge.
2634#[derive(Clone, Debug, Serialize)]
2635pub struct LoopCrossing {
2636    pub face: u64,
2637    pub face_name: Option<String>,
2638    pub loop_id: u64,
2639    /// The two crossing coedges, in loop order.
2640    pub coedge_a: u64,
2641    pub coedge_b: u64,
2642    /// The edges those coedges use — what a caller NAMES the defect by, since
2643    /// an edge survives the surgery that renumbers coedges.
2644    pub edge_a: u64,
2645    pub edge_b: u64,
2646    /// Where they cross, in the face's own domain (wrapped into it on a
2647    /// periodic direction, so it is a parameter the surface can be evaluated
2648    /// at rather than a covering-plane lift).
2649    pub uv: [f64; 2],
2650    /// The surface point there — the 3D place the face passes through itself.
2651    pub point: Vec3,
2652}
2653
2654/// Outcome of [`loop_self_crossings`].
2655#[derive(Clone, Debug, Serialize)]
2656pub struct LoopCrossingReport {
2657    /// Loops scanned.
2658    pub loops: usize,
2659    /// Chords tested — the sampled trim polygon's segments, summed over loops.
2660    pub chords: usize,
2661    /// One entry per crossing COEDGE PAIR per loop, first one found kept. The
2662    /// pair is the stable half: a denser sampling moves the crossing's `uv`
2663    /// without changing which two trims cross.
2664    ///
2665    /// Two kinds of crossing are deliberately NOT here, both because the two
2666    /// strands are the same curve twice rather than two parts of a boundary:
2667    ///
2668    /// * a crossing within `assembler_weld` of one of the loop's own VERTICES.
2669    ///   Two trims arriving at one vertex are fitted independently, so each
2670    ///   ends a fit-accuracy away from it and near the vertex they overlap.
2671    ///   Measured over the case population on 2026-09-13, every such hit sat
2672    ///   within 2.4e-7 of a vertex and the one true crossing sat 1.8e-1 from
2673    ///   one — four orders of margin either side of the band, not a threshold
2674    ///   decision. The cost is stated: a genuine bowtie whose crossing is
2675    ///   inside that band of a vertex is not reported.
2676    /// * a crossing between two DIFFERENT coedges that use the SAME EDGE — a
2677    ///   seam edge, which a periodic face's loop traverses once each way. The
2678    ///   two are one 3D curve under two fits, so they cannot cross on the
2679    ///   surface; where the fits wobble past each other they only look like
2680    ///   it. A coedge against ITSELF is a different question and is asked.
2681    pub crossings: Vec<LoopCrossing>,
2682    /// Loops whose pcurves could not be evaluated, `face/loop: reason`. A scan
2683    /// that could not read a loop did not clear it, so a caller must not read
2684    /// an empty `crossings` beside a non-empty this as "no crossings".
2685    pub unreadable: Vec<String>,
2686}
2687
2688impl LoopCrossingReport {
2689    /// The predicate: some face's trim loop crosses itself.
2690    pub fn is_flagged(&self) -> bool {
2691        !self.crossings.is_empty()
2692    }
2693
2694    /// One line naming the first crossing, or `None` when nothing crossed.
2695    pub fn summary(&self) -> Option<String> {
2696        let first = self.crossings.first()?;
2697        Some(format!(
2698            "{} loop self-crossing(s); face {}{} loop {} crosses itself where edges {} and {} \
2699             meet at uv ({:.6}, {:.6}) = ({:.4}, {:.4}, {:.4})",
2700            self.crossings.len(),
2701            first.face,
2702            first
2703                .face_name
2704                .as_ref()
2705                .map(|name| format!(" '{name}'"))
2706                .unwrap_or_default(),
2707            first.loop_id,
2708            first.edge_a,
2709            first.edge_b,
2710            first.uv[0],
2711            first.uv[1],
2712            first.point.x,
2713            first.point.y,
2714            first.point.z
2715        ))
2716    }
2717}
2718
2719/// One sampled chord of a lifted trim loop.
2720#[derive(Clone, Copy)]
2721struct Chord {
2722    a: Vec2,
2723    b: Vec2,
2724}
2725
2726impl Chord {
2727    fn shifted(self, shift: Vec2) -> Self {
2728        Self {
2729            a: self.a.add(shift),
2730            b: self.b.add(shift),
2731        }
2732    }
2733
2734    fn direction(&self) -> Vec2 {
2735        self.b.sub(self.a)
2736    }
2737
2738    fn min_u(&self) -> f64 {
2739        self.a.x.min(self.b.x)
2740    }
2741
2742    fn max_u(&self) -> f64 {
2743        self.a.x.max(self.b.x)
2744    }
2745
2746    fn min_v(&self) -> f64 {
2747        self.a.y.min(self.b.y)
2748    }
2749
2750    fn max_v(&self) -> f64 {
2751        self.a.y.max(self.b.y)
2752    }
2753}
2754
2755/// One loop's sampled trim polygon, lifted into the covering plane.
2756struct Chain {
2757    /// `chords + 1` sample points. The last is the loop's closing vertex: the
2758    /// first again on a loop that closes in the domain, the first plus
2759    /// [`Chain::wind`] on one that winds the seam.
2760    points: Vec<Vec2>,
2761    /// The coedge each chord was sampled from.
2762    owner: Vec<usize>,
2763    /// End minus start — zero, or a whole number of periods.
2764    wind: Vec2,
2765}
2766
2767impl Chain {
2768    fn chords(&self) -> usize {
2769        self.owner.len()
2770    }
2771
2772    fn chord(&self, index: usize) -> Chord {
2773        Chord {
2774            a: self.points[index],
2775            b: self.points[index + 1],
2776        }
2777    }
2778
2779    /// Chain vertex `index`, continued past either end by one LAP of the
2780    /// loop's own wind — so the strand at the closing vertex still has the
2781    /// neighbour it has on the surface.
2782    fn vertex(&self, index: i64) -> Vec2 {
2783        let last = self.chords() as i64;
2784        if index < 0 {
2785            self.points[(index + last) as usize].sub(self.wind)
2786        } else if index > last {
2787            self.points[(index - last) as usize].add(self.wind)
2788        } else {
2789            self.points[index as usize]
2790        }
2791    }
2792}
2793
2794/// Which side of the directed chord `point` falls on: `1`, `-1`, or `0`
2795/// within `weld` of its line.
2796fn side_of(chord: &Chord, point: Vec2, weld: f64) -> i32 {
2797    let direction = chord.direction();
2798    let length = direction.length();
2799    if !(length > 0.0) {
2800        return 0;
2801    }
2802    let signed = (direction.x * (point.y - chord.a.y) - direction.y * (point.x - chord.a.x)) / length;
2803    if signed > weld {
2804        1
2805    } else if signed < -weld {
2806        -1
2807    } else {
2808        0
2809    }
2810}
2811
2812/// Where along `chord` the nearest point to `point` is, when `point` is within
2813/// `weld` of the chord at all.
2814fn along_chord(chord: &Chord, point: Vec2, weld: f64) -> Option<f64> {
2815    let direction = chord.direction();
2816    let length_squared = direction.dot(direction);
2817    if !(length_squared > 0.0) {
2818        return None;
2819    }
2820    let parameter = point.sub(chord.a).dot(direction) / length_squared;
2821    let foot = chord.a.add(direction.scale(parameter.clamp(0.0, 1.0)));
2822    (point.sub(foot).length() <= weld).then_some(parameter)
2823}
2824
2825/// Do two strands of the chain CROSS where they meet at one point, or does
2826/// the second merely touch the first and leave the way it came?
2827///
2828/// The four directions out of the shared vertex are compared by angle: the
2829/// strands cross exactly when they ALTERNATE around it — one of the second
2830/// strand's directions inside the sector the first sweeps and one outside.
2831///
2832/// A second-strand direction COLLINEAR with either of the first's — the same
2833/// way or the opposite way — declines instead of answering. That is not an
2834/// edge case: it is the standard periodic face. A cylinder's lateral loop
2835/// spans exactly one period, so the seam it runs UP at `u1` is the seam its
2836/// own period-shifted image runs DOWN, and the two strands lie along each
2837/// other at every seam vertex. Two curves that run along each other do not
2838/// cross there, and a rule that had to pick a side would pick one at every
2839/// seam in the corpus.
2840fn strands_cross(
2841    at: Vec2,
2842    first_before: Vec2,
2843    first_after: Vec2,
2844    second_before: Vec2,
2845    second_after: Vec2,
2846    weld: f64,
2847) -> bool {
2848    let angle = |point: Vec2| -> Option<f64> {
2849        let direction = point.sub(at);
2850        (direction.length() > weld).then(|| direction.y.atan2(direction.x))
2851    };
2852    let (Some(from), Some(to), Some(before), Some(after)) = (
2853        angle(first_before),
2854        angle(first_after),
2855        angle(second_before),
2856        angle(second_after),
2857    ) else {
2858        return false;
2859    };
2860    for second in [before, after] {
2861        for first in [from, to] {
2862            if (second - first).sin().abs() <= CHORD_PARALLEL_SINE {
2863                return false;
2864            }
2865        }
2866    }
2867    let sweep = |from: f64, to: f64| (to - from).rem_euclid(std::f64::consts::TAU);
2868    let span = sweep(from, to);
2869    let inside = |value: f64| {
2870        let offset = sweep(from, value);
2871        offset > 0.0 && offset < span
2872    };
2873    inside(before) != inside(after)
2874}
2875
2876/// Where chords `first` and `second` of one chain cross, with `second`
2877/// displaced by `shift` (zero for the loop against itself).
2878///
2879/// Three ways a chain crosses itself, and the last two are not exotic: a
2880/// symmetric bowtie puts the crossing exactly ON a sample of both strands,
2881/// because both pcurves are sampled at the same fractions of their own
2882/// domains. A detector that only tested for a crossing strictly inside both
2883/// chords would miss the very shape it is for.
2884fn chain_crossing(chain: &Chain, first: usize, second: usize, shift: Vec2, weld: f64) -> Option<Vec2> {
2885    let one = chain.chord(first);
2886    let other = chain.chord(second).shifted(shift);
2887
2888    // 1. Transversally, inside both.
2889    let a = one.direction();
2890    let b = other.direction();
2891    let scale = a.length() * b.length();
2892    let denominator = a.x * b.y - a.y * b.x;
2893    if scale > 0.0 && denominator.abs() > CHORD_PARALLEL_SINE * scale {
2894        let offset = other.a.sub(one.a);
2895        let t = (offset.x * b.y - offset.y * b.x) / denominator;
2896        let u = (offset.x * a.y - offset.y * a.x) / denominator;
2897        let interior = |value: f64| value > CHORD_END_EXCLUSION && value < 1.0 - CHORD_END_EXCLUSION;
2898        if interior(t) && interior(u) {
2899            return Some(one.a.add(a.scale(t)));
2900        }
2901    }
2902
2903    // 2. A chain VERTEX sitting inside the other chord: the strand crosses
2904    //    there when the samples either side of it are on opposite sides.
2905    let through = |chord: &Chord, vertex: i64, displace: Vec2| -> Option<Vec2> {
2906        let point = chain.vertex(vertex).add(displace);
2907        let parameter = along_chord(chord, point, weld)?;
2908        if !(parameter > CHORD_END_EXCLUSION && parameter < 1.0 - CHORD_END_EXCLUSION) {
2909            return None;
2910        }
2911        let before = side_of(chord, chain.vertex(vertex - 1).add(displace), weld);
2912        let after = side_of(chord, chain.vertex(vertex + 1).add(displace), weld);
2913        (before != 0 && after != 0 && before != after).then_some(point)
2914    };
2915    if let Some(point) = through(&other, first as i64 + 1, Vec2 { x: 0.0, y: 0.0 }) {
2916        return Some(point);
2917    }
2918    if let Some(point) = through(&one, second as i64 + 1, shift) {
2919        return Some(point);
2920    }
2921
2922    // 3. The two strands meeting AT one shared vertex. Each chain vertex is
2923    //    the END of exactly one chord, so testing end against end reports a
2924    //    shared vertex once; end against START is the same point one chord
2925    //    earlier and is reached as that pair's end-against-end.
2926    if one.b.sub(other.b).length() <= weld {
2927        let first_vertex = first as i64 + 1;
2928        let second_vertex = second as i64 + 1;
2929        if strands_cross(
2930            one.b,
2931            chain.vertex(first_vertex - 1),
2932            chain.vertex(first_vertex + 1),
2933            chain.vertex(second_vertex - 1).add(shift),
2934            chain.vertex(second_vertex + 1).add(shift),
2935            weld,
2936        ) {
2937            return Some(one.b);
2938        }
2939    }
2940    None
2941}
2942
2943/// Offer every chord pair whose boxes overlap — `first` against `second`, or
2944/// against itself when `second` is `None` — by a sweep over sorted
2945/// u-intervals, so a thousand-chord loop is not a million chord tests.
2946fn sweep_chords(
2947    first: &[Chord],
2948    second: Option<&[Chord]>,
2949    mut offer: impl FnMut(usize, usize),
2950) {
2951    let mut order: Vec<(f64, bool, usize)> = first
2952        .iter()
2953        .enumerate()
2954        .map(|(index, chord)| (chord.min_u(), false, index))
2955        .collect();
2956    if let Some(second) = second {
2957        order.extend(
2958            second
2959                .iter()
2960                .enumerate()
2961                .map(|(index, chord)| (chord.min_u(), true, index)),
2962        );
2963    }
2964    order.sort_by(|a, b| a.0.total_cmp(&b.0));
2965
2966    let mut active_first: Vec<usize> = Vec::new();
2967    let mut active_second: Vec<usize> = Vec::new();
2968    for (sweep_u, is_second, index) in order {
2969        let chord = if is_second {
2970            second.expect("second set indexed")[index]
2971        } else {
2972            first[index]
2973        };
2974        // Lazy deletion: a chord whose u-interval ended before this one starts
2975        // can never meet anything from here on.
2976        active_first.retain(|&other| first[other].max_u() >= sweep_u);
2977        if let Some(second) = second {
2978            active_second.retain(|&other| second[other].max_u() >= sweep_u);
2979        }
2980        // A chord is tested against the ACTIVE chords of the OTHER set, or
2981        // against its own when there is no other. Either way a pair is
2982        // offered exactly once.
2983        let (candidates, pool): (&[usize], &[Chord]) = match (second, is_second) {
2984            (None, _) => (&active_first, first),
2985            (Some(second), false) => (&active_second, second),
2986            (Some(_), true) => (&active_first, first),
2987        };
2988        for &other in candidates {
2989            let against = &pool[other];
2990            if chord.max_v() < against.min_v() || against.max_v() < chord.min_v() {
2991                continue;
2992            }
2993            if is_second {
2994                offer(other, index);
2995            } else {
2996                offer(index, other);
2997            }
2998        }
2999        if is_second {
3000            active_second.push(index);
3001        } else {
3002            active_first.push(index);
3003        }
3004    }
3005}
3006
3007/// The shift that carries `point` onto the period of `target` — zero in a
3008/// direction the surface is not closed in.
3009fn period_shift(point: Vec2, target: Vec2, period: [Option<f64>; 2]) -> Vec2 {
3010    let along = |value: f64, want: f64, period: Option<f64>| match period {
3011        Some(period) if period > 0.0 => ((want - value) / period).round() * period,
3012        _ => 0.0,
3013    };
3014    Vec2 {
3015        x: along(point.x, target.x, period[0]),
3016        y: along(point.y, target.y, period[1]),
3017    }
3018}
3019
3020/// One loop's trim polygon, sampled and lifted into the covering plane: each
3021/// coedge's whole sample run is shifted by the period that joins its start to
3022/// the previous coedge's end.
3023///
3024/// The shift is decided ONCE PER COEDGE, at the boundary where a jump can
3025/// actually occur — a pcurve is one continuous NURBS and does not jump inside
3026/// itself. Deciding it per SAMPLE (which is what `validate()`'s uv-wire
3027/// warning does) takes any genuine chord longer than half a period the short
3028/// way round instead.
3029fn lift_loop(loop_record: &LoopRecord, period: [Option<f64>; 2]) -> Result<Chain, String> {
3030    let mut points: Vec<Vec2> = Vec::new();
3031    let mut owner: Vec<usize> = Vec::new();
3032    let mut previous_end: Option<Vec2> = None;
3033    for (index, coedge) in loop_record.coedges.iter().enumerate() {
3034        let curve = &coedge.pcurve;
3035        let [start, end] = curve.domain()?;
3036        let at = |parameter: f64| -> Result<Vec2, String> {
3037            let value = curve.evaluate(parameter)?;
3038            Ok(Vec2 {
3039                x: value.x,
3040                y: value.y,
3041            })
3042        };
3043        let shift = match previous_end {
3044            Some(target) => period_shift(at(start)?, target, period),
3045            None => Vec2 { x: 0.0, y: 0.0 },
3046        };
3047        let samples = crate::classification::trim_sample_count(curve);
3048        for sample in 0..samples {
3049            let parameter = start + (end - start) * sample as f64 / samples as f64;
3050            points.push(at(parameter)?.add(shift));
3051            owner.push(index);
3052        }
3053        previous_end = Some(at(end)?.add(shift));
3054    }
3055    let wind = match (points.first(), previous_end) {
3056        (Some(first), Some(last)) => last.sub(*first),
3057        _ => Vec2 { x: 0.0, y: 0.0 },
3058    };
3059    if let Some(last) = previous_end {
3060        points.push(last);
3061    }
3062    Ok(Chain {
3063        points,
3064        owner,
3065        wind,
3066    })
3067}
3068
3069/// Does any face's trim loop cross ITSELF in its own parameter domain — a
3070/// bowtie?
3071///
3072/// This is the defect every other detector in this module is blind to by
3073/// construction (see the blind-spot section of the module docs): the crossing
3074/// is INSIDE one face, so there is no face pair to confirm; the incidence in
3075/// the record is perfect, so `validate()` is silent; the counts are unchanged,
3076/// so parity and connectivity are unchanged. The face's own AREA is the only
3077/// other tell, and nothing compares it to anything.
3078///
3079/// The loop is sampled at exactly the density
3080/// [`crate::parameter_point_in_face`]'s generic lane classifies against
3081/// (`trim_sample_count`), lifted into the covering plane a coedge at a time,
3082/// and swept for chord pairs that cross. Chords the CHAIN joins are skipped by
3083/// index, never by proximity: a loop that runs back over its own last vertex
3084/// is a finding, not an adjacency.
3085///
3086/// Reported once per crossing COEDGE PAIR per loop — the stable half, since a
3087/// denser sampling moves a crossing's `uv` without changing which two trims
3088/// cross. Nothing in the kernel's build path calls this; the blend acceptance
3089/// and the case gate are its callers.
3090pub fn loop_self_crossings(solid: &BrepSolid) -> LoopCrossingReport {
3091    let mut report = LoopCrossingReport {
3092        loops: 0,
3093        chords: 0,
3094        crossings: Vec::new(),
3095        unreadable: Vec::new(),
3096    };
3097    // "Are these two independently fitted points the SAME vertex?" — the
3098    // kernel's own named answer, and the band this detector holds a crossing
3099    // AT a loop vertex to. See the `crossings` field for why it is needed and
3100    // what it costs.
3101    let vertex_band = crate::tolerance::assembler_weld(
3102        crate::KernelTolerances::for_solid(solid, 1e-7).model,
3103    );
3104    for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
3105        let (closed_u, closed_v) = match face.surface.closed_directions() {
3106            Ok(closed) => closed,
3107            Err(message) => {
3108                report.unreadable.push(format!("face {}: {message}", face.id));
3109                continue;
3110            }
3111        };
3112        let (domain_u, domain_v) = match (face.surface.domain_u(), face.surface.domain_v()) {
3113            (Ok(u), Ok(v)) => (u, v),
3114            (Err(message), _) | (_, Err(message)) => {
3115                report.unreadable.push(format!("face {}: {message}", face.id));
3116                continue;
3117            }
3118        };
3119        let span_u = domain_u[1] - domain_u[0];
3120        let span_v = domain_v[1] - domain_v[0];
3121        let period = [closed_u.then_some(span_u), closed_v.then_some(span_v)];
3122        // The coincidence band, and the band the side tests hold a sample to.
3123        // Keyed to the face's own parameter extent: uv is not a length, and a
3124        // domain is as likely to be [0, 1] as [0, 2π].
3125        let weld = UV_WELD_FRACTION * (span_u.abs() + span_v.abs()).max(1.0);
3126        for loop_record in &face.loops {
3127            let chain = match lift_loop(loop_record, period) {
3128                Ok(chain) => chain,
3129                Err(message) => {
3130                    report.unreadable.push(format!(
3131                        "face {} loop {}: {message}",
3132                        face.id, loop_record.id
3133                    ));
3134                    continue;
3135                }
3136            };
3137            let count = chain.chords();
3138            if count < 4 {
3139                continue;
3140            }
3141            report.loops += 1;
3142            report.chords += count;
3143            let chords: Vec<Chord> = (0..count).map(|index| chain.chord(index)).collect();
3144
3145            let mut seen: HashSet<(usize, usize)> = HashSet::default();
3146            let mut hits: Vec<(usize, usize, Vec2)> = Vec::new();
3147            let zero = Vec2 { x: 0.0, y: 0.0 };
3148            // Two different coedges of one loop that use the SAME EDGE are two
3149            // parameter-space images of ONE 3D curve — a seam edge, which a
3150            // periodic face's loop traverses once each way. They are the same
3151            // locus on the surface and cannot cross it; where their two
3152            // independent fits wobble past each other at the fit tolerance,
3153            // that is the wobble and not a bowtie. (A coedge against ITSELF is
3154            // a different question and is still asked.)
3155            let same_edge = |first: usize, second: usize| {
3156                let (a, b) = (chain.owner[first], chain.owner[second]);
3157                a != b
3158                    && loop_record.coedges[a].edge_id == loop_record.coedges[b].edge_id
3159            };
3160            let mut record = |first: usize, second: usize, point: Vec2| {
3161                let (a, b) = (chain.owner[first], chain.owner[second]);
3162                let pair = if a <= b { (a, b) } else { (b, a) };
3163                if seen.insert(pair) {
3164                    hits.push((pair.0, pair.1, point));
3165                }
3166            };
3167
3168            // How many whole PERIODS the loop's end is from its start. This is
3169            // the structural question — does the loop close in the domain, or
3170            // wind the seam — and it must not be asked with a tolerance: the
3171            // two ends are one vertex reached by two independent pcurve fits,
3172            // and their uv gap is whatever those fits left (measured up to
3173            // 2.4e-5 on an offset shell, which is above `validate()`'s own
3174            // uv-wire closure band). Rounding to laps answers it exactly.
3175            let laps_between = |value: f64, period: Option<f64>| -> i64 {
3176                match period {
3177                    Some(period) if period > 0.0 => (value / period).round() as i64,
3178                    _ => 0,
3179                }
3180            };
3181            let wind_laps = (
3182                laps_between(chain.wind.x, period[0]),
3183                laps_between(chain.wind.y, period[1]),
3184            );
3185            let joined = wind_laps == (0, 0);
3186
3187            // The loop against itself. Chords the chain JOINS are skipped: the
3188            // consecutive pairs, and the closing pair when the loop closes in
3189            // the domain rather than winding the seam.
3190            sweep_chords(&chords, None, |first, second| {
3191                let (low, high) = if first <= second {
3192                    (first, second)
3193                } else {
3194                    (second, first)
3195                };
3196                if high == low + 1 || (joined && low == 0 && high == count - 1) {
3197                    return;
3198                }
3199                if same_edge(low, high) {
3200                    return;
3201                }
3202                if let Some(point) = chain_crossing(&chain, low, high, zero, weld) {
3203                    record(low, high, point);
3204                }
3205            });
3206
3207            // A PERIODIC face's loop and its own period-shifted image are one
3208            // curve on the surface, so a loop that WINDS can cross itself
3209            // across the seam. The shift is taken in one direction only:
3210            // `(+P, i, j)` and `(-P, j, i)` are the same two points on the
3211            // surface, so testing both would find every seam crossing twice.
3212            let extent = |axis: fn(&Vec2) -> f64| -> f64 {
3213                let values = chain.points.iter().map(axis);
3214                let (lo, hi) = values.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), value| {
3215                    (lo.min(value), hi.max(value))
3216                });
3217                hi - lo
3218            };
3219            let laps = |period: Option<f64>, extent: f64| -> i64 {
3220                match period {
3221                    Some(period) if period > 0.0 => (extent / period).ceil() as i64,
3222                    _ => 0,
3223                }
3224            };
3225            let laps_u = laps(period[0], extent(|point| point.x));
3226            let laps_v = laps(period[1], extent(|point| point.y));
3227            for step_u in 0..=laps_u {
3228                for step_v in -laps_v..=laps_v {
3229                    // (0, 0) is the scan above; the half-lattice `step_u > 0`,
3230                    // or `step_u == 0` and `step_v > 0`, holds one of every
3231                    // (shift, −shift) pair.
3232                    if step_u == 0 && step_v <= 0 {
3233                        continue;
3234                    }
3235                    let shift = Vec2 {
3236                        x: step_u as f64 * period[0].unwrap_or(0.0),
3237                        y: step_v as f64 * period[1].unwrap_or(0.0),
3238                    };
3239                    let shifted: Vec<Chord> = chords
3240                        .iter()
3241                        .map(|chord| chord.shifted(shift))
3242                        .collect();
3243                    // One lap on, the chain's own last chord is joined to its
3244                    // first: that is the seam, not a crossing.
3245                    let laps_forward = (step_u, step_v) == wind_laps;
3246                    let laps_back = (step_u, step_v) == (-wind_laps.0, -wind_laps.1);
3247                    sweep_chords(&chords, Some(&shifted), |first, second| {
3248                        if laps_forward && first == count - 1 && second == 0 {
3249                            return;
3250                        }
3251                        if laps_back && first == 0 && second == count - 1 {
3252                            return;
3253                        }
3254                        if same_edge(first, second) {
3255                            return;
3256                        }
3257                        if let Some(point) = chain_crossing(&chain, first, second, shift, weld) {
3258                            record(first, second, point);
3259                        }
3260                    });
3261                }
3262            }
3263
3264            // Where this loop's own VERTICES are, in 3D: one per coedge, at
3265            // the start of its pcurve.
3266            let mut corners: Vec<Vec3> = Vec::with_capacity(loop_record.coedges.len());
3267            if !hits.is_empty() {
3268                for coedge in &loop_record.coedges {
3269                    let Ok([start, _]) = coedge.pcurve.domain() else {
3270                        continue;
3271                    };
3272                    let Ok(value) = coedge.pcurve.evaluate(start) else {
3273                        continue;
3274                    };
3275                    if let Ok(point) = face.surface.evaluate(value.x, value.y) {
3276                        corners.push(point);
3277                    }
3278                }
3279            }
3280            for (first, second, point) in hits {
3281                let wrap = |value: f64, domain: [f64; 2], period: Option<f64>| match period {
3282                    Some(period) if period > 0.0 => {
3283                        domain[0] + (value - domain[0]).rem_euclid(period)
3284                    }
3285                    _ => value.clamp(domain[0], domain[1]),
3286                };
3287                let u = wrap(point.x, domain_u, period[0]);
3288                let v = wrap(point.y, domain_v, period[1]);
3289                let Ok(position) = face.surface.evaluate(u, v) else {
3290                    report.unreadable.push(format!(
3291                        "face {} loop {}: crossing at uv ({u:.6}, {v:.6}) is off the surface",
3292                        face.id, loop_record.id
3293                    ));
3294                    continue;
3295                };
3296                // A crossing AT one of the loop's own vertices is the gap
3297                // between the two pcurves that meet there, not a bowtie. Two
3298                // trims arriving at one vertex are fitted independently, so
3299                // each ends a fit-accuracy away from it and near the vertex
3300                // they overlap; `assembler_weld` is the kernel's own band for
3301                // "the same vertex reached by two independent fits".
3302                if corners
3303                    .iter()
3304                    .any(|corner| corner.sub(position).length() <= vertex_band)
3305                {
3306                    continue;
3307                }
3308                let coedge = |index: usize| &loop_record.coedges[index];
3309                report.crossings.push(LoopCrossing {
3310                    face: face.id,
3311                    face_name: face.name.clone(),
3312                    loop_id: loop_record.id,
3313                    coedge_a: coedge(first).id,
3314                    coedge_b: coedge(second).id,
3315                    edge_a: coedge(first).edge_id,
3316                    edge_b: coedge(second).edge_id,
3317                    uv: [u, v],
3318                    point: position,
3319                });
3320            }
3321        }
3322    }
3323    report
3324}
3325
3326// ---------------------------------------------------------------------------
3327// Shell vector area — the closure a trim owes its own edge
3328// ---------------------------------------------------------------------------
3329
3330/// Gauss–Legendre accuracy target per span, RELATIVE to that span's own
3331/// magnitude `∫ |r − R| |dr|`. A span is bisected until the two halves agree
3332/// with the whole to this fraction of what the span can contribute at all.
3333///
3334/// Scale-free by construction, so a 3000 mm part and a 3 mm one are held to
3335/// the same number of digits. 1e-13 is about a hundred times the f64 noise of
3336/// an eight-station rule (each station is one rounding of a product of two
3337/// evaluated vectors), so the recursion terminates on geometry rather than on
3338/// arithmetic, and two orders below it is what the check needs: the whole
3339/// point is a residual read at 1e-9 out of face terms worth 1e+2.
3340const VECTOR_AREA_QUADRATURE_RELATIVE: f64 = 1e-13;
3341
3342/// `BREP_VECTOR_AREA_QUADRATURE` overrides that target, so the convergence of
3343/// a residual can be MEASURED rather than argued: read the same shell at
3344/// 1e-13 and at 1e-15 and the difference is what the quadrature is worth.
3345/// Read once per process.
3346fn vector_area_quadrature_relative() -> f64 {
3347    static TARGET: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
3348    *TARGET.get_or_init(|| {
3349        std::env::var("BREP_VECTOR_AREA_QUADRATURE")
3350            .ok()
3351            .and_then(|value| value.parse::<f64>().ok())
3352            .filter(|value| *value > 0.0 && value.is_finite())
3353            .unwrap_or(VECTOR_AREA_QUADRATURE_RELATIVE)
3354    })
3355}
3356
3357/// Bisection depth per span. A span that has not converged by here is
3358/// accepted with its last difference counted into
3359/// [`ShellVectorArea::quadrature_error`] and into
3360/// [`ShellVectorArea::spans_at_depth_limit`], never silently.
3361///
3362/// The limit is a COST bound, and it is sound because the error it leaves is
3363/// measured rather than assumed. A span whose integrand has a kink in it — a
3364/// trim read through the C¹ boundary extension past its surface's domain —
3365/// converges only linearly, so each further level halves its error for twice
3366/// the work, and the recursion is buying precision the bar never asked for.
3367/// `BREP_VECTOR_AREA_DEPTH` overrides it so that trade can be measured.
3368fn vector_area_quadrature_depth() -> usize {
3369    static DEPTH: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
3370    *DEPTH.get_or_init(|| {
3371        std::env::var("BREP_VECTOR_AREA_DEPTH")
3372            .ok()
3373            .and_then(|value| value.parse::<usize>().ok())
3374            .filter(|value| *value > 0 && *value <= 24)
3375            .unwrap_or(VECTOR_AREA_QUADRATURE_DEPTH)
3376    })
3377}
3378
3379/// Chosen by measurement, not by taste — and the cheapest setting that keeps
3380/// the reported error bar is NOT the right one.
3381///
3382/// Read on the notched cap's `F7` (curved trims) against the depth-12 value,
3383/// and on `20_helmet_merge_stage/a` (253 faces, half its spans hitting the
3384/// limit):
3385///
3386/// ```text
3387/// depth  cap residual   its bar    off depth 12   cap ms   helmet ms
3388///   12   5.106706e-6    4.204e-10        --        334.9    17347
3389///    8   5.107557e-6    7.496e-9      8.5e-10       56.4     7356
3390///    6   5.072736e-6    2.206e-9      3.4e-8        42.9     6385
3391///    4   5.206351e-6    3.116e-7      1.0e-7        39.2       --
3392/// ```
3393///
3394/// Depth 6 is the trap. It reports a TIGHTER bar than depth 8 while sitting
3395/// 15x further from the converged value: the half-vs-whole difference
3396/// underestimates what is left when the recursion stops early on a
3397/// slowly-converging integrand, so the estimate stops being conservative
3398/// before the answer stops being right. On the helmet, whose residual is
3399/// invariant at every depth, that failure is invisible — cost alone would
3400/// have picked 6.
3401///
3402/// 8 is the shallowest depth whose reported bar still COVERS its own
3403/// deviation from converged, and it is 5.9x cheaper than 12 on the cap and
3404/// 2.4x on the helmet. The residual stays 681x its own error bar there, so it
3405/// is still a measurement by a wide margin.
3406const VECTOR_AREA_QUADRATURE_DEPTH: usize = 8;
3407
3408/// The most spans one SHELL is integrated over before the scan gives up on it.
3409///
3410/// This is a COST bound and it is the only kind that keeps the residual
3411/// honest: on reaching it the shell is reported in
3412/// [`VectorAreaReport::unreadable`] and NOT given a residual, so it falls into
3413/// the same "unjudged" the report uses for a solid with no closed shell. The
3414/// alternative — integrate the remaining spans at whatever accuracy is to
3415/// hand and publish the sum — would turn a measurement into a number with an
3416/// unknown error bar, which is exactly what [`ShellVectorArea::quadrature_error`]
3417/// exists to prevent. A scan that ran out of budget did not measure anything,
3418/// and it must not be able to look like a scan that read zero.
3419///
3420/// The value is read off the case corpus rather than chosen: see the record's
3421/// §6.3. `BREP_VECTOR_AREA_SPANS` overrides it.
3422pub(crate) const VECTOR_AREA_SPAN_BUDGET: usize = 400_000;
3423//                                        ^ read off the case corpus, not chosen.
3424// Over its 317 shells the span count runs median 168, p90 8,278, p99 301,826,
3425// max 1,842,688 — and the distribution has a GAP exactly where a budget wants
3426// to sit. The four shells above 400,000 are the two `gear-hex-bore-push` rows
3427// (1.84M spans each, the only rows whose scan broke the case gate's time
3428// budget); the next largest shell in the whole corpus is 301,826. So this
3429// number separates the bodies this scan cannot afford from every body it can,
3430// with a 33% margin below it and a 4.6x jump above it, and it is a property of
3431// the corpus rather than of anyone's preference.
3432
3433fn vector_area_span_budget() -> usize {
3434    static BUDGET: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
3435    *BUDGET.get_or_init(|| {
3436        std::env::var("BREP_VECTOR_AREA_SPANS")
3437            .ok()
3438            .and_then(|value| value.parse::<usize>().ok())
3439            .filter(|value| *value > 0)
3440            .unwrap_or(VECTOR_AREA_SPAN_BUDGET)
3441    })
3442}
3443
3444/// The ABSOLUTE half of the acceptance, per unit of arc: a span is also
3445/// accepted once the halves agree to this times its own length.
3446///
3447/// It is what makes the cost bounded. The relative target alone is a target
3448/// on a SMOOTH integrand: a trim that runs outside its surface's domain is
3449/// read through the C¹ boundary extension, so its integrand has a kink, a
3450/// bisection gains only two orders per four levels, and the recursion bottoms
3451/// out at the depth limit on every span. Measured on the committed STEP
3452/// corpus that was not a slow scan but a stopped one — one imported solid
3453/// held the sweep for eight minutes.
3454///
3455/// 1e-11 is a ten-thousandth of [`PCURVE_REFINEMENT_TOLERANCE`], the same
3456/// per-unit-of-length currency the bar is quoted in, so the whole shell's
3457/// quadrature error is bounded by `bar / 5000` (a shell's boundary is twice
3458/// its edge length) whatever its geometry does — and the bound is on the
3459/// REPORTED estimate too, since every accepted difference is summed into it.
3460const VECTOR_AREA_QUADRATURE_FLOOR: f64 = 1e-11;
3461
3462/// `BREP_VECTOR_AREA_FLOOR` overrides [`VECTOR_AREA_QUADRATURE_FLOOR`], so the
3463/// cost the floor buys can be measured against the error it leaves rather
3464/// than argued.
3465fn vector_area_quadrature_floor() -> f64 {
3466    static FLOOR: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
3467    *FLOOR.get_or_init(|| {
3468        std::env::var("BREP_VECTOR_AREA_FLOOR")
3469            .ok()
3470            .and_then(|value| value.parse::<f64>().ok())
3471            .filter(|value| *value > 0.0 && value.is_finite())
3472            .unwrap_or(VECTOR_AREA_QUADRATURE_FLOOR)
3473    })
3474}
3475
3476/// One face's vector area `∫ n dA`, as its TRIMS claim it and as its EDGES
3477/// claim it, and the difference between the two.
3478///
3479/// Both are the same boundary integral `(1/2) ∮ (r − R) × dr` about the
3480/// shell's own reference `R` (see [`ShellVectorArea::reference`]), over the
3481/// same loops in the same order; only the curve differs. The `trim` walks the
3482/// pcurve mapped through this face's surface — the boundary the mass
3483/// integrals actually integrate over — and the `edge` walks the 3D edge curve
3484/// the coedge names, which is the SAME curve the face across that edge walks.
3485#[derive(Clone, Debug, Serialize)]
3486pub struct FaceVectorArea {
3487    pub shell: u64,
3488    pub face: u64,
3489    pub face_name: Option<String>,
3490    /// `(1/2) ∮ (r − R) × dr` over the face's TRIMS, each coedge sampled in
3491    /// its own parameter direction (a pcurve is parameterized in its coedge's
3492    /// traversal direction, so the loop is walked by sampling each pcurve
3493    /// forwards, never by reversing a reverse coedge's sweep).
3494    pub trim: Vec3,
3495    /// The same integral over the 3D EDGE curves, each edge traversed in its
3496    /// coedge's direction — forwards over `[t0, t1]` for a forward coedge,
3497    /// backwards for a reverse one.
3498    pub edge: Vec3,
3499    /// `trim − edge`: what THIS face's trims add to the shell's residual.
3500    /// Every edge term cancels across the shell (each edge is walked once
3501    /// each way), so the shell's residual is exactly the sum of these.
3502    pub departure: Vec3,
3503    /// The face's own boundary arc length, taken on the edge curves.
3504    pub boundary_length: f64,
3505    /// `PCURVE_REFINEMENT_TOLERANCE * boundary_length` — what this face's
3506    /// departure can be while every one of its trims lies within the fit
3507    /// floor of the edge it claims. See [`ShellVectorArea::bar`] for the
3508    /// derivation; this is the same bound applied to one loop.
3509    ///
3510    /// **It does not cover a face whose `closure_gap` is over the floor.**
3511    /// The derivation takes one integration by parts over a CLOSED loop; a
3512    /// loop that does not close carries a boundary term the bound never
3513    /// modelled, and the sliver its chords span is real area that this face's
3514    /// trims genuinely enclose. Read `closure_gap` first.
3515    pub bar: f64,
3516    /// How far this face's own trims fail to MEET each other: the summed 3D
3517    /// distance from where one coedge's trim ends to where the next one
3518    /// starts, over this face's loops, including the chord home.
3519    ///
3520    /// A different defect from the departure, and the one that is easy to
3521    /// misread as it. Two trims arriving at one vertex are fitted
3522    /// independently, each refined to `PCURVE_REFINEMENT_TOLERANCE`, so they
3523    /// can miss each other by twice it — about 2e-7 — and anything at that
3524    /// scale is the fit floor rather than a defect. Past it the loop is not
3525    /// closed in the record, and then this face's `departure` is partly the
3526    /// area of the sliver its closing chords span rather than any trim lying
3527    /// off an edge.
3528    ///
3529    /// Reported, not verdicted: a bar on it would be a new tolerance, and the
3530    /// slice that owns a loop's closure is the one that opens it. On the
3531    /// notched cap (2026-09-16) the top cap `E5:S4:PROFILE:L2_END` carries a
3532    /// 4.095e-7 net step in uv while every other loop on that shell closes to
3533    /// 1e-12 or better, and it is the fillet that opens it — the same
3534    /// document's pre-fillet body closes exactly.
3535    pub closure_gap: f64,
3536}
3537
3538impl FaceVectorArea {
3539    /// `|departure|`.
3540    pub fn residual(&self) -> f64 {
3541        self.departure.length()
3542    }
3543
3544    /// Does this face's trim lie further from its own edges than a fit at the
3545    /// refinement floor can account for?
3546    pub fn is_over_bar(&self) -> bool {
3547        self.residual() > self.bar
3548    }
3549}
3550
3551/// One shell's vector-area closure: [`shell_vector_areas`]' per-shell verdict.
3552#[derive(Clone, Debug, Serialize)]
3553pub struct ShellVectorArea {
3554    pub shell: u64,
3555    pub faces: usize,
3556    /// Every non-degenerate edge this shell's coedges use is used EXACTLY
3557    /// TWICE by them. `∫ n dA = 0` is a statement about a closed shell only,
3558    /// so an open one is measured and reported but never flagged: its
3559    /// residual is its open boundary's own vector area, which is a fact about
3560    /// the sheet rather than a defect.
3561    pub closed: bool,
3562    /// The point every arm is taken from — the centre of the bounding box of
3563    /// the vertices this shell's edges use. The residual is independent of it
3564    /// to `|R| * closure_gap` (below), and a local reference is what keeps a
3565    /// far-translated part's face terms from dwarfing the number being read,
3566    /// the same reason `mass_properties::shell_volume_reference` exists.
3567    pub reference: Vec3,
3568    /// `Σ_faces (1/2) ∮ (r − R) × dr` over the TRIMS. **Exactly zero on a
3569    /// sound closed shell**, and otherwise the sum of every face's departure
3570    /// from the edges it shares.
3571    pub trim_residual: Vec3,
3572    /// The same sum over the EDGE curves. Every edge's integral is computed
3573    /// ONCE and added with each coedge's sign, so on a shell where every edge
3574    /// is used twice with opposite senses the curve terms cancel to rounding
3575    /// and what is left is the loops' closing chords alone. It is therefore a
3576    /// BOOKKEEPING check rather than an accuracy one: it moves off zero when
3577    /// the shell is open, when an edge is used twice the SAME way, or when
3578    /// the edge curves do not meet at the vertices they share.
3579    pub edge_residual: Vec3,
3580    /// Total arc length of the shell's non-degenerate edges, each counted
3581    /// ONCE.
3582    pub edge_length: f64,
3583    /// Total arc length walked by the faces' loops — `2 * edge_length` on a
3584    /// closed shell.
3585    pub boundary_length: f64,
3586    /// The comparand: `PCURVE_REFINEMENT_TOLERANCE * edge_length`. See the
3587    /// section docs for the derivation and for why the strict bound is twice
3588    /// this.
3589    pub bar: f64,
3590    /// The summed |half-vs-whole| difference at every accepted span — an
3591    /// OVER-estimate of this shell's quadrature error, and the error bar the
3592    /// residual is read against. A residual is a measurement only while it is
3593    /// far above this.
3594    pub quadrature_error: f64,
3595    /// Total length of the chords that close the TRIM loops — how far the
3596    /// sampled trims fail to meet each other at the vertices they share, over
3597    /// the whole shell. Two trims arriving at one vertex are fitted
3598    /// independently, so a few 1e-9 per vertex is ordinary; the chords are
3599    /// integrated (they are part of the region the mass integrator's own trim
3600    /// polygon bounds), so this is reported as a fact about the record rather
3601    /// than as an error term.
3602    pub trim_closure_gap: f64,
3603    /// The same for the EDGE loops: how far consecutive edge curves fail to
3604    /// meet at their shared vertex. `validate()` checks each curve against
3605    /// its vertex (`IssueKind::CurveVertexGap`); this is what the gaps are
3606    /// worth to the shell's closure.
3607    pub edge_closure_gap: f64,
3608    /// Spans integrated: the pcurve's own knot spans, cut at the surface's
3609    /// knot levels, bisected to convergence, plus the edge curves' own. The
3610    /// cost of the whole check is this times one eight-station rule.
3611    pub spans: usize,
3612    /// How many of those spans were accepted at the DEPTH LIMIT rather than
3613    /// because they converged. Their last half-vs-whole difference is in
3614    /// `quadrature_error` like any other, so the error bar stays honest, but
3615    /// a shell with many of these is paying the full recursion on an
3616    /// integrand the bisection cannot resolve — a kink rather than a curve —
3617    /// and the cost is the symptom.
3618    pub spans_at_depth_limit: usize,
3619    /// The scan ABANDONED this shell on [`VECTOR_AREA_SPAN_BUDGET`] with
3620    /// whole faces still unread.
3621    ///
3622    /// Its `trim_residual` is then a PARTIAL SUM over the faces that were
3623    /// reached, and a partial sum of a boundary integral is not a smaller
3624    /// version of the answer — it is the vector area of an arbitrary subset
3625    /// of the shell, which on a 1004-face gear reads 6.08e+3 where the whole
3626    /// shell reads 1.16e-5. [`Self::residual`] returns `NaN` for such a shell
3627    /// so that number cannot be read as either a residual or an open
3628    /// boundary's area.
3629    pub gave_up: bool,
3630    /// Per face, in record order.
3631    pub face_areas: Vec<FaceVectorArea>,
3632}
3633
3634/// What a shell's closure scan came back with — a measurement, or nothing.
3635///
3636/// The reason this is a type rather than a float: `NaN` is a value, and a value
3637/// gets folded into a comparison. [`ShellVectorArea::is_over_bar`] is
3638/// `residual() > bar`, and `NaN > bar` is FALSE, so a shell the scan gave up on
3639/// reads exactly like a shell that closed — the worst possible answer, since the
3640/// scan gave up on the bodies whose geometry is hardest. A caller that matches on
3641/// this cannot make that mistake without writing the arm.
3642#[derive(Clone, Copy, Debug, Serialize)]
3643pub enum ClosureReading {
3644    /// The trims were integrated to the end. `at_depth_limit` spans were
3645    /// accepted at the bisection's depth limit rather than because they
3646    /// converged; their last difference is inside `quadrature_error` either way.
3647    Measured {
3648        residual: f64,
3649        bar: f64,
3650        spans: usize,
3651        at_depth_limit: usize,
3652    },
3653    /// No residual exists for this shell: the scan stopped before it had read
3654    /// every face, and a partial sum of a boundary integral is not a smaller
3655    /// version of the answer.
3656    Unmeasured {
3657        reason: &'static str,
3658        spans: usize,
3659        faces_read: usize,
3660        faces: usize,
3661    },
3662    /// An OPEN shell is not judged by this check at all: `Σ ∫ n dA = 0` is a
3663    /// statement about a closed one, and an open shell's residual is its own
3664    /// boundary's vector area, which is a fact about the sheet.
3665    Open { residual: f64, faces: usize },
3666}
3667
3668impl ShellVectorArea {
3669    /// The typed outcome — what this shell's scan actually came back with.
3670    pub fn reading(&self) -> ClosureReading {
3671        if self.gave_up {
3672            return ClosureReading::Unmeasured {
3673                reason: "the scan reached its span budget with faces unread",
3674                spans: self.spans,
3675                faces_read: self.face_areas.len(),
3676                faces: self.faces,
3677            };
3678        }
3679        if !self.closed {
3680            return ClosureReading::Open {
3681                residual: self.trim_residual.length(),
3682                faces: self.faces,
3683            };
3684        }
3685        ClosureReading::Measured {
3686            residual: self.trim_residual.length(),
3687            bar: self.bar,
3688            spans: self.spans,
3689            at_depth_limit: self.spans_at_depth_limit,
3690        }
3691    }
3692
3693    /// `|trim_residual|`, and **`NaN` when the scan gave up on this shell**:
3694    /// see [`Self::gave_up`]. A partial sum is not a residual. Prefer
3695    /// [`Self::reading`], which cannot be compared by accident.
3696    pub fn residual(&self) -> f64 {
3697        if self.gave_up {
3698            return f64::NAN;
3699        }
3700        self.trim_residual.length()
3701    }
3702
3703    /// The predicate: a CLOSED shell whose trims enclose a vector area no fit
3704    /// at the refinement floor can account for. **False for a shell the scan
3705    /// did not measure** — ask [`Self::reading`] to tell those apart.
3706    pub fn is_over_bar(&self) -> bool {
3707        matches!(self.reading(), ClosureReading::Measured { residual, bar, .. } if residual > bar)
3708    }
3709
3710    /// The faces that own the residual, worst departure first.
3711    pub fn worst_faces(&self, count: usize) -> Vec<&FaceVectorArea> {
3712        let mut ordered: Vec<&FaceVectorArea> = self.face_areas.iter().collect();
3713        ordered.sort_by(|a, b| {
3714            b.residual()
3715                .partial_cmp(&a.residual())
3716                .unwrap_or(std::cmp::Ordering::Equal)
3717                .then_with(|| a.face.cmp(&b.face))
3718        });
3719        ordered.truncate(count);
3720        ordered
3721    }
3722
3723    /// The worst face's name, or its id when it has none.
3724    pub fn worst_face_name(&self) -> Option<String> {
3725        self.worst_faces(1).first().map(|face| {
3726            face.face_name
3727                .clone()
3728                .unwrap_or_else(|| format!("face {}", face.face))
3729        })
3730    }
3731}
3732
3733/// Outcome of [`shell_vector_areas`].
3734#[derive(Clone, Debug, Serialize)]
3735pub struct VectorAreaReport {
3736    pub shells: Vec<ShellVectorArea>,
3737    /// Shells or faces whose trims could not be read, `shell/face: reason`. A
3738    /// scan that could not read a face did not clear it, so an empty
3739    /// `shells` beside a non-empty this is not "every shell closes".
3740    pub unreadable: Vec<String>,
3741}
3742
3743impl VectorAreaReport {
3744    /// The predicate: some CLOSED shell is over its bar.
3745    pub fn is_flagged(&self) -> bool {
3746        self.shells.iter().any(ShellVectorArea::is_over_bar)
3747    }
3748
3749    /// The shells this scan did NOT measure, with what each reached. A report
3750    /// with entries here has not cleared those shells, whatever the rest read.
3751    pub fn unmeasured(&self) -> Vec<(u64, ClosureReading)> {
3752        self.shells
3753            .iter()
3754            .map(|shell| (shell.shell, shell.reading()))
3755            .filter(|(_, reading)| matches!(reading, ClosureReading::Unmeasured { .. }))
3756            .collect()
3757    }
3758
3759    /// The largest closed-shell residual, and the shell that carries it.
3760    pub fn worst_shell(&self) -> Option<&ShellVectorArea> {
3761        self.shells
3762            .iter()
3763            .filter(|shell| shell.closed)
3764            .max_by(|a, b| {
3765                a.residual()
3766                    .partial_cmp(&b.residual())
3767                    .unwrap_or(std::cmp::Ordering::Equal)
3768            })
3769    }
3770
3771    /// `|trim_residual|` of the worst CLOSED shell, and **`NaN` when this
3772    /// solid has no closed shell at all**.
3773    ///
3774    /// Not 0.0. A residual of zero is what a perfectly sound shell reads, so
3775    /// a solid whose shells are all open — one that has LOST faces, say —
3776    /// must not be able to read like one through this accessor. The whole
3777    /// point of the check is that a number nothing compares against is how
3778    /// the defects it hunts stayed hidden; handing back a sound-looking zero
3779    /// for "there was nothing here to judge" would rebuild that trap inside
3780    /// the detector. Use [`Self::has_closed_shell`] to tell the two apart
3781    /// before reading this, or read the per-shell reports, where an open
3782    /// shell's residual is its boundary's own vector area and is reported.
3783    pub fn residual(&self) -> f64 {
3784        self.worst_shell()
3785            .map(ShellVectorArea::residual)
3786            .unwrap_or(f64::NAN)
3787    }
3788
3789    /// That shell's bar, `NaN` when there is no closed shell — see
3790    /// [`Self::residual`] for why this is not 0.0.
3791    pub fn bar(&self) -> f64 {
3792        self.worst_shell().map(|shell| shell.bar).unwrap_or(f64::NAN)
3793    }
3794
3795    /// Is there a closed shell here for [`Self::residual`] to be about?
3796    ///
3797    /// `Σ ∫ n dA = 0` is a statement about a CLOSED shell, so a solid with
3798    /// none is not "sound" and not "unsound" — it is unjudged, and every
3799    /// reader of this report has to decide what to do about that rather than
3800    /// inherit a zero.
3801    pub fn has_closed_shell(&self) -> bool {
3802        self.shells.iter().any(|shell| shell.closed)
3803    }
3804
3805    /// The shells that are NOT closed, with the vector area their open
3806    /// boundary encloses — the reading that names a shell which has lost
3807    /// faces.
3808    ///
3809    /// A closed shell's every edge is used exactly twice. A shell that has
3810    /// dropped a face has that face's edges used once, so it is open here and
3811    /// its residual is what the missing boundary encloses. That is a
3812    /// different question from Euler parity, and a strictly stronger one for
3813    /// this purpose: parity notices an ODD number of faces going and is
3814    /// silent when an even number does, while this counts the uses of every
3815    /// edge and does not care how many faces left.
3816    pub fn open_shells(&self) -> impl Iterator<Item = &ShellVectorArea> {
3817        self.shells.iter().filter(|shell| !shell.closed)
3818    }
3819
3820    /// One line naming the worst shell and the faces that own its residual,
3821    /// or `None` when every closed shell is inside its bar.
3822    pub fn summary(&self) -> Option<String> {
3823        let shell = self
3824            .shells
3825            .iter()
3826            .filter(|shell| shell.is_over_bar())
3827            .max_by(|a, b| {
3828                a.residual()
3829                    .partial_cmp(&b.residual())
3830                    .unwrap_or(std::cmp::Ordering::Equal)
3831            })?;
3832        let named: Vec<String> = shell
3833            .worst_faces(3)
3834            .iter()
3835            .map(|face| {
3836                format!(
3837                    "{} {:.3e}",
3838                    face.face_name
3839                        .clone()
3840                        .unwrap_or_else(|| format!("face {}", face.face)),
3841                    face.residual()
3842                )
3843            })
3844            .collect();
3845        Some(format!(
3846            "shell {} closes to {:.3e} of vector area against a bar of {:.3e} \
3847             ({} faces, {:.4} of edge, quadrature {:.1e}); worst: {}",
3848            shell.shell,
3849            shell.residual(),
3850            shell.bar,
3851            shell.faces,
3852            shell.edge_length,
3853            shell.quadrature_error,
3854            named.join(", ")
3855        ))
3856    }
3857}
3858
3859/// `(1/2) ∮ (r − reference) × dr` along the straight chord `from → to` — the
3860/// term that closes a loop across the gap between two independently fitted
3861/// curves that both claim to reach one vertex.
3862fn chord_area(from: Vec3, to: Vec3, reference: Vec3) -> Vec3 {
3863    from.sub(reference).cross(to.sub(from)).scale(0.5)
3864}
3865
3866/// One Gauss–Legendre pass over `[t0, t1]`: that span's half of
3867/// `(1/2) ∮ (r − reference) × dr`, the magnitude `∫ |r − R| |dr|` its relative
3868/// accuracy is judged against, and the arc `∫ |dr|` its absolute floor is.
3869fn vector_area_span<F>(
3870    at: &F,
3871    t0: f64,
3872    t1: f64,
3873    reference: Vec3,
3874) -> Result<(Vec3, f64, f64), String>
3875where
3876    F: Fn(f64) -> Result<(Vec3, Vec3), String>,
3877{
3878    let half = (t1 - t0) * 0.5;
3879    let middle = (t1 + t0) * 0.5;
3880    let mut total = Vec3::new(0.0, 0.0, 0.0);
3881    let mut magnitude = 0.0;
3882    let mut arc = 0.0;
3883    for index in 0..GAUSS_X.len() {
3884        let (point, tangent) = at(middle + half * GAUSS_X[index])?;
3885        let arm = point.sub(reference);
3886        let weight = GAUSS_W[index] * half;
3887        total = total.add(arm.cross(tangent).scale(0.5 * weight));
3888        magnitude += weight.abs() * arm.length() * tangent.length();
3889        arc += weight.abs() * tangent.length();
3890    }
3891    Ok((total, magnitude, arc))
3892}
3893
3894/// [`vector_area_span`] bisected until the halves agree with the whole, or until
3895/// the disagreement is below `share` — the error this span is ALLOWED, which is
3896/// the trim's own budget spread over the leaves the depth limit permits.
3897///
3898/// Without it a span whose 3D length has collapsed cannot ever be accepted. Its
3899/// acceptance target is proportional to its own size (relative to `magnitude`,
3900/// absolute per unit of `arc`), while the disagreement between its halves and
3901/// its whole is set by the round-off in a tangent the parameterization no longer
3902/// resolves, which does not shrink with the span. Measured on
3903/// `fixture_25_cube_pierce_pole_gap_bridge`, a FOUR-face body: spans of arc
3904/// 2.0e-16 whose halves and whole differ by 1.4e-14 against a target of 2.8e-27,
3905/// bisecting to the depth limit every time — 313,900 of its 355,610 spans, 5.0 s
3906/// for a residual whose bar is 1.4e-4. A span contributing 1e-14 cannot move
3907/// that verdict, and `share` says so.
3908///
3909///
3910/// The composition `S(pcurve(t))` is not a polynomial of any degree — a
3911/// rational patch read along a rational curve — so no fixed rule integrates
3912/// it exactly and a fixed rule's error is unknown rather than small. Halving
3913/// is also what finds a surface KNOT crossing for free: the pcurve knows
3914/// nothing of the surface's knot lines, and a span that straddles one has a
3915/// kink in its integrand that the difference sees and the recursion resolves.
3916fn vector_area_adaptive<F>(
3917    at: &F,
3918    t0: f64,
3919    t1: f64,
3920    reference: Vec3,
3921    depth: usize,
3922    share: f64,
3923    error: &mut f64,
3924    spans: &mut usize,
3925    exhausted: &mut usize,
3926) -> Result<Vec3, String>
3927where
3928    F: Fn(f64) -> Result<(Vec3, Vec3), String>,
3929{
3930    let (whole, magnitude, arc) = vector_area_span(at, t0, t1, reference)?;
3931    let middle = 0.5 * (t0 + t1);
3932    let (left, _, _) = vector_area_span(at, t0, middle, reference)?;
3933    let (right, _, _) = vector_area_span(at, middle, t1, reference)?;
3934    let halves = left.add(right);
3935    let difference = halves.sub(whole).length();
3936    let target = (vector_area_quadrature_relative() * magnitude)
3937        .max(vector_area_quadrature_floor() * arc)
3938        .max(share);
3939    if depth == 0 || difference <= target {
3940        *error += difference;
3941        *spans += 2;
3942        if depth == 0 && difference > target {
3943            *exhausted += 2;
3944        }
3945        return Ok(halves);
3946    }
3947    let first =
3948        vector_area_adaptive(at, t0, middle, reference, depth - 1, share, error, spans, exhausted)?;
3949    let second =
3950        vector_area_adaptive(at, middle, t1, reference, depth - 1, share, error, spans, exhausted)?;
3951    Ok(first.add(second))
3952}
3953
3954/// The spans one curve is integrated over: its own knot breaks, clipped to
3955/// `[t0, t1]`.
3956fn vector_area_breaks(curve: &NurbsCurve, t0: f64, t1: f64) -> Result<Vec<f64>, String> {
3957    let mut breaks = vec![t0];
3958    for knot in crate::curve::interior_knots(&curve.knots, curve.degree) {
3959        if knot > t0 && knot < t1 {
3960            breaks.push(knot);
3961        }
3962    }
3963    breaks.push(t1);
3964    Ok(breaks)
3965}
3966
3967/// `(1/2) ∮ (r − reference) × dr` over one curve span, plus the arc length
3968/// and the end-to-end displacement of what was walked.
3969fn vector_area_of<F>(
3970    at: &F,
3971    breaks: &[f64],
3972    reference: Vec3,
3973    share: f64,
3974    error: &mut f64,
3975    spans: &mut usize,
3976    exhausted: &mut usize,
3977) -> Result<Vec3, String>
3978where
3979    F: Fn(f64) -> Result<(Vec3, Vec3), String>,
3980{
3981    let mut total = Vec3::new(0.0, 0.0, 0.0);
3982    let depth = vector_area_quadrature_depth();
3983    for pair in breaks.windows(2) {
3984        if pair[1] <= pair[0] {
3985            continue;
3986        }
3987        total = total.add(vector_area_adaptive(
3988            at,
3989            pair[0],
3990            pair[1],
3991            reference,
3992            depth,
3993            share,
3994            error,
3995            spans,
3996            exhausted,
3997        )?);
3998    }
3999    Ok(total)
4000}
4001
4002/// Arc length over the same spans, by the same rule — used for the bar, so it
4003/// needs the rule's accuracy and not the residual's.
4004fn vector_area_length<F>(at: &F, breaks: &[f64]) -> Result<f64, String>
4005where
4006    F: Fn(f64) -> Result<(Vec3, Vec3), String>,
4007{
4008    let mut length = 0.0;
4009    for pair in breaks.windows(2) {
4010        if pair[1] <= pair[0] {
4011            continue;
4012        }
4013        let half = (pair[1] - pair[0]) * 0.5;
4014        let middle = (pair[1] + pair[0]) * 0.5;
4015        for index in 0..GAUSS_X.len() {
4016            let (_, tangent) = at(middle + half * GAUSS_X[index])?;
4017            length += GAUSS_W[index] * half * tangent.length();
4018        }
4019    }
4020    Ok(length)
4021}
4022
4023/// Does a closed shell's boundary enclose the vector area it should — zero?
4024///
4025/// A patch's vector area is a BOUNDARY integral,
4026/// `∫_F n dA = (1/2) ∮_{∂F} r × dr`, so the whole shell's is a sum over the
4027/// faces' loops. Every edge of a closed shell is walked exactly twice, once
4028/// each way, so **`Σ_faces ∫ n dA` is exactly zero on a closed shell** — a
4029/// statement about the RECORD, with no tolerance in it, as long as the two
4030/// faces meeting at an edge walk the same curve. They do not: each walks its
4031/// OWN TRIM, the pcurve mapped through its own surface, and what is left over
4032/// is the sum of every trim's departure from the edge it claims.
4033///
4034/// That residual is the one whole-shell number that reads the trims — which
4035/// is what every mass property is actually an integral over
4036/// (`geometry/pcurve.rs`, `PCURVE_REFINEMENT_TOLERANCE`) — and nothing in the
4037/// kernel read it before 2026-09-16. `validate()` compares a pcurve to its
4038/// edge at `pcurve_consistency` (4e-3 on a small model, 2.5% of the diagonal
4039/// on a large one): three defects on the notched cap sat between 5e-7 and
4040/// 1.9e-5, four orders inside that bar, and the solid's volume matched its
4041/// closed form because two of them cancelled.
4042///
4043/// ## The bar, and where it comes from
4044///
4045/// Write a trim as its edge plus a departure, `r = e + d`. Over a CLOSED loop,
4046///
4047/// ```text
4048/// (1/2)∮ r×dr − (1/2)∮ e×de = (1/2)∮ (e×dd + d×de) + (1/2)∮ d×dd
4049///                           = ∮ d×de + (1/2)∮ d×dd
4050/// ```
4051///
4052/// (the middle step is one integration by parts: `∮ e×dd = −∮ de×d` because
4053/// the loop closes). So a face whose every trim lies within `δ` of its edge
4054/// departs by at most `δ · L`, its own boundary length, and the second-order
4055/// term is `δ²`-small. Summing over a closed shell, whose faces walk every
4056/// edge twice, the strict bound is `2δ · Σ|e|`.
4057///
4058/// `δ` is [`PCURVE_REFINEMENT_TOLERANCE`], 1e-7 — the 3D distance
4059/// `fit_pcurve_on_surface` refines a trim to, and the floor under every mass
4060/// property. The bar this module applies is the TIGHTER
4061/// `δ · Σ|e|` — half the strict bound — because the worst case it drops is a
4062/// shell where every pair of trims sits at the floor on OPPOSITE sides of
4063/// every edge all the way round. A shell between the two is named, and the
4064/// per-face `bar` says which faces put it there.
4065///
4066/// Two things the bar does not cover, both reported rather than absorbed:
4067///
4068/// * a fit that exited at [`crate::PcurveFitExit::SampleCeiling`] never
4069///   reached the floor, so a shell carrying one can be over the bar and still
4070///   be the best this kernel builds. That is a finding about the fit, not a
4071///   reason to widen the bar.
4072/// * the quadrature's own error, reported per shell as
4073///   [`ShellVectorArea::quadrature_error`]. A residual inside its own error
4074///   bar is not a measurement.
4075///
4076/// ## What it does not ask
4077///
4078/// It does not ask whether a face's trim is in the right PLACE — a trim
4079/// displaced along its own edge, or two faces' trims displaced the same way,
4080/// cancel in this sum exactly as they cancel in the volume. It is a closure
4081/// test, not a fit test: `d` enters only through `∮ d×de`. And it says
4082/// nothing about an open shell, where the residual is the boundary's own
4083/// vector area and not a defect.
4084pub fn shell_vector_areas(solid: &BrepSolid) -> VectorAreaReport {
4085    shell_vector_areas_scan(solid, vector_area_span_budget(), "raise BREP_VECTOR_AREA_SPANS to read it")
4086}
4087
4088/// [`shell_vector_areas`] with an explicit span budget instead of the
4089/// default ([`VECTOR_AREA_SPAN_BUDGET`], or `BREP_VECTOR_AREA_SPANS`). Every
4090/// quadrature target, bar and reading is the same; only where the scan gives
4091/// up moves. For a caller that must judge a body the default cannot afford
4092/// and has a stated bound of its own (the inherited-trim refit's gate reads
4093/// at most two default budgets); the public entry and its default are
4094/// unchanged.
4095pub(crate) fn shell_vector_areas_with_budget(solid: &BrepSolid, span_budget: usize) -> VectorAreaReport {
4096    shell_vector_areas_scan(solid, span_budget, "raise the caller's span budget to read it")
4097}
4098
4099/// The scan behind both entries; `advice` ends an over-budget row and says
4100/// which budget the reader can raise (the environment's for the public
4101/// entry, the caller's for an explicit one).
4102fn shell_vector_areas_scan(solid: &BrepSolid, span_budget: usize, advice: &str) -> VectorAreaReport {
4103    let mut report = VectorAreaReport {
4104        shells: Vec::new(),
4105        unreadable: Vec::new(),
4106    };
4107    let edges: HashMap<u64, &crate::topology::EdgeRecord> =
4108        solid.edges.iter().map(|edge| (edge.id, edge)).collect();
4109    let vertices: HashMap<u64, Vec3> = solid
4110        .vertices
4111        .iter()
4112        .map(|vertex| (vertex.id, vertex.point))
4113        .collect();
4114    for shell in &solid.shells {
4115        // Which edges this shell uses, and how often — the closedness test,
4116        // and the edge set the bar's length is taken over.
4117        let mut uses: HashMap<u64, usize> = HashMap::default();
4118        for coedge in shell
4119            .faces
4120            .iter()
4121            .flat_map(|face| &face.loops)
4122            .flat_map(|record| &record.coedges)
4123        {
4124            *uses.entry(coedge.edge_id).or_insert(0) += 1;
4125        }
4126        let closed = uses.iter().all(|(id, count)| {
4127            *count == 2
4128                || edges
4129                    .get(id)
4130                    .map(|edge| edge.degenerate)
4131                    .unwrap_or(false)
4132        });
4133
4134        // The reference: the centre of the bounding box of the vertices this
4135        // shell's edges use. Far-from-origin arms cost digits in a sum read
4136        // at 1e-9, and the kernel's volume integrator anchors locally for the
4137        // same reason.
4138        let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
4139        let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
4140        let mut seen = false;
4141        for point in uses
4142            .keys()
4143            .filter_map(|id| edges.get(id))
4144            .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
4145            .filter_map(|id| vertices.get(&id))
4146        {
4147            if !point.x.is_finite() || !point.y.is_finite() || !point.z.is_finite() {
4148                continue;
4149            }
4150            low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
4151            high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
4152            seen = true;
4153        }
4154        let reference = if seen {
4155            low.add(high).scale(0.5)
4156        } else {
4157            Vec3::new(0.0, 0.0, 0.0)
4158        };
4159
4160        // Each edge's own integral and length, computed ONCE and added with
4161        // the coedge's sign: the two uses of an edge then cancel to rounding
4162        // rather than to the quadrature, so `edge_residual` reads the
4163        // BOOKKEEPING (is every edge used twice, and once each way) and the
4164        // trims carry the geometry.
4165        // Per edge: its integral over its own ordered span, its arc length,
4166        // and the two end points — computed once and read by both coedges, so
4167        // the two uses cancel to rounding rather than to the quadrature.
4168        // What this shell's reading is ALLOWED to be wrong by, spread over every
4169        // leaf the depth limit permits — the same bound the per-arc floor above
4170        // publishes (`VECTOR_AREA_QUADRATURE_FLOOR` per unit of boundary), stated
4171        // once for the shell instead of per span.
4172        //
4173        // A span's own size cannot carry that bound. Where a trim's
4174        // parameterization has collapsed, the span's magnitude and arc vanish
4175        // while the disagreement between its halves and its whole does not: it is
4176        // round-off in a tangent that is no longer resolved. Then the target is
4177        // unreachable and the span bisects to the depth limit, every time.
4178        // Measured on `fixture_25_cube_pierce_pole_gap_bridge`, a FOUR-face body:
4179        // 313,900 of its 355,610 spans, 5.0 s, for a contribution of 1e-14 against
4180        // a bar of 1.4e-4. A reading may stop once it is inside what the verdict
4181        // allows, and this says where that is.
4182        let mut shell_edge_length = 0.0f64;
4183        let mut top_spans = 0usize;
4184        {
4185            let mut measured: HashSet<u64> = HashSet::default();
4186            for face in &shell.faces {
4187                for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
4188                    if let Ok(domain) = coedge.pcurve.domain() {
4189                        if let Ok(breaks) = vector_area_breaks(&coedge.pcurve, domain[0], domain[1]) {
4190                            top_spans += breaks.len().saturating_sub(1);
4191                        }
4192                    }
4193                    let Some(record) = edges.get(&coedge.edge_id) else { continue };
4194                    if !measured.insert(record.id) {
4195                        continue;
4196                    }
4197                    let (lo, hi) = if record.t0 <= record.t1 { (record.t0, record.t1) } else { (record.t1, record.t0) };
4198                    let on_edge = |parameter: f64| -> Result<(Vec3, Vec3), String> { record.curve.deriv1(parameter) };
4199                    if let Ok(breaks) = vector_area_breaks(&record.curve, lo, hi) {
4200                        top_spans += breaks.len().saturating_sub(1);
4201                        if let Ok(length) = vector_area_length(&on_edge, &breaks) {
4202                            shell_edge_length += length;
4203                        }
4204                    }
4205                }
4206            }
4207        }
4208        let share = if top_spans == 0 {
4209            0.0
4210        } else {
4211            vector_area_quadrature_floor() * 2.0 * shell_edge_length
4212                / (top_spans as f64 * (1u64 << vector_area_quadrature_depth()) as f64)
4213        };
4214        let mut edge_terms: HashMap<u64, (Vec3, f64, Vec3, Vec3)> = HashMap::default();
4215        let mut quadrature_error = 0.0;
4216        let mut spans = 0usize;
4217        let mut exhausted = 0usize;
4218        let mut unreadable_here = 0usize;
4219        let mut over_budget = false;
4220        let mut face_areas: Vec<FaceVectorArea> = Vec::with_capacity(shell.faces.len());
4221        let mut trim_residual = Vec3::new(0.0, 0.0, 0.0);
4222        let mut edge_residual = Vec3::new(0.0, 0.0, 0.0);
4223        let mut trim_gap = 0.0;
4224        let mut edge_gap = 0.0;
4225        let mut boundary_length = 0.0;
4226        for face in &shell.faces {
4227            // The surface's own knot levels, and its parameter periods: a
4228            // trim span that crosses one is cut there. The pcurve knows
4229            // nothing of the surface's knots, and a Gauss rule read straight
4230            // across one is the same 0.3% error `winding_loops_integral`
4231            // records — five orders above what this check reads. Bisection
4232            // alone cannot fix it either: a kink converges polynomially, so
4233            // the recursion bottoms out at its depth limit instead.
4234            let levels = surface_breaks(&face.surface)
4235                .ok()
4236                .zip(face.surface.domain_u().ok())
4237                .zip(face.surface.domain_v().ok())
4238                .map(|(((u_breaks, v_breaks), [u0, u1]), [v0, v1])| {
4239                    (u_breaks, v_breaks, u1 - u0, v1 - v0)
4240                });
4241            let mut trim = Vec3::new(0.0, 0.0, 0.0);
4242            let mut edge = Vec3::new(0.0, 0.0, 0.0);
4243            let mut face_length = 0.0;
4244            let mut face_gap = 0.0;
4245            let mut failed = false;
4246            for loop_record in &face.loops {
4247                // A loop is walked as a CLOSED curve: each coedge's own
4248                // integral, plus the straight chord from where the previous
4249                // one ended to where this one starts, and the chord home at
4250                // the end. Those chords are not bookkeeping — two trims
4251                // arriving at one vertex are fitted independently and land a
4252                // fit-accuracy apart, so the sampled loop has small jumps in
4253                // it, and `(1/2) ∮ r × dr` over an OPEN curve is not a vector
4254                // area at all: it moves with the reference by
4255                // `(1/2) R × Σ jumps`. Measured on the notched cap the jumps
4256                // total 4.1e-7 and that reference dependence is 2e-6, half
4257                // the residual being read. Closed with its chords the loop
4258                // has `∮ dr = 0` exactly and the number is a property of the
4259                // face.
4260                let mut trim_previous: Option<Vec3> = None;
4261                let mut trim_first: Option<Vec3> = None;
4262                let mut edge_previous: Option<Vec3> = None;
4263                let mut edge_first: Option<Vec3> = None;
4264                for coedge in &loop_record.coedges {
4265                    // The trim: the pcurve through this face's own surface.
4266                    // `deriv1_extended` and not `deriv1`, because a sample a
4267                    // rounding past a periodic domain must WRAP; the plain
4268                    // evaluator clamps, which would flatten the curve onto
4269                    // the domain boundary and manufacture a residual.
4270                    let on_trim = |parameter: f64| -> Result<(Vec3, Vec3), String> {
4271                        let (uv, duv) = coedge.pcurve.deriv1(parameter)?;
4272                        let (point, su, sv) = face.surface.deriv1_extended(uv.x, uv.y)?;
4273                        Ok((point, su.scale(duv.x).add(sv.scale(duv.y))))
4274                    };
4275                    let mut note = |message: String| {
4276                        report
4277                            .unreadable
4278                            .push(format!("shell {} face {}: {message}", shell.id, face.id));
4279                    };
4280                    let trim_span = coedge.pcurve.domain().and_then(|domain| {
4281                        let mut breaks =
4282                            vector_area_breaks(&coedge.pcurve, domain[0], domain[1])?;
4283                        if let Some((u_breaks, v_breaks, u_period, v_period)) = levels.as_ref() {
4284                            breaks = split_at_surface_breaks(
4285                                &coedge.pcurve,
4286                                breaks,
4287                                u_breaks,
4288                                *u_period,
4289                                true,
4290                                false,
4291                            )?;
4292                            breaks = split_at_surface_breaks(
4293                                &coedge.pcurve,
4294                                breaks,
4295                                v_breaks,
4296                                *v_period,
4297                                false,
4298                                false,
4299                            )?;
4300                        }
4301                        let value = vector_area_of(
4302                            &on_trim,
4303                            &breaks,
4304                            reference,
4305                            share,
4306                            &mut quadrature_error,
4307                            &mut spans,
4308                            &mut exhausted,
4309                        )?;
4310                        Ok((value, on_trim(domain[0])?.0, on_trim(domain[1])?.0))
4311                    });
4312                    match trim_span {
4313                        Ok((value, start, end)) => {
4314                            trim = trim.add(value);
4315                            if let Some(previous) = trim_previous {
4316                                trim = trim.add(chord_area(previous, start, reference));
4317                                trim_gap += start.sub(previous).length();
4318                                face_gap += start.sub(previous).length();
4319                            } else {
4320                                trim_first = Some(start);
4321                            }
4322                            trim_previous = Some(end);
4323                        }
4324                        Err(message) => {
4325                            note(format!("coedge {}: {message}", coedge.id));
4326                            unreadable_here += 1;
4327                            failed = true;
4328                        }
4329                    }
4330
4331                    // The edge: the 3D curve, in the coedge's direction.
4332                    let Some(record) = edges.get(&coedge.edge_id) else {
4333                        note(format!(
4334                            "coedge {} names edge {}, which is not in the record",
4335                            coedge.id, coedge.edge_id
4336                        ));
4337                        unreadable_here += 1;
4338                        failed = true;
4339                        continue;
4340                    };
4341                    if !edge_terms.contains_key(&record.id) {
4342                        let on_edge = |parameter: f64| -> Result<(Vec3, Vec3), String> {
4343                            record.curve.deriv1(parameter)
4344                        };
4345                        let (lo, hi) = if record.t0 <= record.t1 {
4346                            (record.t0, record.t1)
4347                        } else {
4348                            (record.t1, record.t0)
4349                        };
4350                        let term = vector_area_breaks(&record.curve, lo, hi).and_then(|breaks| {
4351                            let value = vector_area_of(
4352                                &on_edge,
4353                                &breaks,
4354                                reference,
4355                                share,
4356                                &mut quadrature_error,
4357                                &mut spans,
4358                                &mut exhausted,
4359                            )?;
4360                            let length = vector_area_length(&on_edge, &breaks)?;
4361                            Ok((value, length, on_edge(lo)?.0, on_edge(hi)?.0))
4362                        });
4363                        match term {
4364                            Ok(term) => {
4365                                edge_terms.insert(record.id, term);
4366                            }
4367                            Err(message) => {
4368                                note(format!("edge {}: {message}", record.id));
4369                                unreadable_here += 1;
4370                                failed = true;
4371                                continue;
4372                            }
4373                        }
4374                    }
4375                    let Some((value, length, low, high)) = edge_terms.get(&record.id).copied()
4376                    else {
4377                        continue;
4378                    };
4379                    // `t0 > t1` on the record is the same span walked the
4380                    // other way; the integral above was taken over the
4381                    // ordered span, so the record's own order is one sign and
4382                    // the coedge's `forward` the other.
4383                    let ordered = if record.t0 <= record.t1 { 1.0 } else { -1.0 };
4384                    let sense = if coedge.forward { 1.0 } else { -1.0 };
4385                    let signed = ordered * sense;
4386                    edge = edge.add(value.scale(signed));
4387                    let (start, end) = if signed > 0.0 { (low, high) } else { (high, low) };
4388                    if let Some(previous) = edge_previous {
4389                        edge = edge.add(chord_area(previous, start, reference));
4390                        edge_gap += start.sub(previous).length();
4391                    } else {
4392                        edge_first = Some(start);
4393                    }
4394                    edge_previous = Some(end);
4395                    face_length += length;
4396                }
4397                if let (Some(previous), Some(first)) = (trim_previous, trim_first) {
4398                    trim = trim.add(chord_area(previous, first, reference));
4399                    trim_gap += first.sub(previous).length();
4400                    face_gap += first.sub(previous).length();
4401                }
4402                if let (Some(previous), Some(first)) = (edge_previous, edge_first) {
4403                    edge = edge.add(chord_area(previous, first, reference));
4404                    edge_gap += first.sub(previous).length();
4405                }
4406            }
4407            if failed {
4408                continue;
4409            }
4410            // The budget, checked per face so a shell gives up on a face
4411            // boundary rather than part-way through a loop. `over_budget`
4412            // makes the shell unreadable below; it is never a residual.
4413            if spans > span_budget {
4414                over_budget = true;
4415                report.unreadable.push(format!(
4416                    "shell {}: gave up after {} spans on {} of {} faces (budget {}); UNJUDGED rather than clean — {advice}",
4417                    shell.id,
4418                    spans,
4419                    face_areas.len() + 1,
4420                    shell.faces.len(),
4421                    span_budget
4422                ));
4423                break;
4424            }
4425            boundary_length += face_length;
4426            trim_residual = trim_residual.add(trim);
4427            edge_residual = edge_residual.add(edge);
4428            face_areas.push(FaceVectorArea {
4429                shell: shell.id,
4430                face: face.id,
4431                face_name: face.name.clone(),
4432                trim,
4433                edge,
4434                departure: trim.sub(edge),
4435                boundary_length: face_length,
4436                bar: crate::pcurve::PCURVE_REFINEMENT_TOLERANCE * face_length,
4437                closure_gap: face_gap,
4438            });
4439        }
4440        let edge_length: f64 = edge_terms.values().map(|(_, length, _, _)| *length).sum();
4441        report.shells.push(ShellVectorArea {
4442            shell: shell.id,
4443            faces: shell.faces.len(),
4444            // A face that could not be read leaves its share of the residual
4445            // out of the sum, so the shell is not claimed to close — and a
4446            // shell abandoned on the span budget is in exactly that position,
4447            // with whole faces missing from the sum.
4448            closed: closed && unreadable_here == 0 && !over_budget,
4449            reference,
4450            trim_residual,
4451            edge_residual,
4452            edge_length,
4453            boundary_length,
4454            bar: crate::pcurve::PCURVE_REFINEMENT_TOLERANCE * edge_length,
4455            quadrature_error,
4456            spans,
4457            spans_at_depth_limit: exhausted,
4458            trim_closure_gap: trim_gap,
4459            edge_closure_gap: edge_gap,
4460            gave_up: over_budget,
4461            face_areas,
4462        });
4463    }
4464    report
4465}