Skip to main content

brep_kernel/brep/
soundness.rs

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