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