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