Skip to main content

brep_kernel/healing/
accept.rs

1//! Is a constructed result SOUND enough to return — and if not, can it be
2//! repaired rather than refused?
3//!
4//! `validate()` is an incidence test and says nothing about a body passing
5//! through itself. [`crate::solid_self_intersections`] does, in two forms: two
6//! faces CROSSING, and one face FOLDING through itself. Neither changes the
7//! volume in a way a closed-form oracle can see — a shape can enclose exactly
8//! the right amount of space while passing through itself — so a lane that
9//! returns one reports success for a different result. That is the failure
10//! mode this module exists to stop.
11//!
12//! ## Repair first, refuse second
13//!
14//! A self-intersection is not automatically a dead end. Where two faces of one
15//! body cross along a curve and the material beyond that curve is a sliver,
16//! the answer is to SPLIT both faces along their carriers' exact intersection
17//! and drop the pieces that should not be there —
18//! [`resolve_face_crossing`]. Only when that cannot be bounded does the lane
19//! refuse.
20//!
21//! The drop is closed under one rule — a face left holding a boundary no other
22//! face answers for bordered the dropped material — and that rule has two
23//! answers. DROPPING the face is right when the unanswered boundary is the
24//! whole of what it contributed: a tail beside a tail. RE-TRIMMING it — taking
25//! the unanswered coedges off and re-linking what is left into closed loops —
26//! is right when the body keeps the face and only part of its boundary went,
27//! which is the only way a BREAKOUT closes. A bore carried out through a side
28//! wall leaves the mouth flat holding the stretch of a corner edge that now
29//! runs INSIDE the bore and the arc of its own mouth ring that now runs
30//! OUTSIDE the body; dropping the flat is absurd, and taking those two coedges
31//! off it merges its mouth loop into its outer loop, which is the topology a
32//! breakout has. The enumeration runs under the dropping closure first and
33//! reaches the re-trimming one only where that resolved nothing at all, so the
34//! second can turn a refusal into a repair and can never change a repair this
35//! module already made.
36//!
37//! A FOLD is different and is refused outright here. A face folded through
38//! itself is a TRIM that was never constructed: the answer is to carve the
39//! folded part of the trim away at the point the carrier is built
40//! (`offset/carve.rs`), not to repair the finished body. Refusing it by name
41//! is what sends the caller to the lane that can fix it.
42//!
43//! ## A hole that left its face
44//!
45//! The scan reads the body as a SURFACE: it convicts two faces that cross and
46//! one face that folds. Neither describes a face whose HOLE loop has wandered
47//! off the face it is a hole in — the trim is nonsense but nothing crosses,
48//! because the wall the hole opens into has gone with it. A direct edit that
49//! carries a bore clean off the body builds exactly that: `validate()` is an
50//! incidence test and passes, the scan finds no pair to convict, and the
51//! divergence-theorem volume of a cylinder is blind to where its axis sits, so
52//! the wrong body measures right to the last bit. [`refuse_holes_leaving_their_faces`]
53//! is the floor for it, and it runs LAST — after the repair, on whatever body
54//! the repair left — so a crossing the repair can take is still taken.
55//!
56//! ## And a hole that left its face ONTO A NEIGHBOUR
57//!
58//! That floor convicts two different things, and only one of them has no right
59//! answer. A bore dragged CLEAN OFF the body has none: the carrier misses the
60//! solid, and there is nothing to cut at any extent. A bore dragged so that its
61//! exit mouth crosses the edge between the face it opened through and the face
62//! next door has one, and it is the body the crossing repair already builds for
63//! a bore carried out through a side wall — the moved carrier and the
64//! neighbour split along their exact section, the overshoot dropped, and the
65//! faces left holding an unanswered boundary RE-TRIMMED, which merges the mouth
66//! into the outer loops of the two faces it now opens through.
67//!
68//! [`accept_or_break_out`] tries that before refusing, and the two cases are
69//! told apart by GEOMETRY rather than by a threshold: the convicted hole loop
70//! is traced in its own face's parameter plane and asked which of that face's
71//! outer coedges it crosses ([`breakout_pair`]). A hole that has broken out
72//! onto a neighbour crosses one, and the face across that edge is the one to
73//! re-trim against. A hole that has left the body altogether crosses none, and
74//! there is no second face for the repair to be about — so the runaway refuses
75//! before a single face is split, and it refuses with the floor's own words
76//! plus the repair's reason for declining.
77//!
78//! The extent is DERIVED and not invented, which is what makes this the same
79//! repair rather than a new operation with a new input. The tool is the moved
80//! carrier trimmed against the body, and where the carrier meets the body is
81//! what the imprint computes. Measured on the 2026-09-21 exit-hole report: the
82//! body this builds removes 4195.389267765 from the filled wedge, against
83//! 4195.389268713 for the same wedge with the INFINITE moved carrier
84//! subtracted through the boolean and 4195.389267095 for a slice integration
85//! that never enters the kernel — three roads, 1.6e-9 apart.
86//!
87//! ## The order
88//!
89//! Three separate slices built [`accept_sound`]'s sequence on 2026-09-21 and
90//! none of them saw the whole of it, so here it is end to end:
91//!
92//! 1. SCAN once. One tessellation, one BVH sweep; every verdict below is read
93//!    off that single reading.
94//! 2. NOT FLAGGED — [`accept_or_break_out`]: the hole-containment floor runs on
95//!    the body as built, and the body is returned when it says nothing. When it
96//!    convicts, the BREAKOUT is tried before the refusal — the same split and
97//!    re-trim step 4 runs, on a pair the floor names instead of one the scan
98//!    confirmed — and only a breakout that cannot be built refuses. This is the
99//!    ONLY path a bore carried clear of its faces reaches, because a bore that
100//!    has left the solid crosses nothing.
101//! 3. FLAGGED — the body is dumped under `BREP_HEAL_DUMP`, and a FOLD refuses
102//!    here and now: a fold is a trim that was never constructed, not material
103//!    to drop.
104//! 4. A CROSSING goes to [`resolve_face_crossing`], which returns the body
105//!    UNCHANGED when nothing is confirmed, refuses a fold and refuses more than
106//!    one crossing pair, and otherwise imprints the two carriers' section onto
107//!    both faces and enumerates every way of dropping the resulting pieces —
108//!    closed under `Drop` first, and under `Retrim` only where that resolved
109//!    nothing at all.
110//! 5. The repaired body is SCANNED AGAIN, and a repair that still
111//!    self-intersects refuses, naming the original crossing.
112//! 6. [`accept_or_break_out`] AGAIN, on the repaired body, so a re-trim that
113//!    left a hole off its face is broken out in turn or refused, rather than
114//!    shipped.
115//!
116//! Steps 4 and 6 bound each other, and the boundary is not a gap. The crossing
117//! repair needs a section to split along, and a section exists exactly while
118//! the moved carrier still MEETS the body; the containment floor decides what
119//! is left. Measured on the 2026-09-21 runaway-bore report, the crossing
120//! regime ends and the carrier's clearance turns positive in the SAME step of
121//! a swept drag, and 13.90 mm past that the repair is handed a body with
122//! nothing to imprint and hands it straight back.
123//!
124//! What step 2's breakout adds is the OTHER side of that boundary: the window
125//! where the mouth has crossed its face's edge but the wall's overshoot is
126//! still a sliver the tessellation cannot resolve, so the scan confirms
127//! nothing and step 4 is never entered. On the reported document that window
128//! is `s = 0.99 … 1.03` of the drag, and the scan takes over at `s = 1.04`.
129//! The two routes reach the same [`split_and_drop`] and the repair is one
130//! repair.
131//!
132//! ## Cost
133//!
134//! One tessellation and one BVH sweep per accepted result, and only on the
135//! success path. Both verdicts come off that one scan. The containment floor
136//! traces the loops of PLANAR faces that have more than one, in their own
137//! parameters; a face with a single loop costs nothing.
138//!
139//! That note used to stop at that sentence, and stopping there was
140//! misleading: the floor's cost was never in how MANY faces qualify but in
141//! how LARGE one qualifying face's loops can be. A loop is traced at 32
142//! stations per KNOT SPAN, so a box's face gives four points and an imported
143//! 100-tooth gear's flat gives 1.1 MILLION; testing every hole point against
144//! every one of those is a PRODUCT, and three such faces out of 1004 cost 24
145//! s of a 30 s replay. The trace is unchanged — decimating it would weaken
146//! the predicate — and the product is what went instead: the outer loop is
147//! indexed once into bands of its own `v` ([`BoundaryBands`]) and each hole
148//! point reads only the band it falls in, for the same answer at `hole
149//! points × segments per band`.
150
151use crate::{KernelRefusal, KernelStage, OrRefuse, SoundnessDefect};
152use crate::topology::{BrepSolid, LoopRecord};
153use crate::{solid_self_intersections, SelfIntersectionOptions, SelfIntersectionReport};
154
155/// Scan a finished result and either accept it, repair it, or refuse it by
156/// name.
157///
158/// `entry` is the lane's own name, so the refusal reads as that lane's
159/// refusal rather than as a soundness module's.
160pub fn accept_sound(solid: BrepSolid, entry: &str) -> Result<BrepSolid, KernelRefusal> {
161    let report = scan(&solid, entry)?;
162    if !report.is_flagged() {
163        return accept_or_break_out(solid, entry, &report);
164    }
165    dump_flagged(&solid, entry);
166    if let Some(refusal) = fold_refusal(&report, entry) {
167        return Err(refusal);
168    }
169    // Two faces crossing: try to split and trim before refusing.
170    let repaired = match resolve_face_crossing(&solid) {
171        Ok(repaired) => repaired,
172        // The crossing lane declined. Where the containment floor ALSO convicts
173        // a hole loop that has left its face, this body may be a BREAKOUT the
174        // other route can take — a mouth that has migrated onto a neighbouring
175        // face crosses that neighbour, so the scan flags it, and the pair the
176        // scan picked is not always the pair the re-trim needs.
177        //
178        // Guarded three ways, because a second chance is exactly how a body
179        // that should refuse gets talked into building. The floor's conviction
180        // is the ticket — a body that merely self-intersects never reaches
181        // here. The success path above is untouched, so a body that repairs
182        // today repairs identically. And the result goes through the same
183        // re-scan and the same floor below as any other repair.
184        Err(reason) if first_hole_breach(&solid).is_some() => {
185            match break_out_until_contained(solid.clone(), entry) {
186                Ok(repaired) => repaired,
187                Err(second) => {
188                    return Err(crossing_refusal(
189                        &report,
190                        entry,
191                        &format!(
192                            "{reason}. The breakout route was offered it too and declined: \
193                             {second}"
194                        ),
195                    ))
196                }
197            }
198        }
199        Err(reason) => return Err(crossing_refusal(&report, entry, &reason.message)),
200    };
201    let after = scan(&repaired, entry)?;
202    if after.is_flagged() {
203        return Err(crossing_refusal(
204            &report,
205            entry,
206            &format!(
207                "the repair ran and the result still self-intersects — {}",
208                after.summary().unwrap_or_else(|| "no summary".into())
209            ),
210        ));
211    }
212    accept_or_break_out(repaired, entry, &after)
213}
214
215/// How many hole-containment breaches one acceptance may break out, before it
216/// stops and refuses.
217///
218/// A bore has TWO mouths, and a motion big enough to walk one onto the next
219/// face can walk the other too — so one is not enough, and the whole point of a
220/// bound is that a repair which does not actually reduce the breaches cannot
221/// spin. Four leaves room for a two-mouth migration and a re-trim that opens
222/// one more, and stops well short of a body being talked into building by
223/// attrition.
224const MAX_BREAKOUT_ROUNDS: usize = 4;
225
226/// The containment floor, with the BREAKOUT tried before the refusal.
227///
228/// A hole loop that has left its face is not one case but two, and they are
229/// told apart by where it went. A hole that has left ONTO ANOTHER FACE has a
230/// right answer the kernel can build — the moved carrier re-trimmed against the
231/// face it now opens through — and this builds it when the mouth STRADDLES its
232/// face's boundary, which is the reported case. A hole that has left the body
233/// has no such answer, and this refuses exactly as the floor always did, with
234/// the repair's own reason for declining carried after the floor's so the
235/// refusal says which of the two it was.
236///
237/// **A mouth that has migrated WHOLLY off its face is built by the CROSSING
238/// LANE, not by this route.** Until 2026-09-22 it was built by neither: the
239/// departed face kept a hole loop the imprint never cut and the drop
240/// enumeration had no move for retiring one, so every candidate face this
241/// route offered the carrier to declined. The drop closures have that move now
242/// — [`fragment_is_detached`] and [`holds_only_stale_loops`] — and a turned
243/// bore whose exit migrates onto the next face is repaired to its closed form
244/// through [`resolve_face_crossing`], which `accept_sound` reaches first.
245///
246/// What is left for this route to decide is the mouth that has arrived on
247/// NOTHING: a carrier clear of the body cuts no face of it, every candidate
248/// declines, and the refusal names each face it tried and the distance it
249/// tried it at. That is measured, not assumed, by
250/// `a_mouth_that_has_left_the_body_reaches_the_migration_route_and_is_refused_by_name`.
251fn accept_or_break_out(solid: BrepSolid, entry: &str, report: &SelfIntersectionReport) -> Result<BrepSolid, KernelRefusal> {
252    if refuse_holes_leaving_their_faces(&solid, entry).is_ok() {
253        // accept_refuses_undecided_crossing_20261016: contained, and a scan
254        // that ended undecided is still not a clean one. A hole that left its
255        // face is named by the floor below first: that is the more specific
256        // diagnosis, and its breakout re-scans what it builds.
257        if let Some(refusal) = undecided_refusal(report, entry) {
258            return Err(refusal);
259        }
260        return Ok(solid);
261    }
262    break_out_until_contained(solid, entry)
263}
264
265/// Break out every hole the containment floor convicts, one at a time, and hand
266/// back a body that BOTH scans clean and passes the floor.
267///
268/// The rounds are what a two-mouth migration needs: repairing one mouth leaves
269/// the other still off its face, and — while it is — the body legitimately
270/// still passes through itself, because the second mouth's wall is still
271/// standing where the material is not. So an intermediate round is allowed to
272/// be flagged; the END of it is not. Nothing is returned that would not have
273/// been accepted had it been built that way in the first place.
274fn break_out_until_contained(solid: BrepSolid, entry: &str) -> Result<BrepSolid, KernelRefusal> {
275    let mut current = solid;
276    for _ in 0..MAX_BREAKOUT_ROUNDS {
277        let Some(breach) = first_hole_breach(&current) else {
278            // Contained. It still has to be SOUND to be returned.
279            let report = scan_output(&current, entry)?;
280            if report.is_flagged() {
281                return Err(KernelRefusal::unsound(
282                    KernelStage::Validate,
283                    SoundnessDefect::Crossing,
284                    crossing_faces(&report),
285                    format!(
286                        "{entry}: the breakout repair built a body and it SELF-INTERSECTS — {}",
287                        report.summary().unwrap_or_else(|| "no summary".into()),
288                    ),
289                ));
290            }
291            return Ok(current);
292        };
293        // The floor's refusal is the primary one; the breakout's decline is
294        // context appended to it, and the class stays the floor's.
295        let floor = hole_breach_refusal(&current, &breach, entry);
296        current = repair_hole_breakout(&current, &breach).map_err(|reason| {
297            floor.with_message(|floor| {
298                format!("{floor}. The breakout repair was offered this body and declined it: {reason}")
299            })
300        })?;
301    }
302    let breach = first_hole_breach(&current).expect("the loop only falls through on a breach");
303    Err(
304        hole_breach_refusal(&current, &breach, entry).with_message(|floor| {
305            format!(
306                "{floor}. {MAX_BREAKOUT_ROUNDS} breakouts were built and a hole is still off its face, so \
307                 this stops rather than repairing a body into one nobody asked for"
308            )
309        }),
310    )
311}
312
313/// [`accept_sound`] for a lane whose INPUTS may themselves self-intersect.
314///
315/// A boolean of two bodies that already pass through themselves is entitled to
316/// return a result that does: the defect came in with the operand and refusing
317/// it here would blame the wrong operation. What must never pass is a crossing
318/// the operation ITSELF introduced, so the inputs are scanned only when the
319/// output is flagged — the common case pays for one scan, not three.
320pub fn accept_sound_against(
321    inputs: &[&BrepSolid],
322    solid: BrepSolid,
323    entry: &str,
324) -> Result<BrepSolid, KernelRefusal> {
325    let report = scan_output(&solid, entry)?;
326    if !report.is_flagged() {
327        return Ok(solid);
328    }
329    for input in inputs {
330        let before = scan(input, entry)?;
331        if before.is_flagged() {
332            // Said plainly rather than silently: the operand arrived broken.
333            return Ok(solid);
334        }
335    }
336    accept_sound(solid, entry)
337}
338
339/// [`accept_sound_against`] reading only the faces in `candidates`: the faces
340/// the operation built or changed, their neighbours, and every face whose box
341/// reaches the other operand. A face outside that set is an operand's own,
342/// untouched, and lies clear of everything the operation added, so a crossing
343/// on it was the operand's and passes through exactly as the full scan passes
344/// it (the inputs are scanned, flagged, and the result returned). An empty
345/// set is a result with nothing to scan. A flagged subset hands the whole
346/// result to the full acceptance, so every repair and refusal reads as
347/// before. (`BooleanOptions::untouched_passthrough`)
348pub fn accept_sound_against_scoped(
349    inputs: &[&BrepSolid],
350    solid: BrepSolid,
351    entry: &str,
352    candidates: &rustc_hash::FxHashSet<u64>,
353) -> Result<BrepSolid, KernelRefusal> {
354    if candidates.is_empty() {
355        return Ok(solid);
356    }
357    let faces: Vec<crate::topology::FaceRecord> = solid
358        .shells
359        .iter()
360        .flat_map(|shell| &shell.faces)
361        .filter(|face| candidates.contains(&face.id))
362        .cloned()
363        .collect();
364    if faces.len() == solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>() {
365        return accept_sound_against(inputs, solid, entry);
366    }
367    let edge_ids: rustc_hash::FxHashSet<u64> = faces
368        .iter()
369        .flat_map(|face| &face.loops)
370        .flat_map(|loop_record| &loop_record.coedges)
371        .map(|coedge| coedge.edge_id)
372        .collect();
373    let edges: Vec<crate::topology::EdgeRecord> =
374        solid.edges.iter().filter(|edge| edge_ids.contains(&edge.id)).cloned().collect();
375    let vertex_ids: rustc_hash::FxHashSet<u64> =
376        edges.iter().flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id]).collect();
377    let subset = BrepSolid {
378        mass_properties_cache: Default::default(),
379        cells: Default::default(),
380        id: solid.id,
381        vertices: solid.vertices.iter().filter(|vertex| vertex_ids.contains(&vertex.id)).cloned().collect(),
382        edges,
383        shells: vec![crate::topology::ShellRecord { id: 1, faces }],
384        genus: 0,
385        kind: crate::BodyKind::Solid,
386    };
387    // The bands come from the whole result, as the full scan derives them.
388    let options = SelfIntersectionOptions::for_solid(&solid);
389    let report = scan_with(&subset, options, entry)?;
390    if let Some(refusal) = undecided_refusal(&report, entry) {
391        return Err(refusal);
392    }
393    if !report.is_flagged() {
394        return Ok(solid);
395    }
396    accept_sound_against(inputs, solid, entry)
397}
398
399/// The scan, with a read failure reported as a refusal rather than swallowed:
400/// a result that could not be scanned is not a result that scanned clean.
401fn scan(solid: &BrepSolid, entry: &str) -> Result<SelfIntersectionReport, KernelRefusal> {
402    scan_with(solid, SelfIntersectionOptions::for_solid(solid), entry)
403}
404
405// BREP named hunk: begin accept_refuses_undecided_crossing_20261016
406/// Opt-in (`BREP_SOUNDNESS_REFUSE_UNDECIDED=1`), see `resolve::refuse_enabled`.
407/// A RESULT whose scan ends undecided is not a result that scanned clean: the
408/// mesh confirmation could not decide some candidate crossings and the carriers
409/// did not settle them either (`brep/soundness/resolve.rs`). It is refused,
410/// naming the face pairs and why each stayed open. A report that is flagged is
411/// left to the crossing and fold refusals, which say what is wrong.
412fn undecided_refusal(report: &SelfIntersectionReport, entry: &str) -> Option<KernelRefusal> {
413    if !crate::soundness::resolve::enabled() || !crate::soundness::resolve::refuse_enabled() {
414        return None;
415    }
416    undecided_refusal_message(report, entry)
417}
418
419/// The refusal itself, whatever the switch: `None` for a flagged or a clean report.
420fn undecided_refusal_message(report: &SelfIntersectionReport, entry: &str) -> Option<KernelRefusal> {
421    if report.is_flagged() || report.undecided == 0 {
422        return None;
423    }
424    let pairs: Vec<String> = report
425        .undecided_pairs
426        .iter()
427        .map(|pair| {
428            let cause = match pair.cause {
429                crate::CrossingUndecidedCause::Witness => "the mesh crossing's witness is not a point both faces own",
430                crate::CrossingUndecidedCause::Pierce => "a projection or a normal could not be evaluated",
431                crate::CrossingUndecidedCause::MissingFace => "a triangle's face is not in the body",
432            };
433            format!("faces {} and {} near ({:.4}, {:.4}, {:.4}): {cause}", pair.face_a, pair.face_b, pair.point.x, pair.point.y, pair.point.z)
434        })
435        .collect();
436    let more = report.undecided.saturating_sub(pairs.len());
437    Some(KernelRefusal::non_convergence(
438        KernelStage::Validate,
439        "self_intersection_undecided",
440        format!(
441            "{entry}: the soundness scan could not decide {} candidate crossing(s), and the exact carriers \
442             did not settle them either, so the result is not known to be free of self-intersection: {}{}",
443            report.undecided,
444            pairs.join("; "),
445            if more > 0 { format!(" (and {more} more)") } else { String::new() },
446        ),
447    ))
448}
449
450/// [`scan`] for a RESULT about to be returned: an undecided scan refuses.
451fn scan_output(solid: &BrepSolid, entry: &str) -> Result<SelfIntersectionReport, KernelRefusal> {
452    let report = scan(solid, entry)?;
453    match undecided_refusal(&report, entry) {
454        Some(refusal) => Err(refusal),
455        None => Ok(report),
456    }
457}
458// BREP named hunk: end accept_refuses_undecided_crossing_20261016
459
460fn scan_with(
461    solid: &BrepSolid,
462    options: SelfIntersectionOptions,
463    entry: &str,
464) -> Result<SelfIntersectionReport, KernelRefusal> {
465    solid_self_intersections(solid, options).map_err(|message| {
466        KernelRefusal::internal(
467            KernelStage::Validate,
468            "soundness_scan",
469            format!("{entry}: the soundness scan could not run: {message}"),
470        )
471    })
472}
473
474/// The faces the scan's confirmed crossings name, each once, in report order.
475fn crossing_faces(report: &SelfIntersectionReport) -> Vec<u64> {
476    let mut faces = Vec::new();
477    for crossing in &report.confirmed {
478        for face in [crossing.face_a, crossing.face_b] {
479            if !faces.contains(&face) {
480                faces.push(face);
481            }
482        }
483    }
484    faces
485}
486
487/// The refusal for a face folded through itself — named, with both feet, the
488/// point and the margins, and pointing at the lane that owns the fix.
489fn fold_refusal(report: &SelfIntersectionReport, entry: &str) -> Option<KernelRefusal> {
490    let fold = report.folds.first()?;
491    let faces = report.folds.iter().map(|fold| fold.face).collect();
492    let message = format!(
493        "{entry}: the result's face {}{} FOLDS THROUGH ITSELF in 3D — its surface carries \
494         ({:.6}, {:.6}) and ({:.6}, {:.6}), {:.3e} apart in its own domain and both inside \
495         its trim, to the same point ({:.4}, {:.4}, {:.4}) (residual {:.3e}, branches {:.1} \
496         degrees apart){}. The face is wrong in AREA where its volume is not, so nothing that \
497         measures volume notices and `validate()` cannot see it at all. A fold is a trim that \
498         was never constructed: the folded part of the parameter region has to be carved away \
499         where the carrier is built, not repaired out of the finished body.",
500        fold.face,
501        fold.face_name
502            .as_ref()
503            .map(|name| format!(" '{name}'"))
504            .unwrap_or_default(),
505        fold.uv_a[0],
506        fold.uv_a[1],
507        fold.uv_b[0],
508        fold.uv_b[1],
509        fold.uv_separation,
510        fold.point.x,
511        fold.point.y,
512        fold.point.z,
513        fold.residual,
514        fold.normal_angle.to_degrees(),
515        if report.folds.len() > 1 {
516            format!(" ({} folded faces in all)", report.folds.len())
517        } else {
518            String::new()
519        },
520    );
521    Some(KernelRefusal::unsound(
522        KernelStage::Validate,
523        SoundnessDefect::Fold,
524        faces,
525        message,
526    ))
527}
528
529/// The refusal for two faces crossing, naming every pair and why the repair
530/// did not take it.
531fn crossing_refusal(report: &SelfIntersectionReport, entry: &str, reason: &str) -> KernelRefusal {
532    let pairs = report
533        .confirmed
534        .iter()
535        .map(|crossing| {
536            format!(
537                "faces {} and {} near ({:.4}, {:.4}, {:.4}) (straddle {:.3e} against a band of {:.1e})",
538                crossing.face_a,
539                crossing.face_b,
540                crossing.point.x,
541                crossing.point.y,
542                crossing.point.z,
543                crossing.straddle,
544                report.options.straddle_band
545            )
546        })
547        .collect::<Vec<_>>()
548        .join("; ");
549    KernelRefusal::unsound(
550        KernelStage::Validate,
551        SoundnessDefect::Crossing,
552        crossing_faces(report),
553        format!(
554            "{entry}: the result SELF-INTERSECTS — {pairs}. The enclosed space can still measure \
555             right, so neither a volume oracle nor `validate()` would have caught this. The repair \
556             that splits both faces along their carriers' intersection and drops the enclosed \
557             material did not take it: {reason}"
558        ),
559    )
560}
561
562// ---------------------------------------------------------------------------
563// The hole-containment floor
564// ---------------------------------------------------------------------------
565
566/// Stations per curved pcurve span when a loop is traced in its face's own
567/// parameters. A straight pcurve contributes its start point only — the next
568/// coedge begins where it ends.
569const LOOP_SAMPLES_PER_SPAN: usize = 32;
570
571/// A loop of a face as a closed polyline in that face's own parameter plane,
572/// traced coedge by coedge along each pcurve (already oriented to its coedge).
573fn loop_parameter_polyline(loop_record: &LoopRecord) -> Result<Vec<(f64, f64)>, KernelRefusal> {
574    pcurve_polyline(loop_record.coedges.iter().map(|coedge| &coedge.pcurve))
575}
576
577/// The same walk for a FRAGMENT's loop, which carries the same pcurves under a
578/// different record. One sampler, so a loop traced during the repair and the
579/// same loop traced by the containment floor are the same polyline.
580fn fragment_loop_polyline(
581    fragment_loop: &crate::fragment::FragmentLoop,
582) -> Result<Vec<(f64, f64)>, KernelRefusal> {
583    pcurve_polyline(fragment_loop.coedges.iter().map(|coedge| &coedge.pcurve))
584}
585
586fn pcurve_polyline<'a>(
587    pcurves: impl Iterator<Item = &'a crate::NurbsCurve>,
588) -> Result<Vec<(f64, f64)>, KernelRefusal> {
589    let mut points = Vec::new();
590    for pcurve in pcurves {
591        let [d0, d1] = pcurve.domain().or_refuse(KernelStage::Validate, "domain")?;
592        let steps = if pcurve.degree <= 1 && pcurve.control_points.len() == 2 {
593            1
594        } else {
595            let mut spans = 0usize;
596            for window in pcurve.knots.windows(2) {
597                if window[1] > window[0] {
598                    spans += 1;
599                }
600            }
601            spans.max(1) * LOOP_SAMPLES_PER_SPAN
602        };
603        for step in 0..steps {
604            let uv = pcurve.evaluate(d0 + (d1 - d0) * (step as f64 / steps as f64)).or_refuse(KernelStage::Validate, "evaluate")?;
605            points.push((uv.x, uv.y));
606        }
607    }
608    Ok(points)
609}
610
611fn polyline_area(points: &[(f64, f64)]) -> f64 {
612    let mut twice = 0.0;
613    for index in 0..points.len() {
614        let (x0, y0) = points[index];
615        let (x1, y1) = points[(index + 1) % points.len()];
616        twice += x0 * y1 - x1 * y0;
617    }
618    0.5 * twice
619}
620
621/// The outer loop's segments, bucketed by the band of `v` each one spans.
622///
623/// The floor asks a hole point two questions — even-odd parity against the
624/// outer loop, and whether the point sits within `tolerance` of it — and both
625/// read ONLY the segments whose own `v` range reaches the point's `v`. A
626/// segment that does not reach it has both endpoints on the same side of the
627/// horizontal ray, so it cannot toggle the parity; and every point of it is
628/// further away than `tolerance` in `v` alone, so it cannot be the witness
629/// that the point is on the boundary. Answering out of the band is therefore
630/// the SAME answer as walking the whole loop, not an approximation of it: the
631/// set of segments that can toggle is identical, parity is an XOR and does not
632/// care in what order they come, and the nearest segment — if there is one
633/// within `tolerance` at all — is among those searched.
634///
635/// What changes is the COST. The brute-force walk is `hole points × outer
636/// points`, which on an imported 100-tooth gear's flat is 1.1 million outer
637/// stations against 11 904 hole stations — 1.3e10 pairs on ONE face, 24 s of a
638/// 30 s replay. Through the bands it is `hole points × segments per band`,
639/// plus one linear pass to build.
640///
641/// Laid out CSR — one offsets array and one flat segment array — because a
642/// loop of a million points would otherwise want a million `Vec` headers.
643struct BoundaryBands<'a> {
644    points: &'a [(f64, f64)],
645    /// The bottom of the first band: the loop's lowest `v`.
646    v_lo: f64,
647    /// Bands per unit of `v`. Zero when the loop has no `v` extent to divide,
648    /// in which case there is one band and everything lands in it.
649    bands_per_v: f64,
650    /// `starts[band] .. starts[band + 1]` indexes `segments`.
651    starts: Vec<u32>,
652    /// Segment `i` is `points[i] -> points[(i + 1) % len]`, the closing
653    /// segment included.
654    segments: Vec<u32>,
655}
656
657impl<'a> BoundaryBands<'a> {
658    fn build(points: &'a [(f64, f64)]) -> Self {
659        let count = points.len();
660        debug_assert!(count < u32::MAX as usize, "segment indices are u32");
661        let (mut v_lo, mut v_hi) = (f64::INFINITY, f64::NEG_INFINITY);
662        let mut variation = 0.0;
663        for index in 0..count {
664            let (_, y0) = points[index];
665            let (_, y1) = points[(index + 1) % count];
666            v_lo = v_lo.min(y0);
667            v_hi = v_hi.max(y0);
668            variation += (y1 - y0).abs();
669        }
670        // One band per `variation / count` of `v`. That width is what makes the
671        // index linear in the loop whatever the loop's shape: a segment lands
672        // in `its own v extent / band width + 2` bands, and those sum to about
673        // `3 × count` however the vertical variation is distributed — a gear
674        // tooth's thousands of short segments and a box's four long ones alike.
675        let span = v_hi - v_lo;
676        let bands = if span.is_finite() && span > 0.0 && variation > 0.0 {
677            let wanted = span * count as f64 / variation;
678            if wanted.is_finite() && wanted >= 1.0 {
679                (wanted as usize).min(count.max(1))
680            } else {
681                1
682            }
683        } else {
684            1
685        };
686        let bands_per_v = if bands > 1 { bands as f64 / span } else { 0.0 };
687        let band_of = |v: f64| -> usize {
688            if bands_per_v == 0.0 {
689                return 0;
690            }
691            let raw = (v - v_lo) * bands_per_v;
692            // Written so a NaN takes the first branch rather than falling
693            // through to an `as usize` cast of it.
694            if !(raw > 0.0) {
695                0
696            } else if raw >= (bands - 1) as f64 {
697                bands - 1
698            } else {
699                raw as usize
700            }
701        };
702        let mut starts = vec![0u32; bands + 1];
703        for index in 0..count {
704            let (_, y0) = points[index];
705            let (_, y1) = points[(index + 1) % count];
706            let (lo, hi) = (band_of(y0.min(y1)), band_of(y0.max(y1)));
707            for start in starts[lo + 1..=hi + 1].iter_mut() {
708                *start += 1;
709            }
710        }
711        for band in 1..=bands {
712            starts[band] += starts[band - 1];
713        }
714        let mut cursor = starts.clone();
715        let mut segments = vec![0u32; starts[bands] as usize];
716        for index in 0..count {
717            let (_, y0) = points[index];
718            let (_, y1) = points[(index + 1) % count];
719            let (lo, hi) = (band_of(y0.min(y1)), band_of(y0.max(y1)));
720            for band in lo..=hi {
721                segments[cursor[band] as usize] = index as u32;
722                cursor[band] += 1;
723            }
724        }
725        Self {
726            points,
727            v_lo,
728            bands_per_v,
729            starts,
730            segments,
731        }
732    }
733
734    fn band_count(&self) -> usize {
735        self.starts.len() - 1
736    }
737
738    /// The band `v` falls in, clamped. A `v` outside the loop's own extent
739    /// clamps to an end band, whose segments then fail the tests on their own
740    /// terms — nothing straddles a ray the whole loop sits above or below.
741    fn band_of(&self, v: f64) -> usize {
742        if self.bands_per_v == 0.0 {
743            return 0;
744        }
745        let bands = self.band_count();
746        let raw = (v - self.v_lo) * self.bands_per_v;
747        if !(raw > 0.0) {
748            0
749        } else if raw >= (bands - 1) as f64 {
750            bands - 1
751        } else {
752            raw as usize
753        }
754    }
755
756    fn segment(&self, index: u32) -> ((f64, f64), (f64, f64)) {
757        let index = index as usize;
758        (
759            self.points[index],
760            self.points[(index + 1) % self.points.len()],
761        )
762    }
763
764    /// Even-odd parity: is `(u, v)` inside the closed loop?
765    fn contains(&self, point: (f64, f64)) -> bool {
766        let (u, v) = point;
767        let band = self.band_of(v);
768        let mut inside = false;
769        for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize] {
770            let ((x0, y0), (x1, y1)) = self.segment(index);
771            if (y0 > v) != (y1 > v) && u < x0 + (v - y0) * (x1 - x0) / (y1 - y0) {
772                inside = !inside;
773            }
774        }
775        inside
776    }
777
778    /// Does `(u, v)` sit within `tolerance` of the loop — is it ON the
779    /// boundary rather than off the face?
780    ///
781    /// The bands searched are widened by one on each side of the `tolerance`
782    /// band, so the margin against a segment being excluded by a rounding of
783    /// its own `v` extent is a whole band rather than an ulp.
784    fn within(&self, point: (f64, f64), tolerance: f64) -> bool {
785        let (u, v) = point;
786        let lo = self.band_of(v - tolerance).saturating_sub(1);
787        let hi = (self.band_of(v + tolerance) + 1).min(self.band_count() - 1);
788        for band in lo..=hi {
789            for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize]
790            {
791                let ((x0, y0), (x1, y1)) = self.segment(index);
792                let (dx, dy) = (x1 - x0, y1 - y0);
793                let length_squared = dx * dx + dy * dy;
794                let t = if length_squared > 0.0 {
795                    (((u - x0) * dx + (v - y0) * dy) / length_squared).clamp(0.0, 1.0)
796                } else {
797                    0.0
798                };
799                let (px, py) = (x0 + t * dx, y0 + t * dy);
800                if ((u - px).powi(2) + (v - py).powi(2)).sqrt() <= tolerance {
801                    return true;
802                }
803            }
804        }
805        false
806    }
807
808    /// The point of the loop nearest to `(u, v)` in the parameter plane, and
809    /// how far it is.
810    ///
811    /// Searched outward from the point's own band a ring at a time, stopping
812    /// when the next ring's `v` alone is further than the best found: every
813    /// segment in band `b` has every point of it within that band's `v`
814    /// range, so a band `r` away from the query's cannot hold a closer point
815    /// than `(r - 1)` band heights. A query beyond the loop's `v` extent
816    /// clamps to an end band and the same bound holds, larger. Called only
817    /// for a station that has already FAILED parity and the parameter band,
818    /// so its cost is paid once per conviction, not once per station.
819    fn nearest(&self, point: (f64, f64)) -> ((f64, f64), f64) {
820        let (u, v) = point;
821        let bands = self.band_count();
822        let centre = self.band_of(v);
823        let band_height = if self.bands_per_v > 0.0 { 1.0 / self.bands_per_v } else { f64::INFINITY };
824        let mut best = f64::INFINITY;
825        let mut best_point = self.points[0];
826        let search = |band: usize, best: &mut f64, best_point: &mut (f64, f64)| {
827            for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize] {
828                let ((x0, y0), (x1, y1)) = self.segment(index);
829                let (dx, dy) = (x1 - x0, y1 - y0);
830                let length_squared = dx * dx + dy * dy;
831                let t = if length_squared > 0.0 {
832                    (((u - x0) * dx + (v - y0) * dy) / length_squared).clamp(0.0, 1.0)
833                } else {
834                    0.0
835                };
836                let candidate = (x0 + t * dx, y0 + t * dy);
837                let distance = ((u - candidate.0).powi(2) + (v - candidate.1).powi(2)).sqrt();
838                if distance < *best {
839                    *best = distance;
840                    *best_point = candidate;
841                }
842            }
843        };
844        for ring in 0..bands {
845            if ring > 1 && (ring - 1) as f64 * band_height > best {
846                break;
847            }
848            let below = centre.checked_sub(ring);
849            let above = if centre + ring < bands { Some(centre + ring) } else { None };
850            if below.is_none() && above.is_none() {
851                break;
852            }
853            if let Some(band) = below {
854                search(band, &mut best, &mut best_point);
855            }
856            if let Some(band) = above {
857                if ring > 0 {
858                    search(band, &mut best, &mut best_point);
859                }
860            }
861        }
862        (best_point, best)
863    }
864}
865
866/// THE HOLE-CONTAINMENT FLOOR: a face whose hole loop has left that face's
867/// own outer loop is refused, naming the hole — on EVERY carrier, planar or
868/// not.
869///
870/// A hole is a hole IN something. Carry a bore's mouth off the flat it opens
871/// through — a direct edit dragging the wall clear of the body — and what the
872/// fixed-topology road emits is the mouth loop hanging in the plane beside the
873/// face it is supposed to perforate, with the wall it bounds somewhere else
874/// entirely. Every other floor is blind to it: `validate()` tests INCIDENCE and
875/// the incidences are all intact; the scan needs two faces to CROSS and the
876/// runaway bore crosses nothing; and the divergence-theorem volume of a
877/// cylinder does not depend on where its axis sits, so the body can measure to
878/// the last bit of the right answer while being the wrong body. The correct
879/// result needs the hole to break through a neighbouring face — a topology
880/// change the direct edits do not make — so this refuses instead of shipping it.
881///
882/// Decided in the face's own parameter plane by parity against the outer loop
883/// (the one enclosing the largest parameter area). A point that fails parity
884/// but sits within `tolerance` of the outer loop is treated as ON it, so a
885/// mouth tangent to its face's own boundary is not convicted by sampling
886/// noise. The predicate is only as fine as the sampling: a curved hole poking
887/// out by less than its sampled chord sag can still pass.
888///
889/// On a PLANE the parameter plane is affine to space and the pcurves are exact
890/// images, so the band is the parameter band alone. On every other carrier the
891/// pcurves are FITTED, to [`crate::PCURVE_REFINEMENT_TOLERANCE`] in space, and
892/// a fit's noise can put a station of a legitimately tangent mouth a few
893/// parameter ulps outside its face. So a station that fails both tests on a
894/// non-affine face is convicted only if it is also further than
895/// [`CURVED_BREACH_ACQUITTAL`] IN SPACE from the nearest point of the outer
896/// loop — the same trace read back through the surface. That distance is the
897/// smallest curved breach the floor can see; it is a floor on the fit, not on
898/// the sampling. Until 2026-09-30 every non-planar face was skipped outright,
899/// so a mouth carried off a cylinder wall or a torus by a motion that pierced
900/// nothing the scan could resolve shipped as a wrong body.
901///
902/// A closed carrier is read in two shapes. Where NO loop winds the period
903/// (the nested case — a bored post's wall, whose outer loop holds the seam
904/// twice), the outer loop is the one enclosing the largest parameter area and
905/// the holes must lie inside it; a hole station is folded by whole periods
906/// into the outer loop's window only where that loop carries no seam coedge,
907/// so a hole traced across a seam that IS an edge of the outer loop (a hole
908/// with an edge of its own face running through it — the builders split the
909/// seam where a mouth reaches it) is a wrong body and is convicted. Where some
910/// loop DOES wind (the banded case — a refitted full cylinder is bounded by
911/// two rings and no seam, a dome cut off a sphere by one), the face has no
912/// outer loop and a hole must lie on every ring's material side, decided
913/// along the open direction with the ring's winding sign and the face's own
914/// orientation convention ([`banded_hole_breach`]). A band whose rings wind a
915/// carrier closed BOTH ways selects its intended region by the same orientation
916/// convention, using the next transverse boundary modulo that period. A loop
917/// winding both ways remains outside this arm. A breach the banded arm convicts is refused, not
918/// broken out: the breakout route reads a nested outer loop.
919///
920/// Both questions go through [`BoundaryBands`], which is an INDEX over the
921/// outer loop and not a coarser reading of it: the answers are the ones a walk
922/// of the whole traced polyline gives, off the whole traced polyline, at a
923/// cost that is no longer the product of the two loops' lengths.
924///
925/// This runs at the END of [`accept_sound`], on the body the repair left, so a
926/// crossing the repair can take is still taken — the reason an earlier
927/// primitive-level copy of this floor was removed.
928fn refuse_holes_leaving_their_faces(solid: &BrepSolid, entry: &str) -> Result<(), KernelRefusal> {
929    match first_hole_breach(solid) {
930        Some(breach) => Err(hole_breach_refusal(solid, &breach, entry)),
931        None => Ok(()),
932    }
933}
934
935/// A hole loop the floor has convicted: WHICH face, which loop of it, and the
936/// sample that fell outside, in that face's own parameters.
937///
938/// The floor's verdict is a yes or no; this is the same verdict with the
939/// witness kept, so a lane that can BUILD the right body has something to
940/// build it from. Nothing about the predicate changes by recording it.
941struct HoleBreach {
942    face: u64,
943    loop_index: usize,
944    loop_id: u64,
945    at: (f64, f64),
946    /// Convicted by the banded arm — the face has rings and no outer loop, so
947    /// the breakout route, which reads a nested outer loop, has nothing to
948    /// read and declines by name.
949    banded: bool,
950}
951
952/// THE FLOOR'S PREDICATE, verbatim — the first hole loop on any face that has
953/// left that face's own outer loop, or `None`.
954///
955/// This is the whole of the decision [`refuse_holes_leaving_their_faces`] used
956/// to make inline; the refusal is now written by [`hole_breach_refusal`] off
957/// what this returns. Same loops, same trace, same bands, same tolerance, same
958/// first-hit order.
959fn first_hole_breach(solid: &BrepSolid) -> Option<HoleBreach> {
960    for shell in &solid.shells {
961        for face in &shell.faces {
962            if face.loops.len() < 2 {
963                continue;
964            }
965            // Exact pcurves on an affine carrier; fitted ones everywhere else,
966            // which earn the acquittal in space.
967            let fitted = !face.surface.is_affine().unwrap_or(false);
968            let closed = face.surface.closed_directions().unwrap_or((false, false));
969            let period = (
970                face.surface.domain_u().map(|[a, b]| b - a).unwrap_or(0.0),
971                face.surface.domain_v().map(|[a, b]| b - a).unwrap_or(0.0),
972            );
973            let windings: Vec<(i32, i32)> = face
974                .loops
975                .iter()
976                .map(|record| loop_winding(record, closed, period))
977                .collect();
978            let polylines = face
979                .loops
980                .iter()
981                .map(loop_parameter_polyline)
982                .collect::<Result<Vec<_>, KernelRefusal>>()
983                // A loop this cannot be traced from is not a loop this can
984                // convict: the acceptance's own floors already ran.
985                .unwrap_or_default();
986            if polylines.len() != face.loops.len() || polylines.iter().any(Vec::is_empty) {
987                continue;
988            }
989            let breach = if windings.iter().all(|&winding| winding == (0, 0)) {
990                let spherical = matches!(face.surface.analytic(), Some(crate::AnalyticSurface::Sphere { .. }));
991                if spherical && !sphere_has_parameter_outer(solid, face, &polylines) {
992                    spherical_hole_breach(solid, face)
993                } else {
994                    nested_hole_breach(face, &polylines, fitted, closed, period)
995                }
996            } else {
997                banded_hole_breach(face, &polylines, &windings, fitted, closed, period)
998            };
999            if breach.is_some() {
1000                return breach;
1001            }
1002        }
1003    }
1004    None
1005}
1006
1007/// A bounded spherical patch can use the existing indexed nested floor.
1008/// Its largest UV loop must be a real, correctly oriented material outer
1009/// boundary, with the other real loops oriented as holes. A full-domain
1010/// rectangle made only of seams and collapsed poles is not such a boundary.
1011fn sphere_has_parameter_outer(
1012    solid: &BrepSolid,
1013    face: &crate::FaceRecord,
1014    polylines: &[Vec<(f64, f64)>],
1015) -> bool {
1016    let areas: Vec<_> = polylines.iter().map(|points| polyline_area(points)).collect();
1017    let Some(outer) = (0..areas.len()).max_by(|&a, &b| areas[a].abs().total_cmp(&areas[b].abs())) else {
1018        return false;
1019    };
1020    let orientation = if face.same_sense { 1.0 } else { -1.0 };
1021    if areas[outer] * orientation <= 0.0 { return false; }
1022    let real = |index: usize| face.loops[index].coedges.iter().any(|coedge| {
1023        face.loops.iter().flat_map(|rim| &rim.coedges)
1024            .filter(|other| other.edge_id == coedge.edge_id).count() == 1
1025            && !solid.edges.iter().any(|edge| edge.id == coedge.edge_id && edge.degenerate)
1026    });
1027    real(outer) && (0..areas.len()).all(|i| i == outer || !real(i) || areas[i] * orientation <= 0.0)
1028}
1029
1030/// A sphere has no distinguished outer boundary: two disjoint negatively
1031/// oriented trims can both be holes in the remaining sphere. Choosing the
1032/// largest UV polygon as an outer loop falsely rejects that topology, and
1033/// even mistakes an imported retraced pole cut for a runaway hole. Ask the
1034/// same spherical material-side classifier used by containment and meshing:
1035/// each real boundary must lie in the region left by the other loops.
1036fn spherical_hole_breach(solid: &BrepSolid, face: &crate::FaceRecord) -> Option<HoleBreach> {
1037    let mut uses = std::collections::HashMap::new();
1038    for coedge in face.loops.iter().flat_map(|rim| &rim.coedges) {
1039        *uses.entry(coedge.edge_id).or_insert(0usize) += 1;
1040    }
1041    for (index, rim) in face.loops.iter().enumerate() {
1042        let mut other = face.clone();
1043        other.loops.remove(index);
1044        // A loop containing only domain seams and collapsed poles carries
1045        // no material boundary. Keep mixed loops intact: the sampler reads
1046        // each coedge's endpoint from the next coedge's start. Removing a
1047        // paired seam joining two rims would bridge those rims with fake
1048        // chords; the spherical classifier cancels the seam itself.
1049        other.loops.retain(|boundary| {
1050            boundary.coedges.iter().any(|coedge| {
1051                uses[&coedge.edge_id] == 1
1052                    && !solid.edges.iter().any(|edge| {
1053                        edge.id == coedge.edge_id && edge.degenerate
1054                    })
1055            })
1056        });
1057        if other.loops.is_empty() {
1058            continue;
1059        }
1060        let [u0, u1] = face.surface.domain_u().ok()?;
1061        for coedge in &rim.coedges {
1062            // A seam traversed twice and a collapsed pole have no spherical
1063            // area. Their UV trace is not a boundary of the retained region.
1064            if uses[&coedge.edge_id] > 1
1065                || solid
1066                    .edges
1067                    .iter()
1068                    .any(|edge| edge.id == coedge.edge_id && edge.degenerate)
1069            {
1070                continue;
1071            }
1072            let boundary = LoopRecord {
1073                id: rim.id,
1074                coedges: vec![coedge.clone()],
1075            };
1076            let Ok(mut points) = loop_parameter_polyline(&boundary) else {
1077                continue;
1078            };
1079            // Straight UV edges contribute only their start in the ordinary
1080            // polygon sampler. Include an interior witness here: two holes
1081            // can share horizontal boundary levels while their vertical sides
1082            // run through the other hole's interior.
1083            if coedge.pcurve.degree == 1 && coedge.pcurve.control_points.len() == 2 {
1084                let [a, b] = coedge.pcurve.domain().ok()?;
1085                let midpoint = coedge.pcurve.evaluate((a + b) * 0.5).ok()?;
1086                points.push((midpoint.x, midpoint.y));
1087            }
1088            for (u, v) in points {
1089                if crate::parameter_point_in_face(
1090                    &other,
1091                    crate::Vec2 {
1092                        x: u0 + (u - u0).rem_euclid(u1 - u0),
1093                        y: v,
1094                    },
1095                    1e-6,
1096                )
1097                .ok()
1098                    != Some(crate::PolygonClass::Outside)
1099                {
1100                    continue;
1101                }
1102                return Some(HoleBreach {
1103                    face: face.id,
1104                    loop_index: index,
1105                    loop_id: rim.id,
1106                    at: (u, v),
1107                    banded: true,
1108                });
1109            }
1110        }
1111    }
1112    None
1113}
1114
1115/// How many times a loop goes round each CLOSED direction of its carrier, read
1116/// off its coedges' pcurve ENDPOINTS rather than its traced stations: a ring
1117/// the refit writes as one straight pcurve across the whole period traces to a
1118/// single station, and the trace cannot see it go round. An open direction
1119/// winds nothing.
1120fn loop_winding(loop_record: &LoopRecord, closed: (bool, bool), period: (f64, f64)) -> (i32, i32) {
1121    let (mut du, mut dv) = (0.0, 0.0);
1122    for coedge in &loop_record.coedges {
1123        let Ok([d0, d1]) = coedge.pcurve.domain() else { return (0, 0) };
1124        let (Ok(start), Ok(end)) = (coedge.pcurve.evaluate(d0), coedge.pcurve.evaluate(d1)) else {
1125            return (0, 0);
1126        };
1127        du += end.x - start.x;
1128        dv += end.y - start.y;
1129    }
1130    let wind = |closed: bool, net: f64, period: f64| {
1131        if closed && period > 0.0 && net.is_finite() {
1132            (net / period).round() as i32
1133        } else {
1134            0
1135        }
1136    };
1137    (wind(closed.0, du, period.0), wind(closed.1, dv, period.1))
1138}
1139
1140/// Does this loop hold the same edge twice — a SEAM of a closed carrier, both
1141/// sides of it on this one face?
1142fn has_seam_pair(loop_record: &LoopRecord) -> bool {
1143    loop_record.coedges.iter().enumerate().any(|(index, coedge)| {
1144        loop_record.coedges[index + 1..]
1145            .iter()
1146            .any(|other| other.edge_id == coedge.edge_id)
1147    })
1148}
1149
1150/// THE NESTED CASE: no loop winds the carrier, so the loop enclosing the
1151/// largest parameter area is the face's outer boundary and every other loop is
1152/// a hole that must lie inside it. This is the floor exactly as the planar arm
1153/// always ran it, with two additions for a closed carrier: a hole station is
1154/// FOLDED by whole periods into the outer loop's own window where the outer
1155/// loop carries no seam coedge (a fitted pcurve unwraps across the seam on its
1156/// own terms, and a partial patch's outer loop and its hole need not have
1157/// unwrapped the same way), and never folded where it does — then the seam is
1158/// an edge of the outer loop and a hole across it is a wrong body.
1159fn nested_hole_breach(
1160    face: &crate::FaceRecord,
1161    polylines: &[Vec<(f64, f64)>],
1162    fitted: bool,
1163    closed: (bool, bool),
1164    period: (f64, f64),
1165) -> Option<HoleBreach> {
1166    let outer_index = (0..polylines.len())
1167        .max_by(|&a, &b| {
1168            polyline_area(&polylines[a])
1169                .abs()
1170                .total_cmp(&polyline_area(&polylines[b]).abs())
1171        })
1172        .unwrap_or(0);
1173    let outer = &polylines[outer_index];
1174    // Every hole point is asked the same two questions against this one
1175    // loop, so the loop is indexed ONCE and each question then reads the band
1176    // it falls in rather than the whole polyline. The answers are the ones
1177    // the whole-polyline walk gives; see [`BoundaryBands`].
1178    let bands = BoundaryBands::build(outer);
1179    let tolerance = boundary_tolerance(outer);
1180    let seamed = has_seam_pair(&face.loops[outer_index]);
1181    let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
1182    for &(u, v) in outer {
1183        lo = (lo.0.min(u), lo.1.min(v));
1184        hi = (hi.0.max(u), hi.1.max(v));
1185    }
1186    let centre = (0.5 * (lo.0 + hi.0), 0.5 * (lo.1 + hi.1));
1187    let fold_u = closed.0 && !seamed && period.0 > 0.0;
1188    let fold_v = closed.1 && !seamed && period.1 > 0.0;
1189    let fold = |(u, v): (f64, f64)| {
1190        (
1191            if fold_u { u + period.0 * ((centre.0 - u) / period.0).round() } else { u },
1192            if fold_v { v + period.1 * ((centre.1 - v) / period.1).round() } else { v },
1193        )
1194    };
1195    for (index, hole) in polylines.iter().enumerate() {
1196        if index == outer_index {
1197            continue;
1198        }
1199        let Some(at) = hole.iter().map(|&point| fold(point)).find(|&point| {
1200            !bands.contains(point)
1201                && !bands.within(point, tolerance)
1202                && !(fitted && acquitted_in_space(&face.surface, point, bands.nearest(point).0))
1203        }) else {
1204            continue;
1205        };
1206        return Some(HoleBreach {
1207            face: face.id,
1208            loop_index: index,
1209            loop_id: face.loops[index].id,
1210            at,
1211            banded: false,
1212        });
1213    }
1214    None
1215}
1216
1217/// A ring's segments in the parameter plane, one coedge after another with
1218/// every end point included, closed by the whole periods the ring winds — so
1219/// the segment that closes it is the short one back to the start's image, not
1220/// a jump across the domain.
1221struct Ring {
1222    segments: Vec<((f64, f64), (f64, f64))>,
1223    /// Is the face's material on the ring's POSITIVE side along the transverse
1224    /// direction?
1225    material_positive: bool,
1226    positive_winding: bool,
1227}
1228
1229/// THE BANDED CASE: some loop winds the carrier's closed direction, so the
1230/// face has no outer loop in the parameter plane — it is a band between RINGS
1231/// (a refitted full cylinder is bounded by two, a dome cut off a sphere by
1232/// one), and its holes are the loops that do not wind. A hole is in the face
1233/// when it is on every ring's MATERIAL side, decided by a ray along the open
1234/// direction: the ring's winding sign says which side its material is on once
1235/// the face's own orientation convention is read off a hole (a hole loop runs
1236/// with the material on its left when its parameter area is negative — the
1237/// mirror of the outer loop it is a hole in — and on its right otherwise).
1238/// Stations and rings are compared modulo the period, so a ring or a hole
1239/// unwrapped across the seam reads the same as one that was not.
1240///
1241/// Every winding loop must go round exactly once along the same closed axis.
1242/// If the transverse direction is also closed, oriented crossings select the
1243/// material region (including the complementary band); ring windings must
1244/// cancel. Loops winding both axes remain outside this arm.
1245fn banded_hole_breach(
1246    face: &crate::FaceRecord,
1247    polylines: &[Vec<(f64, f64)>],
1248    windings: &[(i32, i32)],
1249    fitted: bool,
1250    closed: (bool, bool),
1251    period: (f64, f64),
1252) -> Option<HoleBreach> {
1253    let rings_wind = |axis: usize| {
1254        windings
1255            .iter()
1256            .filter(|&&winding| winding != (0, 0))
1257            .all(|&(u, v)| {
1258                if axis == 0 {
1259                    u.abs() == 1 && v == 0
1260                } else {
1261                    v.abs() == 1 && u == 0
1262                }
1263            })
1264    };
1265    // `along` is the closed direction the rings wind; `open` names the
1266    // transverse axis (which can itself be periodic).
1267    let along = if rings_wind(0) && closed.0 && period.0 > 0.0 {
1268        0
1269    } else if rings_wind(1) && closed.1 && period.1 > 0.0 {
1270        1
1271    } else {
1272        return None;
1273    };
1274    let open = 1 - along;
1275    let axis = |point: (f64, f64), which: usize| if which == 0 { point.0 } else { point.1 };
1276    let period_along = axis(period, along);
1277    let transverse_period = if if open == 0 { closed.0 } else { closed.1 } {
1278        let p = axis(period, open);
1279        // Boundaries of a region on a torus must cancel in homology. A
1280        // nonseparating single ring does not define a material band.
1281        if p <= 0.0
1282            || windings
1283                .iter()
1284                .map(|&w| if along == 0 { w.0 } else { w.1 })
1285                .sum::<i32>()
1286                != 0
1287        {
1288            return None;
1289        }
1290        Some(p)
1291    } else {
1292        None
1293    };
1294    let domain_start = if along == 0 {
1295        face.surface.domain_u().ok()?[0]
1296    } else {
1297        face.surface.domain_v().ok()?[0]
1298    };
1299
1300    // The orientation convention, read off the first hole with an area.
1301    let left = polylines
1302        .iter()
1303        .zip(windings)
1304        .filter(|(_, &winding)| winding == (0, 0))
1305        .map(|(hole, _)| {
1306            // Area is translation invariant. Subtract a local origin before
1307            // multiplying: period translations must not cancel a small
1308            // hole's area to zero or invert its orientation convention.
1309            let origin = hole[0];
1310            let local: Vec<_> = hole.iter()
1311                .map(|&(u, v)| (u - origin.0, v - origin.1))
1312                .collect();
1313            polyline_area(&local)
1314        })
1315        .find(|area| *area != 0.0)?
1316        < 0.0;
1317
1318    let mut rings: Vec<Ring> = Vec::new();
1319    let mut extent = 0.0f64;
1320    for (index, record) in face.loops.iter().enumerate() {
1321        let winding = windings[index];
1322        if winding == (0, 0) {
1323            continue;
1324        }
1325        let mut points: Vec<(f64, f64)> = Vec::new();
1326        for coedge in &record.coedges {
1327            points.extend(coedge_parameter_polyline(coedge).ok()?);
1328        }
1329        let first = *points.first()?;
1330        points.push((
1331            first.0 + winding.0 as f64 * period.0,
1332            first.1 + winding.1 as f64 * period.1,
1333        ));
1334        for window in points.windows(2) {
1335            extent = extent
1336                .max(axis(window[0], open) - axis(window[1], open))
1337                .max(axis(window[1], open) - axis(window[0], open));
1338        }
1339        let segments = points
1340            .windows(2)
1341            .map(|window| (window[0], window[1]))
1342            .collect();
1343        // Material on the LEFT of a ring winding +u lies at +v; on the left of
1344        // one winding +v it lies at −u.
1345        let positive_winding = axis((winding.0 as f64, winding.1 as f64), along) > 0.0;
1346        let material_positive = if along == 0 {
1347            positive_winding == left
1348        } else {
1349            positive_winding != left
1350        };
1351        rings.push(Ring {
1352            segments,
1353            material_positive,
1354            positive_winding,
1355        });
1356    }
1357    let tolerance = (extent.max(period_along) * 1e-9).max(1e-12);
1358
1359    for (index, hole) in polylines.iter().enumerate() {
1360        if windings[index] != (0, 0) {
1361            continue;
1362        }
1363        for &station in hole {
1364            // Reduced into one period along the rings.
1365            let station = {
1366                let s = axis(station, along);
1367                let reduced = domain_start + (s - domain_start).rem_euclid(period_along);
1368                if along == 0 {
1369                    (reduced, station.1)
1370                } else {
1371                    (station.0, reduced)
1372                }
1373            };
1374            let held = if let Some(transverse) = transverse_period {
1375                periodic_band_holds(&rings, station, along, period_along, transverse)?
1376            } else {
1377                rings
1378                    .iter()
1379                    .all(|ring| ring_holds(ring, station, along, period_along))
1380            };
1381            if held {
1382                continue;
1383            }
1384            let (nearest, distance) =
1385                rings_nearest(&rings, station, along, period_along, transverse_period);
1386            if distance <= tolerance {
1387                continue;
1388            }
1389            if fitted && acquitted_in_space(&face.surface, station, nearest) {
1390                continue;
1391            }
1392            return Some(HoleBreach {
1393                face: face.id,
1394                loop_index: index,
1395                loop_id: face.loops[index].id,
1396                at: station,
1397                banded: true,
1398            });
1399        }
1400    }
1401    None
1402}
1403
1404/// Images relative to the segment midpoint nearest the query, rather than
1405/// the domain origin. Ordinary traced ring segments span at most a period
1406/// and a bit; two images either way cover those even after arbitrary whole
1407/// period translations. This is not a bound for multiply traversed segments.
1408const RING_IMAGES: [i32; 5] = [-2, -1, 0, 1, 2];
1409
1410/// Is `station` on this ring's material side? A ray from the station along
1411/// the OPEN direction, positive way, crosses the ring an odd number of times
1412/// exactly when the ring lies on the positive side of the station — and then
1413/// the station is on the ring's NEGATIVE side, which is the material side
1414/// exactly when the ring's material is not on its positive side.
1415fn ring_holds(ring: &Ring, station: (f64, f64), along: usize, period: f64) -> bool {
1416    let (s_along, s_open) = if along == 0 {
1417        (station.0, station.1)
1418    } else {
1419        (station.1, station.0)
1420    };
1421    let mut crossings = 0usize;
1422    for &(a, b) in &ring.segments {
1423        let (a_along, a_open) = if along == 0 { (a.0, a.1) } else { (a.1, a.0) };
1424        let (b_along, b_open) = if along == 0 { (b.0, b.1) } else { (b.1, b.0) };
1425        for image in RING_IMAGES {
1426            let shift =
1427                (((s_along - 0.5 * (a_along + b_along)) / period).round() + image as f64) * period;
1428            let (a_s, b_s) = (a_along + shift, b_along + shift);
1429            if (a_s > s_along) != (b_s > s_along) {
1430                let at = a_open + (s_along - a_s) * (b_open - a_open) / (b_s - a_s);
1431                if at > s_open {
1432                    crossings += 1;
1433                }
1434            }
1435        }
1436    }
1437    let ring_is_positive = crossings % 2 == 1;
1438    ring_is_positive != ring.material_positive
1439}
1440
1441/// On a doubly periodic chart there is no outside at infinity. The first
1442/// transverse boundary ahead of a station decides its material side. Its
1443/// orientation comes from the same hole-area convention as the open band;
1444/// reversing all loops therefore preserves the region, while reversing just
1445/// the rings chooses its complement. Signed events cancel tangent contacts.
1446fn periodic_band_holds(
1447    rings: &[Ring],
1448    station: (f64, f64),
1449    along: usize,
1450    period: f64,
1451    transverse: f64,
1452) -> Option<bool> {
1453    let (s, t) = if along == 0 {
1454        station
1455    } else {
1456        (station.1, station.0)
1457    };
1458    let mut events = Vec::new();
1459    for ring in rings {
1460        for &(a, b) in &ring.segments {
1461            let (a_s, a_t) = if along == 0 { a } else { (a.1, a.0) };
1462            let (b_s, b_t) = if along == 0 { b } else { (b.1, b.0) };
1463            let centre = ((s - 0.5 * (a_s + b_s)) / period).round();
1464            for image in RING_IMAGES {
1465                let shift = (centre + image as f64) * period;
1466                let (a_image, b_image) = (a_s + shift, b_s + shift);
1467                if (a_image > s) != (b_image > s) {
1468                    // Both segments incident to a tangent vertex must emit
1469                    // the stored vertex value. Re-interpolating an endpoint
1470                    // can move one event by ulps, splitting an exact signed
1471                    // cancellation and falsely rejecting a remote station.
1472                    // Compare in the same shifted chart as the crossing test;
1473                    // no event-distance tolerance or physical band changes.
1474                    let at = if s == a_image {
1475                        a_t
1476                    } else if s == b_image {
1477                        b_t
1478                    } else {
1479                        a_t + (s - a_s - shift) * (b_t - a_t) / (b_s - a_s)
1480                    };
1481                    let positive = if (b_s > a_s) == ring.positive_winding {
1482                        ring.material_positive
1483                    } else {
1484                        !ring.material_positive
1485                    };
1486                    events.push((
1487                        (at - t).rem_euclid(transverse),
1488                        if positive { 1i32 } else { -1 },
1489                    ));
1490                }
1491            }
1492        }
1493    }
1494    events.sort_by(|a, b| a.0.total_cmp(&b.0));
1495    let mut i = 0;
1496    while i < events.len() {
1497        let distance = events[i].0;
1498        let mut sign = 0;
1499        while i < events.len() && events[i].0 == distance {
1500            sign += events[i].1;
1501            i += 1;
1502        }
1503        if sign != 0 {
1504            return Some(sign < 0);
1505        }
1506    }
1507    None
1508}
1509
1510/// The nearest point of any ring to `station`, over the rings' period images,
1511/// and its distance in the parameter plane.
1512fn rings_nearest(
1513    rings: &[Ring],
1514    station: (f64, f64),
1515    along: usize,
1516    period: f64,
1517    transverse: Option<f64>,
1518) -> ((f64, f64), f64) {
1519    let (u, v) = station;
1520    let mut best = (station, f64::INFINITY);
1521    for ring in rings {
1522        for &(a, b) in &ring.segments {
1523            for image in RING_IMAGES {
1524                let midpoint = (0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
1525                let (s, m) = if along == 0 {
1526                    (u, midpoint.0)
1527                } else {
1528                    (v, midpoint.1)
1529                };
1530                let along_shift = (((s - m) / period).round() + image as f64) * period;
1531                for across_image in if transverse.is_some() {
1532                    &RING_IMAGES[..]
1533                } else {
1534                    &RING_IMAGES[2..3]
1535                } {
1536                    let across_shift = transverse
1537                        .map(|p| {
1538                            let (s, m) = if along == 0 {
1539                                (v, midpoint.1)
1540                            } else {
1541                                (u, midpoint.0)
1542                            };
1543                            (((s - m) / p).round() + *across_image as f64) * p
1544                        })
1545                        .unwrap_or(0.0);
1546                    let shift = if along == 0 {
1547                        (along_shift, across_shift)
1548                    } else {
1549                        (across_shift, along_shift)
1550                    };
1551                    let (x0, y0) = (a.0 + shift.0, a.1 + shift.1);
1552                    let (x1, y1) = (b.0 + shift.0, b.1 + shift.1);
1553                    let (dx, dy) = (x1 - x0, y1 - y0);
1554                    let length_squared = dx * dx + dy * dy;
1555                    let t = if length_squared > 0.0 {
1556                        (((u - x0) * dx + (v - y0) * dy) / length_squared).clamp(0.0, 1.0)
1557                    } else {
1558                        0.0
1559                    };
1560                    let candidate = (x0 + t * dx, y0 + t * dy);
1561                    let distance = ((u - candidate.0).powi(2) + (v - candidate.1).powi(2)).sqrt();
1562                    if distance < best.1 {
1563                        best = (candidate, distance);
1564                    }
1565                }
1566            }
1567        }
1568    }
1569    best
1570}
1571
1572/// The "on the boundary" band, measured against the outer loop's own parameter
1573/// extent so it means the same thing on a 1 mm face and on a 1 m one.
1574fn boundary_tolerance(outer: &[(f64, f64)]) -> f64 {
1575    let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
1576    for &(u, v) in outer {
1577        lo = (lo.0.min(u), lo.1.min(v));
1578        hi = (hi.0.max(u), hi.1.max(v));
1579    }
1580    let extent = (hi.0 - lo.0).max(hi.1 - lo.1);
1581    (extent * 1e-9).max(1e-12)
1582}
1583
1584/// How far IN SPACE a station of a fitted hole loop may sit outside its face's
1585/// outer loop and still be read as ON it: ten times the floor the pcurve fit
1586/// refines to ([`crate::PCURVE_REFINEMENT_TOLERANCE`], 1e-7). A tangent mouth's
1587/// fit noise is under the floor; a breach the floor is for is a mouth carried
1588/// out by a motion, orders above it.
1589const CURVED_BREACH_ACQUITTAL: f64 = 10.0 * crate::PCURVE_REFINEMENT_TOLERANCE;
1590
1591/// Is a station that failed parity and the parameter band still within
1592/// [`CURVED_BREACH_ACQUITTAL`] of the face's boundary IN SPACE? Read through
1593/// the face's own surface at the station and at the boundary's nearest
1594/// parameter point (`nearest`), so a fitted pcurve's parameter noise is
1595/// judged by what it amounts to on the carrier. A surface that cannot be
1596/// evaluated acquits nothing.
1597fn acquitted_in_space(
1598    surface: &crate::NurbsSurface,
1599    point: (f64, f64),
1600    nearest: (f64, f64),
1601) -> bool {
1602    // EXTENDED, not clamped: a station past the end of an open direction is
1603    // read where the carrier's own ruled extension puts it, and one past a
1604    // seam where the period wraps it. `evaluate` clamps a parameter into the
1605    // domain, which would put every overshoot back ON the rim and acquit it.
1606    match (
1607        surface.evaluate_extended(point.0, point.1),
1608        surface.evaluate_extended(nearest.0, nearest.1),
1609    ) {
1610        (Ok(here), Ok(there)) => here.sub(there).length() <= CURVED_BREACH_ACQUITTAL,
1611        _ => false,
1612    }
1613}
1614
1615/// The floor's refusal, written off the breach it convicted.
1616fn hole_breach_refusal(solid: &BrepSolid, breach: &HoleBreach, entry: &str) -> KernelRefusal {
1617    let face = solid
1618        .shells
1619        .iter()
1620        .flat_map(|shell| &shell.faces)
1621        .find(|face| face.id == breach.face);
1622    let label = match face.and_then(|face| face.name.as_deref()) {
1623        Some(name) => format!("`{name}` (face {})", breach.face),
1624        None => format!("face {}", breach.face),
1625    };
1626    let (u, v) = breach.at;
1627    let where_in_space = face
1628        .and_then(|face| face.surface.evaluate_extended(u, v).ok())
1629        .map(|point| format!(" — ({:.4}, {:.4}, {:.4}) in space", point.x, point.y, point.z))
1630        .unwrap_or_default();
1631    KernelRefusal::unsound(
1632        KernelStage::Validate,
1633        SoundnessDefect::HoleBreach,
1634        vec![breach.face],
1635        format!(
1636            "{entry}: hole loop {} of {label} has LEFT that face — it runs outside the \
1637             face's own outer boundary, at ({u:.6}, {v:.6}) in the face's parameters{}. \
1638             A hole is a hole IN a face; this one bounds a wall that no longer opens \
1639             through it, so the right body needs the hole to break through a \
1640             neighbouring face, a topology change this operation does not make. \
1641             `validate()` cannot see it (every incidence is intact), the \
1642             self-intersection scan cannot (nothing crosses), and the volume need not \
1643             change at all — refusing rather than returning the wrong body",
1644            breach.loop_id, where_in_space,
1645        ),
1646    )
1647}
1648
1649// ---------------------------------------------------------------------------
1650// The breakout route: a hole that has left its face onto a NAMED neighbour
1651// ---------------------------------------------------------------------------
1652
1653/// The faces a breakout is between: the WALL the convicted hole loop bounds,
1654/// and the ONE neighbour of the breached face the hole has run out onto.
1655struct BreakoutPair {
1656    wall: u64,
1657    neighbour: u64,
1658}
1659
1660/// WHICH neighbour the runaway hole has gone onto — read in the breached
1661/// face's own parameter plane, where the question is a segment crossing and
1662/// not a guess.
1663///
1664/// The hole loop bounds a wall (for a bore, the bore's own cylindrical face):
1665/// exactly one other face answers for its edges, and that is the face whose
1666/// carrier has to be re-trimmed. The hole has left through some stretch of the
1667/// breached face's OUTER loop, and each outer coedge is shared with exactly one
1668/// neighbouring face, so the coedges the hole polyline crosses name the faces
1669/// it has gone out onto.
1670///
1671/// This refuses rather than choosing whenever the answer is not a single pair:
1672/// a hole bounding more than one face, a hole that crosses no outer coedge at
1673/// all (it has left the face's own parameter patch entirely — the runaway), and
1674/// a hole crossing into MORE THAN ONE neighbour, which is a breakout over a
1675/// corner and needs both re-trims resolved together rather than one at a time.
1676fn breakout_pair(solid: &BrepSolid, breach: &HoleBreach) -> Result<BreakoutPair, KernelRefusal> {
1677    let wall = breakout_wall(solid, breach)?;
1678    match breakout_neighbours_crossed(solid, breach, wall)?[..] {
1679        [neighbour] => Ok(BreakoutPair { wall, neighbour }),
1680        [] => Err(KernelRefusal::unsupported(KernelStage::Validate, NOT_A_STRADDLE_WHAT, NOT_A_STRADDLE)),
1681        ref many => Err(KernelRefusal::unsupported(KernelStage::Validate, "breakout.pair", format!(
1682            "the runaway hole crosses into {} neighbouring faces at once ({many:?}) — a \
1683             breakout over a corner, where the two re-trims bound each other and resolving one \
1684             at a time is a different problem from resolving both",
1685            many.len(),
1686        ))),
1687    }
1688}
1689
1690/// The marker [`breakout_pair`] returns when the hole does not STRADDLE its
1691/// face's boundary. It has either migrated wholly onto another face or left the
1692/// body; [`repair_hole_breakout`] tells those apart, by trying.
1693const NOT_A_STRADDLE: &str = "the hole does not straddle its own face's boundary";
1694
1695/// The slug [`breakout_pair`] mints [`NOT_A_STRADDLE`] under
1696/// (`UnsupportedGeometry`), shared with [`is_not_a_straddle`].
1697const NOT_A_STRADDLE_WHAT: &str = "breakout.not_a_straddle";
1698
1699/// Is this [`breakout_pair`]'s non-straddle answer? Read off the class and
1700/// slug, so the route's decision does not follow the sentence.
1701fn is_not_a_straddle(refusal: &KernelRefusal) -> bool {
1702    matches!(
1703        &refusal.class,
1704        crate::RefusalClass::UnsupportedGeometry { what } if what == NOT_A_STRADDLE_WHAT
1705    )
1706}
1707
1708/// The face the convicted hole loop BOUNDS — the moved carrier, for a bore the
1709/// bore's own wall. Exactly one other face may answer for its edges.
1710fn breakout_wall(solid: &BrepSolid, breach: &HoleBreach) -> Result<u64, KernelRefusal> {
1711    let face = face_by_id(solid, breach.face)?;
1712    let mut walls: Vec<u64> = Vec::new();
1713    for coedge in &face.loops[breach.loop_index].coedges {
1714        for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
1715            if other.id == face.id {
1716                continue;
1717            }
1718            if other
1719                .loops
1720                .iter()
1721                .any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
1722                && !walls.contains(&other.id)
1723            {
1724                walls.push(other.id);
1725            }
1726        }
1727    }
1728    match walls[..] {
1729        [wall] => Ok(wall),
1730        _ => Err(KernelRefusal::unsupported(KernelStage::Validate, "breakout.wall_count", format!(
1731            "the runaway hole loop bounds {} face(s) ({walls:?}); this route re-trims ONE moved \
1732             carrier against the body and cannot decide which of several is the one that moved",
1733            walls.len(),
1734        ))),
1735    }
1736}
1737
1738fn face_by_id<'a>(
1739    solid: &'a BrepSolid,
1740    id: u64,
1741) -> Result<&'a crate::FaceRecord, KernelRefusal> {
1742    solid
1743        .shells
1744        .iter()
1745        .flat_map(|shell| &shell.faces)
1746        .find(|face| face.id == id)
1747        .ok_or_else(|| {
1748            KernelRefusal::internal(KernelStage::Validate, "face_by_id", format!("face {id} is not on the body"))
1749        })
1750}
1751
1752/// The neighbours the hole has run out ONTO, read as segment crossings in the
1753/// breached face's own parameter plane.
1754///
1755/// This answers the STRADDLE: part of the mouth still inside the face it was
1756/// recorded on, part outside. It returns nothing for a mouth that has migrated
1757/// wholly onto another face — such a loop is entirely outside the outer loop
1758/// and crosses none of its coedges — and nothing for one that has left the body
1759/// altogether. Those two are told apart downstream, by geometry, in
1760/// [`repair_hole_breakout`].
1761fn breakout_neighbours_crossed(
1762    solid: &BrepSolid,
1763    breach: &HoleBreach,
1764    wall: u64,
1765) -> Result<Vec<u64>, KernelRefusal> {
1766    let face = face_by_id(solid, breach.face)?;
1767    let polylines = face
1768        .loops
1769        .iter()
1770        .map(loop_parameter_polyline)
1771        .collect::<Result<Vec<_>, KernelRefusal>>()
1772        .map_err(|error| {
1773            error.with_message(|error| format!("the breached face's loops could not be traced: {error}"))
1774        })?;
1775    let outer_index = (0..polylines.len())
1776        .max_by(|&a, &b| {
1777            polyline_area(&polylines[a])
1778                .abs()
1779                .total_cmp(&polyline_area(&polylines[b]).abs())
1780        })
1781        .unwrap_or(0);
1782    if outer_index == breach.loop_index {
1783        return Err(KernelRefusal::internal(KernelStage::Validate, "breakout.outer_loop", "the convicted loop is the face's own outer loop"));
1784    }
1785    let hole = &polylines[breach.loop_index];
1786
1787    let mut neighbours: Vec<u64> = Vec::new();
1788    for coedge in &face.loops[outer_index].coedges {
1789        let side = coedge_parameter_polyline(coedge)
1790            .map_err(|error| error.with_message(|error| format!("an outer coedge could not be traced: {error}")))?;
1791        if !polylines_cross(hole, &side) {
1792            continue;
1793        }
1794        for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
1795            if other.id == face.id || other.id == wall {
1796                continue;
1797            }
1798            if other
1799                .loops
1800                .iter()
1801                .any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
1802                && !neighbours.contains(&other.id)
1803            {
1804                neighbours.push(other.id);
1805            }
1806        }
1807    }
1808    Ok(neighbours)
1809}
1810
1811/// Every face across the breached face's OUTER boundary — the faces a mouth
1812/// leaving that face can have arrived on.
1813///
1814/// A migration is a mouth that has walked off one face and onto the next. The
1815/// next one is over an edge, so this is the candidate set, and it is small: the
1816/// outer loop's own neighbours, the wall excepted. A carrier that has reached
1817/// something further away than that is not a case this route claims, and it
1818/// says so rather than widening until it finds a face it can cut.
1819fn faces_over_the_outer_boundary(
1820    solid: &BrepSolid,
1821    breach: &HoleBreach,
1822    wall: u64,
1823) -> Result<Vec<u64>, KernelRefusal> {
1824    let face = face_by_id(solid, breach.face)?;
1825    let polylines = face
1826        .loops
1827        .iter()
1828        .map(loop_parameter_polyline)
1829        .collect::<Result<Vec<_>, KernelRefusal>>()
1830        .unwrap_or_default();
1831    let outer_index = (0..polylines.len())
1832        .max_by(|&a, &b| {
1833            polyline_area(&polylines[a])
1834                .abs()
1835                .total_cmp(&polyline_area(&polylines[b]).abs())
1836        })
1837        .unwrap_or(0);
1838    let mut over: Vec<u64> = Vec::new();
1839    for coedge in &face.loops[outer_index].coedges {
1840        for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
1841            if other.id == face.id || other.id == wall {
1842                continue;
1843            }
1844            if other
1845                .loops
1846                .iter()
1847                .any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
1848                && !over.contains(&other.id)
1849            {
1850                over.push(other.id);
1851            }
1852        }
1853    }
1854    Ok(over)
1855}
1856
1857/// One coedge as a polyline in its face's parameters, END POINT INCLUDED, so
1858/// the segments it yields are the whole of that coedge.
1859///
1860/// [`loop_parameter_polyline`] deliberately leaves each coedge's end point to
1861/// the next coedge's start; a single coedge asked about on its own has no next.
1862fn coedge_parameter_polyline(
1863    coedge: &crate::topology::CoedgeRecord,
1864) -> Result<Vec<(f64, f64)>, KernelRefusal> {
1865    let pcurve = &coedge.pcurve;
1866    let [d0, d1] = pcurve.domain().or_refuse(KernelStage::Validate, "domain")?;
1867    let steps = if pcurve.degree <= 1 && pcurve.control_points.len() == 2 {
1868        1
1869    } else {
1870        let mut spans = 0usize;
1871        for window in pcurve.knots.windows(2) {
1872            if window[1] > window[0] {
1873                spans += 1;
1874            }
1875        }
1876        spans.max(1) * LOOP_SAMPLES_PER_SPAN
1877    };
1878    let mut points = Vec::with_capacity(steps + 1);
1879    for step in 0..=steps {
1880        let uv = pcurve.evaluate(d0 + (d1 - d0) * (step as f64 / steps as f64)).or_refuse(KernelStage::Validate, "evaluate")?;
1881        points.push((uv.x, uv.y));
1882    }
1883    Ok(points)
1884}
1885
1886/// Does the CLOSED polyline `loop_points` cross the OPEN polyline `side`?
1887/// Proper segment intersection, in the face's own parameters.
1888fn polylines_cross(loop_points: &[(f64, f64)], side: &[(f64, f64)]) -> bool {
1889    for index in 0..loop_points.len() {
1890        let a = loop_points[index];
1891        let b = loop_points[(index + 1) % loop_points.len()];
1892        for window in side.windows(2) {
1893            if segments_cross(a, b, window[0], window[1]) {
1894                return true;
1895            }
1896        }
1897    }
1898    false
1899}
1900
1901fn segments_cross(a: (f64, f64), b: (f64, f64), c: (f64, f64), d: (f64, f64)) -> bool {
1902    let orient = |p: (f64, f64), q: (f64, f64), r: (f64, f64)| {
1903        (q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0)
1904    };
1905    let (d1, d2) = (orient(c, d, a), orient(c, d, b));
1906    let (d3, d4) = (orient(a, b, c), orient(a, b, d));
1907    ((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0))
1908}
1909
1910/// THE BREAKOUT, reached from the containment floor instead of from the scan.
1911///
1912/// The floor convicts a hole loop that has left the face it is recorded on,
1913/// planar or curved. Where that hole has left ONTO a named neighbour — the case the operator
1914/// reported, an exit hole dragged across the edge between two faces — the right
1915/// body is the one the self-crossing repair already builds for a bore carried
1916/// out through a side wall: the moved carrier and the neighbour split along
1917/// their exact section, the pieces that ended up on the wrong side dropped, and
1918/// the faces left holding an unanswered boundary RE-TRIMMED, which merges the
1919/// mouth into the outer loops of the faces it now opens through.
1920///
1921/// What makes it the SAME repair and not a second one is that the extent is
1922/// still derived and not invented: the tool is the moved carrier trimmed
1923/// against the body, and where it meets the body is what the imprint computes.
1924/// The route in is what changes — the scan needs a mesh crossing it can
1925/// confirm, and at the near edge of a breakout the crossing is a sliver the
1926/// tessellation does not resolve, so the floor sees it first.
1927///
1928/// The genuine runaway is excluded by GEOMETRY and not by a threshold. A bore
1929/// carried clean off the body crosses no boundary of the face it left, so
1930/// [`breakout_pair`] finds no neighbour and this refuses before a single face
1931/// is split; and even given a pair, the imprint of a carrier that misses the
1932/// body cuts neither trim and [`split_and_drop`] declines on its own terms.
1933fn repair_hole_breakout(solid: &BrepSolid, breach: &HoleBreach) -> Result<BrepSolid, KernelRefusal> {
1934    let origin = || RepairOrigin::Breakout {
1935        face: breach.face,
1936        loop_id: breach.loop_id,
1937    };
1938    if breach.banded {
1939        return Err(KernelRefusal::unsupported(
1940            KernelStage::Validate,
1941            "breakout.band_face",
1942            format!(
1943                "face {} is a band bounded by rings that wind its carrier's period, with no outer \
1944                 loop in its parameter plane; this route re-trims a hole against the outer loop \
1945                 it straddles and does not re-trim a band",
1946                breach.face,
1947            ),
1948        ));
1949    }
1950    // THE STRADDLE: part of the mouth still inside the face it was recorded on,
1951    // part outside. Which neighbour is read off the crossing, exactly.
1952    match breakout_pair(solid, breach) {
1953        Ok(BreakoutPair { wall, neighbour }) => {
1954            return split_and_drop(solid, wall, neighbour, origin())
1955        }
1956        // Every refusal but the non-straddle is this route's own, classed as it
1957        // always was here; the non-straddle is read off its slug, not its text.
1958        Err(reason) if !is_not_a_straddle(&reason) => {
1959            return Err(KernelRefusal::unsupported(
1960                KernelStage::Validate,
1961                "breakout.pair",
1962                reason.message,
1963            ))
1964        }
1965        Err(_) => {}
1966    }
1967
1968    // THE MIGRATION, or the RUNAWAY. The mouth is wholly outside the face it
1969    // was recorded on, so nothing in that face's own parameter plane can say
1970    // where it went — a mouth that has walked onto the next face and a mouth
1971    // that has left the solid look the same from there.
1972    //
1973    // They do not look the same to the IMPRINT. The question that separates
1974    // them is whether the moved carrier still meets a face of the body, and
1975    // `split_and_drop` answers exactly that: handed a face the carrier misses
1976    // it declines with "the carriers' intersection reaches ... neither", before
1977    // anything is dropped. So the candidates are tried, and the answer is
1978    // decided BY THE RESULT — the same principle the drop enumeration inside
1979    // already runs on, and a discriminator that cannot drift, because a carrier
1980    // clear of the body cuts nothing anywhere.
1981    //
1982    // More than one candidate building is not an answer: the mouth would be on
1983    // two faces at once with no reason to prefer either, and choosing would be
1984    // the guess this module exists not to make.
1985    let wall = breakout_wall(solid, breach).map_err(|refusal| {
1986        KernelRefusal::internal(KernelStage::Validate, "breakout.wall", refusal.message)
1987    })?;
1988    let mut candidates = faces_over_the_outer_boundary(solid, breach, wall)
1989        .map_err(|refusal| {
1990            KernelRefusal::internal(KernelStage::Validate, "breakout.candidates", refusal.message)
1991        })?;
1992    if candidates.is_empty() {
1993        return Err(KernelRefusal::unsupported(
1994            KernelStage::Validate,
1995            "breakout.runaway",
1996            RUNAWAY,
1997        ));
1998    }
1999
2000    // WHICH face the mouth moved onto, when its own face's parameter plane can
2001    // no longer say. The carrier is a whole surface and generally meets the
2002    // body in more than one place — a through bore leaves by one face and
2003    // enters by another, and both are faces this carrier can be re-trimmed
2004    // against — so "some face it cuts" is not the answer. The answer is the
2005    // face the MIGRATED MOUTH IS AT: the candidates are ordered by the least
2006    // 3D distance from the convicted hole loop to the face's own trim, and the
2007    // nearest one that the repair can actually build is taken.
2008    //
2009    // A TIE is refused rather than broken. Two faces the same distance from the
2010    // mouth means the mouth sits on the edge between them, which is a corner
2011    // breakout — the case whose two re-trims bound each other.
2012    let mouth = hole_loop_in_space(solid, breach).map_err(|refusal| {
2013        KernelRefusal::internal(KernelStage::Validate, "breakout.mouth", refusal.message)
2014    })?;
2015    let scale = crate::solid_scale(solid);
2016    let mut ranked: Vec<(f64, u64)> = Vec::new();
2017    for candidate in candidates.drain(..) {
2018        ranked.push((
2019            distance_to_face(solid, candidate, &mouth)
2020                .map_err(|refusal| {
2021                    KernelRefusal::internal(KernelStage::Validate, "breakout.distance", refusal.message)
2022                })?,
2023            candidate,
2024        ));
2025    }
2026    ranked.sort_by(|a, b| a.0.total_cmp(&b.0));
2027    if ranked.len() > 1 && (ranked[1].0 - ranked[0].0).abs() <= scale * 1e-9 {
2028        return Err(KernelRefusal::ill_posed(
2029            KernelStage::Validate,
2030            "breakout.corner_tie",
2031            format!(
2032                "the migrated mouth is the same distance ({:.6}) from faces {} and {}, so the body \
2033                 does not say which of them it moved onto — a breakout over the corner between \
2034                 them, where the two re-trims bound each other",
2035                ranked[0].0, ranked[0].1, ranked[1].1,
2036            ),
2037        ));
2038    }
2039
2040    let carried = take_crossing_repairs();
2041    let mut declined: Vec<String> = Vec::new();
2042    for (distance, candidate) in ranked {
2043        match split_and_drop(solid, wall, candidate, origin()) {
2044            Ok(body) => {
2045                // The trial that built pushed a record; the ones that declined
2046                // pushed none. Keep that one and put the caller's ledger back.
2047                let mine = take_crossing_repairs();
2048                restore_repairs(carried.into_iter().chain(mine).collect());
2049                return Ok(body);
2050            }
2051            Err(reason) => {
2052                declined.push(format!("face {candidate} at {distance:.6}: {reason}"))
2053            }
2054        }
2055    }
2056    restore_repairs(carried);
2057    Err(KernelRefusal::unsupported(
2058        KernelStage::Validate,
2059        "breakout.runaway",
2060        format!(
2061            "{RUNAWAY} — the moved carrier was offered every face across that boundary, nearest \
2062             first, and cut none of them ({})",
2063            declined.join(" | ")
2064        ),
2065    ))
2066}
2067
2068/// The convicted hole loop as 3D points: its pcurves are in the breached face's
2069/// own parameters, so they are read back through that face's surface — the same
2070/// road the floor takes, and the reason the floor was never wrong about where a
2071/// loop is.
2072fn hole_loop_in_space(solid: &BrepSolid, breach: &HoleBreach) -> Result<Vec<crate::Vec3>, KernelRefusal> {
2073    let face = face_by_id(solid, breach.face)?;
2074    let uv = loop_parameter_polyline(&face.loops[breach.loop_index])?;
2075    uv.into_iter()
2076        .map(|(u, v)| face.surface.evaluate(u, v))
2077        .collect::<Result<Vec<_>, _>>()
2078        .map_err(|error| format!("the convicted hole loop could not be read in space: {error}"))
2079        .or_refuse(KernelStage::Validate, "evaluate")
2080}
2081
2082/// The least distance from a set of points to a face's own TRIM — sampled, in
2083/// the face's parameters, so a point beside a face's carrier but off the end of
2084/// the face does not read as being on it.
2085fn distance_to_face(
2086    solid: &BrepSolid,
2087    face_id: u64,
2088    points: &[crate::Vec3],
2089) -> Result<f64, KernelRefusal> {
2090    const STATIONS: usize = 32;
2091    let face = face_by_id(solid, face_id)?;
2092    let polylines = face
2093        .loops
2094        .iter()
2095        .map(loop_parameter_polyline)
2096        .collect::<Result<Vec<_>, KernelRefusal>>()?;
2097    let outer_index = (0..polylines.len())
2098        .max_by(|&a, &b| {
2099            polyline_area(&polylines[a])
2100                .abs()
2101                .total_cmp(&polyline_area(&polylines[b]).abs())
2102        })
2103        .unwrap_or(0);
2104    let bands = BoundaryBands::build(&polylines[outer_index]);
2105    let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
2106    for &(u, v) in &polylines[outer_index] {
2107        lo = (lo.0.min(u), lo.1.min(v));
2108        hi = (hi.0.max(u), hi.1.max(v));
2109    }
2110    let mut best = f64::INFINITY;
2111    for iu in 0..=STATIONS {
2112        for iv in 0..=STATIONS {
2113            let u = lo.0 + (hi.0 - lo.0) * iu as f64 / STATIONS as f64;
2114            let v = lo.1 + (hi.1 - lo.1) * iv as f64 / STATIONS as f64;
2115            if !bands.contains((u, v)) {
2116                continue;
2117            }
2118            if polylines
2119                .iter()
2120                .enumerate()
2121                .any(|(index, hole)| index != outer_index && inside_polyline((u, v), hole))
2122            {
2123                continue;
2124            }
2125            let Ok(point) = face.surface.evaluate(u, v) else {
2126                continue;
2127            };
2128            for probe in points {
2129                best = best.min(probe.sub(point).length());
2130            }
2131        }
2132    }
2133    Ok(best)
2134}
2135
2136/// Even-odd parity against a closed polyline, for the hole loops the band index
2137/// is not built over.
2138fn inside_polyline(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
2139    let (u, v) = point;
2140    let mut parity = false;
2141    for index in 0..polygon.len() {
2142        let (ax, ay) = polygon[index];
2143        let (bx, by) = polygon[(index + 1) % polygon.len()];
2144        if (ay > v) != (by > v) {
2145            let x = ax + (v - ay) / (by - ay) * (bx - ax);
2146            if x > u {
2147                parity = !parity;
2148            }
2149        }
2150    }
2151    parity
2152}
2153
2154/// The sentence a hole that has left the BODY earns, as opposed to one that has
2155/// moved onto another face. It is the one thing standing between the operator
2156/// and a silently ruined model, so it is a constant and not a phrasing.
2157const RUNAWAY: &str = "the runaway hole has left the face it was recorded on without arriving \
2158     on another: the moved carrier meets no face of the body, so there is nothing for it to be \
2159     re-trimmed against and no cut to perform at any extent";
2160
2161/// [`resolve_face_crossing`] for a pair this module has named itself rather
2162/// than read off the scan.
2163///
2164/// The scan is one way to find two faces of one body that have to be split
2165/// along their section, and the containment floor is another. Both hand the
2166/// same pair to the same repair.
2167pub fn resolve_face_crossing_between(
2168    solid: &BrepSolid,
2169    face_a: u64,
2170    face_b: u64,
2171) -> Result<BrepSolid, KernelRefusal> {
2172    let report = scan(solid, "resolve_face_crossing_between")?;
2173    split_and_drop(solid, face_a, face_b, RepairOrigin::Scan(&report))
2174}
2175
2176// ---------------------------------------------------------------------------
2177// The repair
2178// ---------------------------------------------------------------------------
2179
2180/// WHAT FOUND the pair this repair is about to split.
2181///
2182/// Two floors reach the same repair. The scan finds two faces whose meshes
2183/// cross and can say where and by how much; the hole-containment floor finds a
2184/// hole loop that has left its face and can say which loop and which face. The
2185/// repair does the same work either way, and the ledger says which asked for
2186/// it rather than printing a crossing point that was never measured.
2187enum RepairOrigin<'a> {
2188    /// The self-intersection scan confirmed a crossing pair.
2189    Scan(&'a SelfIntersectionReport),
2190    /// The containment floor convicted a hole loop that has broken out onto a
2191    /// neighbouring face.
2192    Breakout { face: u64, loop_id: u64 },
2193}
2194
2195/// The operand tag this repair works under. The solid is its own and only
2196/// operand: what is being resolved is a body crossing ITSELF, so there is no
2197/// second operand to be the other side of.
2198const SELF_OPERAND: u8 = 0;
2199
2200/// How many pieces the crossing may cut the two faces into before this repair
2201/// declines. Every way of dropping some of them is REBUILT and re-checked, so
2202/// the enumeration has to stay small enough to run in full: at eight pieces
2203/// there are at most 225 candidate drops, nearly all of which the re-trim
2204/// closure ends before anything is assembled. The variable-radius fillet's
2205/// crossing cuts four pieces and the two-bore fixture's cuts six.
2206const MAXIMUM_CROSSING_PIECES: usize = 8;
2207
2208/// Split two faces of ONE body that cross each other along their carriers'
2209/// exact intersection, drop the material the crossing encloses, and re-sew.
2210///
2211/// This is the bounded half of the self-intersection answer. Where two walls of
2212/// one body overshoot past each other instead of meeting at an edge, the
2213/// crossing curve IS the edge they should have met at: imprinting it onto both
2214/// faces cuts each into the part that belongs and the tail that overshot, and
2215/// dropping both tails leaves a body sewn along that curve.
2216///
2217/// **What it refuses, and why each refusal is a bound rather than a shrug:**
2218///
2219/// * A FOLD inside one face. There is no second face to imprint against, and
2220///   the answer is a trim that was never constructed rather than material to
2221///   drop — `offset/carve.rs` owns it.
2222/// * More than one crossing pair. Each repair changes the faces the next one
2223///   would be measured against, so resolving them together is a different
2224///   problem from resolving one.
2225/// * A crossing whose carriers' intersection does not cut BOTH trims. Then the
2226///   mesh crossing is not a curve the faces share and there is nothing to split
2227///   along.
2228/// * More pieces than [`MAXIMUM_CROSSING_PIECES`], because every candidate drop
2229///   is rebuilt in full and the enumeration has to run to the end rather than
2230///   be sampled.
2231/// * Any split whose reassembly does not come back SOUND. Every way of dropping
2232///   some pieces of each face — keeping at least one of each — is enumerated,
2233///   closed under the re-trim, rebuilt and re-checked in full (`validate()`
2234///   clean, connected, and scanning clean), so the piece choice is decided by
2235///   the result rather than guessed from a normal. If none verifies, the solid
2236///   is returned untouched and the caller refuses: a repair that cannot prove
2237///   its own answer must not ship one.
2238/// * A split that MORE THAN ONE drop resolves. Two sound answers is not an
2239///   answer, and choosing between them would be the guess this repair exists
2240///   not to make.
2241///
2242/// **The re-trim is the other half of the split.** Dropping a tail also removes
2243/// that tail's boundary uses on the NEIGHBOURING faces it bordered, and a body
2244/// cannot close over an edge only one face answers for. So the drop is closed
2245/// under "a face left holding an unmatched boundary goes too" — which, since
2246/// the imprint already cut those neighbours where the crossing curve met their
2247/// edges, drops the part of each that the crossing cut off. The closure is
2248/// bounded by the faces the imprint TOUCHED: reaching any other face ends that
2249/// candidate. Nothing here edits a loop by hand; every piece it chooses between
2250/// came out of the imprint/fragment machinery.
2251///
2252/// **And the drop RETIRES what the old configuration left.** The touched bound
2253/// is about material the crossing encloses, and it does not reach the third
2254/// thing a moved carrier leaves on a body: a piece, or a whole loop, that the
2255/// NEW configuration has no room for — a bore's exit ring still recorded on the
2256/// top face after the exit has migrated onto the side. Neither is material this
2257/// drop ran past; both are held by nothing the drop keeps, and keeping either
2258/// cannot close. [`fragment_is_detached`] retires the one the fragmenter
2259/// returns as its own island and [`holds_only_stale_loops`] the one it returns
2260/// as a hole loop that has left its face, and the ledger says a repair retired
2261/// something rather than leaving it to be read off the cell counts.
2262///
2263/// **A census has two directions and only one was enforced.** An edge used ONCE
2264/// is a face left holding an unanswered boundary, above. An edge used THREE
2265/// times is three faces along one curve, which is not a body either, and the
2266/// assembly does not refuse it — it puts the third face on a shell of its own
2267/// and every floor in this repository then passes the result.
2268/// [`over_used_edge`] refuses that candidate where the third use still exists,
2269/// which is here. It is what stopped the turned-bore migration returning a body
2270/// 23% out on area.
2271pub fn resolve_face_crossing(solid: &BrepSolid) -> Result<BrepSolid, KernelRefusal> {
2272    let options = SelfIntersectionOptions::for_solid(solid);
2273    let report = solid_self_intersections(solid, options).map_err(|message| {
2274        KernelRefusal::internal(
2275            KernelStage::Validate,
2276            "soundness_scan",
2277            format!("the soundness scan could not run: {message}"),
2278        )
2279    })?;
2280    if let Some(fold) = report.folds.first() {
2281        return Err(KernelRefusal::unsound(
2282            KernelStage::Validate,
2283            SoundnessDefect::Fold,
2284            report.folds.iter().map(|fold| fold.face).collect(),
2285            format!(
2286                "face {} folds through ITSELF; a fold is a trim that was never constructed and \
2287                 belongs to the carrier carve, not to this repair",
2288                fold.face
2289            ),
2290        ));
2291    }
2292    if report.confirmed.is_empty() {
2293        return Ok(solid.clone());
2294    }
2295    if report.confirmed.len() > 1 {
2296        return Err(KernelRefusal::unsupported(
2297            KernelStage::Validate,
2298            "crossing.multiple_pairs",
2299            format!(
2300                "{} crossing face pairs; each repair changes the faces the next would be measured \
2301                 against, so this repair takes one pair at a time",
2302                report.confirmed.len()
2303            ),
2304        ));
2305    }
2306    let crossing = &report.confirmed[0];
2307    split_and_drop(solid, crossing.face_a, crossing.face_b, RepairOrigin::Scan(&report))
2308}
2309
2310/// One crossing pair, split and resolved. Separated from the scan so the
2311/// refusals above read as one list.
2312fn split_and_drop(
2313    solid: &BrepSolid,
2314    face_a: u64,
2315    face_b: u64,
2316    origin: RepairOrigin<'_>,
2317) -> Result<BrepSolid, KernelRefusal> {
2318    use crate::fragment::fragment_solid;
2319    use crate::imprint::{FaceKey, ImprintOptions};
2320    use crate::{apply_edge_splits, build_imprints};
2321
2322    let scale = crate::solid_scale(solid);
2323    let tolerance = (scale * 1e-7).max(1e-9);
2324    if trace_on() {
2325        eprintln!("HEAL split_and_drop: faces {face_a} | {face_b}, scale {scale:.6}");
2326        trace_solid("entry solid", solid);
2327    }
2328
2329    // Each face as its own one-face open body, which is what the imprint
2330    // driver pairs: it walks operand A's faces against operand B's, and a
2331    // solid paired with itself would pair every face with itself first.
2332    let open_a = one_face_body(solid, face_a)?;
2333    let open_b = one_face_body(solid, face_b)?;
2334    let mut request = ImprintOptions {
2335        tolerance,
2336        // The GLOBAL interpolation lane: the section the repair cuts along is
2337        // the faithful one. On the only row this repair takes
2338        // (`FilletFailureWith2RasiusValues`) the global lane's section lies
2339        // 1.6e-6 from its two carriers where the local lane's lies 7.8e-4, and
2340        // it moves every face the section bounds onto its closed form (`Pin_S`
2341        // 1.6e-4 -> 4.4e-7). The local lane was asked for until
2342        // `process_curve` split an edge a section ENDS on whatever the angle:
2343        // that section is tangent to the bore's mouth ring at both ends, the
2344        // global fit reproduces the tangency, and the old gates left the ring
2345        // whole, so the repair succeeded only on the local fit's 1.4-degree
2346        // end-tangent error. `BREP_HEAL_FIT_LANE` re-measures it.
2347        local_fit: false,
2348        // The station budget is the imprint's own (the ladder climbs it to
2349        // twice that). `96` was `offset_shell`'s: every section this repair
2350        // reaches is a sparse march (68 points on the fillet row, 7 and 8 on
2351        // `BadBoolean`) that saturates either budget, bit-identically.
2352        ..ImprintOptions::default()
2353    };
2354    measured_fit_request(&mut request);
2355    let mut imprint = build_imprints(&open_a, &open_b, &request).map_err(|error| {
2356        error.with_message(|error| format!("the two carriers could not be imprinted: {error}"))
2357    })?;
2358
2359    // Both faces belong to ONE body, so both sides of the imprint are rewritten
2360    // onto that body's operand. The face ids are the solid's own and cannot
2361    // collide, so this is a relabel and not a merge.
2362    for face in &mut imprint.by_face {
2363        face.operand = SELF_OPERAND;
2364    }
2365    for split in &mut imprint.edge_splits {
2366        split.operand = SELF_OPERAND;
2367    }
2368    for piece in &mut imprint.pieces {
2369        for support in &mut piece.support_faces {
2370            *support = FaceKey {
2371                operand: SELF_OPERAND,
2372                face_id: support.face_id,
2373            };
2374        }
2375        for pcurve in &mut piece.pcurves {
2376            pcurve.operand = SELF_OPERAND;
2377        }
2378    }
2379    for barrier in &mut imprint.barrier_edges {
2380        barrier.0 = SELF_OPERAND;
2381    }
2382
2383    if trace_on() {
2384        trace_imprint(&imprint);
2385    }
2386
2387    let cut_a = imprint
2388        .by_face
2389        .iter()
2390        .any(|face| face.face_id == face_a && !face.piece_ids.is_empty());
2391    let cut_b = imprint
2392        .by_face
2393        .iter()
2394        .any(|face| face.face_id == face_b && !face.piece_ids.is_empty());
2395    if !cut_a || !cut_b {
2396        return Err(KernelRefusal::unsupported(
2397            KernelStage::Fragment,
2398            "crossing.no_shared_curve",
2399            format!(
2400                "the carriers' intersection reaches {} of the two trims, so there is no shared \
2401                 curve to split along",
2402                match (cut_a, cut_b) {
2403                    (true, false) => format!("only face {face_a}"),
2404                    (false, true) => format!("only face {face_b}"),
2405                    _ => "neither".into(),
2406                }
2407            ),
2408        ));
2409    }
2410
2411    let split = apply_edge_splits(solid, SELF_OPERAND, &imprint).map_err(|error| {
2412        error.with_message(|error| format!("the imprinted edge splits could not be applied: {error}"))
2413    })?;
2414    let fragments = fragment_solid(&split, SELF_OPERAND, &imprint).map_err(|error| {
2415        error.with_message(|error| format!("the split faces could not be fragmented: {error}"))
2416    })?;
2417
2418    if trace_on() {
2419        trace_solid("split solid", &split);
2420        trace_fragments(&fragments);
2421    }
2422
2423    // The faces the imprint TOUCHED: the two that cross, plus every face whose
2424    // own loop carries an edge the imprint split. Those are exactly the faces
2425    // the dropped material can border — the crossing curve ends ON a boundary
2426    // edge of each crossing face, and that edge is shared with a neighbour — so
2427    // they are the only faces this repair may re-trim. Anything beyond them is
2428    // material this repair has not been shown to own, and reaching one ends the
2429    // candidate rather than widening the drop.
2430    let touched = touched_faces(solid, &imprint, face_a, face_b);
2431    // A POLE edge is legitimately used once — it is a collapsed boundary, not a
2432    // shared one — so it never means the face beside it bordered dropped
2433    // material. Left in the census it would drop the fragment holding it, and
2434    // the cascade would refuse a body the repair could have resolved.
2435    let degenerate: rustc_hash::FxHashSet<EdgeKey> = split
2436        .edges
2437        .iter()
2438        .filter(|edge| edge.degenerate)
2439        .map(|edge| EdgeKey::Boundary(SELF_OPERAND, edge.id))
2440        .collect();
2441
2442    let pieces_of = |face: u64| -> Vec<usize> {
2443        fragments
2444            .iter()
2445            .enumerate()
2446            .filter_map(|(index, fragment)| (fragment.source_face_id == face).then_some(index))
2447            .collect()
2448    };
2449    let pieces_a = pieces_of(face_a);
2450    let pieces_b = pieces_of(face_b);
2451    // How many pieces the section has to cut, and it is not the same question
2452    // on both roads.
2453    //
2454    // From the SCAN, two faces overshoot PAST each other and the crossing curve
2455    // is the edge they should have met at, so it runs across both trims and
2456    // each has a tail: both sides must come out in two or more pieces, and one
2457    // piece is a section that grazed a trim rather than cut it.
2458    //
2459    // From the containment floor, the shape is different and the difference is
2460    // the whole of a COMPLETED MIGRATION. When a bore's mouth has walked
2461    // entirely onto the next face, the moved carrier meets that face in a
2462    // CLOSED loop inside its trim — the new mouth — which cuts nothing off it;
2463    // the face comes back as one piece carrying a new hole, and the only tail
2464    // is the wall's. Demanding two pieces of both would refuse exactly the case
2465    // the operator asked for, so this road asks for two pieces on ONE side and
2466    // at least one on the other. Nothing about the enumeration changes: a face
2467    // with one piece has one drop set, the empty one.
2468    let (need_a, need_b) = match &origin {
2469        RepairOrigin::Scan(_) => (2, 2),
2470        RepairOrigin::Breakout { .. } => {
2471            if pieces_a.len().max(pieces_b.len()) >= 2 {
2472                (1, 1)
2473            } else {
2474                (2, 2)
2475            }
2476        }
2477    };
2478    if pieces_a.len() < need_a || pieces_b.len() < need_b {
2479        return Err(KernelRefusal::unsupported(
2480            KernelStage::Fragment,
2481            "crossing.one_sided_cut",
2482            format!(
2483                "the crossing cut face {face_a} into {} piece(s) and face {face_b} into {} — this \
2484                 repair resolves the case where the curve cuts BOTH trims, so one part of each is \
2485                 the tail that overshot",
2486                pieces_a.len(),
2487                pieces_b.len()
2488            ),
2489        ));
2490    }
2491    if pieces_a.len() + pieces_b.len() > MAXIMUM_CROSSING_PIECES {
2492        return Err(KernelRefusal::non_convergence(
2493            KernelStage::Fragment,
2494            "crossing.pieces",
2495            format!(
2496                "the crossing cut face {face_a} into {} piece(s) and face {face_b} into {}, which is \
2497                 {} pieces against this repair's budget of {MAXIMUM_CROSSING_PIECES} — every way to \
2498                 drop some of them is rebuilt and re-checked, so the enumeration is bounded rather \
2499                 than sampled",
2500                pieces_a.len(),
2501                pieces_b.len(),
2502                pieces_a.len() + pieces_b.len(),
2503            ),
2504        ));
2505    }
2506
2507    // WHICH pieces are the tails is decided by the RESULT, not by a normal.
2508    // Every way to drop some pieces of each crossing face (keeping at least one
2509    // of each) is closed under the re-trim below, rebuilt in full and
2510    // re-checked; the repair is the answer only when exactly ONE of them
2511    // verifies.
2512    //
2513    // The enumeration runs under TWO closures, and the second only where the
2514    // first found nothing at all. [`DropClosure::Drop`] drops a neighbour the
2515    // drop leaves holding an unmatched boundary — the rule this repair has
2516    // always used, and the one every body it already resolves is decided by.
2517    // [`DropClosure::Retrim`] RE-TRIMS that neighbour instead. Ordering them
2518    // this way makes the second closure strictly additive: a body the first
2519    // resolves never reaches it, so no answer this repair already returns can
2520    // change. The price is the enumeration twice over on a body that refuses.
2521    let plan = DropPlan {
2522        split: &split,
2523        fragments: &fragments,
2524        imprint: &imprint,
2525        touched: &touched,
2526        degenerate: &degenerate,
2527        pieces_a: &pieces_a,
2528        pieces_b: &pieces_b,
2529        face_a,
2530        face_b,
2531        scale,
2532    };
2533    let (mut verified, refusals) = plan.enumerate(DropClosure::Drop);
2534    if verified.is_empty() {
2535        verified = plan.enumerate(DropClosure::Retrim).0;
2536    }
2537
2538    if verified.len() > 1 {
2539        return Err(KernelRefusal::ill_posed(
2540            KernelStage::Select,
2541            "crossing.ambiguous_drop",
2542            format!(
2543                "{} different drops of the crossing pieces all rebuilt into a sound body, so the \
2544                 result does not decide which material the crossing encloses; this repair answers \
2545                 only where one does",
2546                verified.len()
2547            ),
2548        ));
2549    }
2550    let Some(Candidate {
2551        dropped,
2552        retired,
2553        retrimmed,
2554        solid: repaired,
2555    }) = verified.pop()
2556    else {
2557        // The first few reasons, not all of them: eight pieces is up to 225
2558        // candidates and a refusal nobody can read is a refusal nobody uses.
2559        let listed = refusals
2560            .iter()
2561            .take(4)
2562            .cloned()
2563            .collect::<Vec<_>>()
2564            .join(" | ");
2565        // The candidate reasons (an over-used edge, a closure that consumed
2566        // every face, a rebuild that declined) are CONTEXT inside this one
2567        // refusal, whose class is minted here: the body still crosses itself.
2568        return Err(KernelRefusal::unsound(
2569            KernelStage::Sew,
2570            SoundnessDefect::Crossing,
2571            vec![face_a, face_b],
2572            format!(
2573                "none of the {} ways of dropping the crossing pieces rebuilt into a sound body \
2574                 ({listed}{}); the worst crossing was {:.3e} past its band, so the overshoot is \
2575                 real and it is the SPLIT that did not resolve it",
2576                refusals.len(),
2577                if refusals.len() > 4 {
2578                    format!(" | and {} more", refusals.len() - 4)
2579                } else {
2580                    String::new()
2581                },
2582                match &origin {
2583                    RepairOrigin::Scan(report) => report
2584                        .confirmed
2585                        .first()
2586                        .map(|crossing| crossing.straddle)
2587                        .unwrap_or_default(),
2588                    RepairOrigin::Breakout { .. } => 0.0,
2589                }
2590            ),
2591        ));
2592    };
2593
2594    record_repair(
2595        &fragments,
2596        &dropped,
2597        &retired,
2598        &retrimmed,
2599        face_a,
2600        face_b,
2601        &origin,
2602    );
2603    Ok(repaired)
2604}
2605
2606/// One candidate answer: the fragments the drop removed, the ones it RETIRED
2607/// as detached, the faces it RE-TRIMMED rather than removed, and the body
2608/// those choices rebuilt into.
2609struct Candidate {
2610    dropped: Vec<usize>,
2611    retired: Vec<u64>,
2612    retrimmed: Vec<u64>,
2613    solid: BrepSolid,
2614}
2615
2616/// How a candidate drop closes over a neighbour it leaves holding a boundary
2617/// no other face answers for.
2618#[derive(Clone, Copy, Eq, PartialEq)]
2619enum DropClosure {
2620    /// DROP that neighbour too, iterated to a fixed point — [`close_drop_set`].
2621    Drop,
2622    /// RE-TRIM it — [`retrim_drop_set`]: take the unmatched coedges off it and
2623    /// re-link what remains into closed loops on the same carrier, with the
2624    /// same pcurves. The bound is the same one (a face the imprint never cut
2625    /// still ends the candidate); what changes is that a face it DID cut is
2626    /// re-trimmed rather than consumed.
2627    Retrim,
2628}
2629
2630/// One crossing pair's split and everything a candidate drop is measured
2631/// against. Bundled so the enumeration can run once per closure.
2632struct DropPlan<'a> {
2633    split: &'a BrepSolid,
2634    fragments: &'a [crate::fragment::FaceFragmentRecord],
2635    imprint: &'a crate::imprint::ImprintResultRecord,
2636    touched: &'a rustc_hash::FxHashSet<u64>,
2637    degenerate: &'a rustc_hash::FxHashSet<EdgeKey>,
2638    pieces_a: &'a [usize],
2639    pieces_b: &'a [usize],
2640    face_a: u64,
2641    face_b: u64,
2642    scale: f64,
2643}
2644
2645impl DropPlan<'_> {
2646    /// Every way of dropping some pieces of each crossing face, closed under
2647    /// `closure`, rebuilt in full and re-checked. The candidates that verified
2648    /// come back first, the reasons the rest did not second.
2649    fn enumerate(&self, closure: DropClosure) -> (Vec<Candidate>, Vec<String>) {
2650        let (face_a, face_b) = (self.face_a, self.face_b);
2651        let mut refusals: Vec<String> = Vec::new();
2652        let mut verified: Vec<Candidate> = Vec::new();
2653        let mut seen: Vec<(Vec<usize>, Vec<Vec<usize>>)> = Vec::new();
2654        for drop_a in proper_subsets(self.pieces_a) {
2655            for drop_b in proper_subsets(self.pieces_b) {
2656                let mut dropped: Vec<usize> =
2657                    drop_a.iter().chain(drop_b.iter()).copied().collect();
2658                if dropped.is_empty() {
2659                    continue;
2660                }
2661                let label = format!(
2662                    "dropping piece(s) {:?} of face {face_a} and {:?} of face {face_b}",
2663                    drop_a
2664                        .iter()
2665                        .map(|index| self
2666                            .pieces_a
2667                            .iter()
2668                            .position(|piece| piece == index)
2669                            .unwrap_or(0))
2670                        .collect::<Vec<_>>(),
2671                    drop_b
2672                        .iter()
2673                        .map(|index| self
2674                            .pieces_b
2675                            .iter()
2676                            .position(|piece| piece == index)
2677                            .unwrap_or(0))
2678                        .collect::<Vec<_>>(),
2679                );
2680                // The re-trim: a face that bordered dropped material is left
2681                // with a boundary use no other face answers, and a body cannot
2682                // close over one. Closing the drop under that rule is what
2683                // turns a two-face split into a re-trim of every face the
2684                // dropped material bordered.
2685                // The dropping closure leaves every fragment as it was, so it
2686                // borrows the list rather than copying it per candidate.
2687                let mut retired: Vec<u64> = Vec::new();
2688                let closed = match closure {
2689                    DropClosure::Drop => close_drop_set(
2690                        self.fragments,
2691                        &mut dropped,
2692                        &mut retired,
2693                        self.touched,
2694                        self.degenerate,
2695                    )
2696                    .map(|()| (None, Vec::new())),
2697                    DropClosure::Retrim => retrim_drop_set(
2698                        self.fragments,
2699                        &mut dropped,
2700                        &mut retired,
2701                        self.touched,
2702                        self.degenerate,
2703                    )
2704                    .map(|(working, retrimmed)| (Some(working), retrimmed)),
2705                };
2706                let (working, retrimmed) = match closed {
2707                    Ok(closed) => closed,
2708                    Err(reason) => {
2709                        if trace_on() {
2710                            println!("    candidate died at closure {label}: {reason}");
2711                        }
2712                        refusals.push(format!("{label}: {reason}"));
2713                        continue;
2714                    }
2715                };
2716                let working: &[crate::fragment::FaceFragmentRecord] =
2717                    working.as_deref().unwrap_or(self.fragments);
2718                dropped.sort_unstable();
2719                // Two seeds can close onto the same answer. The signature is
2720                // the drop AND what the re-trim left of each kept fragment, so
2721                // a second closure that re-trimmed differently is a different
2722                // candidate rather than a hidden duplicate.
2723                let signature = (
2724                    dropped.clone(),
2725                    working
2726                        .iter()
2727                        .enumerate()
2728                        .filter(|(index, _)| !dropped.contains(index))
2729                        .map(|(_, fragment)| {
2730                            fragment
2731                                .loops
2732                                .iter()
2733                                .map(|fragment_loop| fragment_loop.coedges.len())
2734                                .collect()
2735                        })
2736                        .collect(),
2737                );
2738                if seen.contains(&signature) {
2739                    continue;
2740                }
2741                seen.push(signature);
2742                if dropped.len() == working.len() {
2743                    refusals.push(format!("{label}: the re-trim consumed every face"));
2744                    continue;
2745                }
2746                // The census's OTHER direction — see [`over_used_edge`].
2747                if let Some((key, count)) = over_used_edge(working, &dropped, self.degenerate) {
2748                    refusals.push(format!(
2749                        "{label}: {} is held {count} times by the pieces this drop keeps, and a \
2750                         closed body holds every edge exactly twice",
2751                        match key {
2752                            EdgeKey::Boundary(_, edge) => format!("edge {edge}"),
2753                            EdgeKey::Imprint(piece) => format!("imprint piece {piece}"),
2754                        }
2755                    ));
2756                    if trace_on() {
2757                        println!(
2758                            "    candidate refused   {label}: {key:?} held {count} times"
2759                        );
2760                    }
2761                    continue;
2762                }
2763                let selected: Vec<_> = working
2764                    .iter()
2765                    .enumerate()
2766                    .filter(|(index, _)| !dropped.contains(index))
2767                    .map(|(_, fragment)| fragment.clone())
2768                    .collect();
2769                match rebuild(self.split, selected, self.imprint, self.scale) {
2770                    // `BREP_HEAL_TRACE=1` prints every candidate the
2771                    // enumeration considered, whether it verified, and what
2772                    // volume it produced. This is the instrument that found the
2773                    // 2026-09-22 stale-fragment defect: the `touched` bound
2774                    // kills most candidates before `verified.len() == 1` picks
2775                    // a survivor, and without the list there is no way to see
2776                    // that the survivor was not the right one.
2777                    Ok(solid) => {
2778                        if trace_on() {
2779                            println!(
2780                                "    CANDIDATE VERIFIED  {label}  volume {:.6}  dropped {:?}",
2781                                crate::solid_mass_properties(&solid)
2782                                    .map(|mass| mass.volume)
2783                                    .unwrap_or(f64::NAN),
2784                                dropped
2785                            );
2786                        }
2787                        verified.push(Candidate {
2788                            dropped,
2789                            retired,
2790                            retrimmed,
2791                            solid,
2792                        })
2793                    }
2794                    Err(reason) => {
2795                        if trace_on() {
2796                            println!("    candidate refused   {label}: {reason}");
2797                        }
2798                        refusals.push(format!("{label}: {reason}"))
2799                    }
2800                }
2801            }
2802        }
2803        (verified, refusals)
2804    }
2805}
2806
2807/// The faces this repair may re-trim: the two that cross, and every face whose
2808/// own loops use an edge the imprint split.
2809fn touched_faces(
2810    solid: &BrepSolid,
2811    imprint: &crate::imprint::ImprintResultRecord,
2812    face_a: u64,
2813    face_b: u64,
2814) -> rustc_hash::FxHashSet<u64> {
2815    use rustc_hash::FxHashSet as HashSet;
2816    let split_edges: HashSet<u64> = imprint
2817        .edge_splits
2818        .iter()
2819        .filter(|split| split.operand == SELF_OPERAND)
2820        .map(|split| split.edge_id)
2821        .collect();
2822    let mut touched: HashSet<u64> = [face_a, face_b].into_iter().collect();
2823    for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
2824        let uses_split = face
2825            .loops
2826            .iter()
2827            .flat_map(|loop_record| &loop_record.coedges)
2828            .any(|coedge| split_edges.contains(&coedge.edge_id));
2829        if uses_split {
2830            touched.insert(face.id);
2831        }
2832    }
2833    touched
2834}
2835
2836/// Close a drop set under the rule a closed body cannot break: every edge is
2837/// used exactly twice. Dropping a tail takes its uses with it, so any face left
2838/// holding a boundary no other face answers bordered the dropped material and
2839/// has to be re-trimmed — which, since the imprint already cut it, means
2840/// dropping the part of it that the crossing cut off. Iterated to a fixed
2841/// point, and bounded by the touched set: reaching a face the imprint never cut
2842/// means the drop has left the material the crossing encloses, and the
2843/// candidate is refused rather than grown.
2844///
2845/// The one thing that bound does not own is a fragment joined to NOTHING the
2846/// drop keeps — see [`fragment_is_detached`]. That is not material the drop ran
2847/// past; it is what the old configuration left, and it is retired wherever it
2848/// came from.
2849fn close_drop_set(
2850    fragments: &[crate::fragment::FaceFragmentRecord],
2851    dropped: &mut Vec<usize>,
2852    retired: &mut Vec<u64>,
2853    touched: &rustc_hash::FxHashSet<u64>,
2854    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
2855) -> Result<(), KernelRefusal> {
2856    use rustc_hash::FxHashMap as HashMap;
2857    loop {
2858        let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
2859        for (index, fragment) in fragments.iter().enumerate() {
2860            if dropped.contains(&index) {
2861                continue;
2862            }
2863            for key in fragment
2864                .loops
2865                .iter()
2866                .flat_map(|fragment_loop| &fragment_loop.coedges)
2867                .filter_map(|coedge| edge_key(&coedge.source))
2868            {
2869                *uses.entry(key).or_default() += 1;
2870            }
2871        }
2872        let mut grew = false;
2873        for (index, fragment) in fragments.iter().enumerate() {
2874            if dropped.contains(&index) {
2875                continue;
2876            }
2877            // RETIRED, not re-trimmed: a fragment holding no edge that any
2878            // other kept fragment holds is attached to nothing this drop keeps.
2879            // The [`touched`] bound does not apply to it, because that bound
2880            // asks whether the drop has run past the material the crossing
2881            // encloses — and a piece joined to the body by nothing is not
2882            // material this drop reached, it is material the OLD configuration
2883            // left behind. Retiring it cannot cascade: the only uses it removes
2884            // are uses nothing else holds. See [`fragment_is_detached`].
2885            if fragment_is_detached(fragment, &uses, degenerate) {
2886                dropped.push(index);
2887                retired.push(fragment.source_face_id);
2888                grew = true;
2889                continue;
2890            }
2891            let unmatched = fragment
2892                .loops
2893                .iter()
2894                .flat_map(|fragment_loop| &fragment_loop.coedges)
2895                .filter_map(|coedge| edge_key(&coedge.source))
2896                .filter(|key| !degenerate.contains(key))
2897                .any(|key| uses.get(&key) == Some(&1));
2898            if !unmatched {
2899                continue;
2900            }
2901            if !touched.contains(&fragment.source_face_id) {
2902                return Err(KernelRefusal::internal(KernelStage::Validate, "retrim.uncut_face", format!(
2903                    "the re-trim reached face {}, which the imprint never cut — the drop has run \
2904                     past the material the crossing encloses",
2905                    fragment.source_face_id
2906                )));
2907            }
2908            dropped.push(index);
2909            grew = true;
2910        }
2911        if !grew {
2912            return Ok(());
2913        }
2914    }
2915}
2916
2917/// [`close_drop_set`]'s other closure: a face left holding a boundary no other
2918/// face answers for is RE-TRIMMED rather than dropped.
2919///
2920/// Dropping it is the right answer when the unanswered boundary is the whole
2921/// of what that face contributed to the dropped material — a tail beside a
2922/// tail. It is the WRONG answer when the face is one the body keeps and only
2923/// part of its boundary went: a bore carried out through a side wall leaves
2924/// the mouth flat holding the stretch of its own corner edge that now runs
2925/// inside the bore, and the arc of the mouth ring that now runs outside the
2926/// body, and neither is material to drop — they are two coedges to take OFF a
2927/// face that otherwise stays exactly as it is. Taking them off merges that
2928/// face's mouth loop into its outer one, which is precisely the topology a
2929/// breakout has and a drop-only closure cannot reach.
2930///
2931/// The bound is [`close_drop_set`]'s: a face the imprint never cut still ends
2932/// the candidate rather than widening the re-trim, with the two exceptions that
2933/// are not re-trims at all — a fragment attached to nothing
2934/// ([`fragment_is_detached`]) and a whole loop that has left its face
2935/// ([`holds_only_stale_loops`]), both of which are retired. What comes back
2936/// is the fragment list to rebuild from — the untouched fragments as they were,
2937/// the re-trimmed ones with their loops re-linked — and the ids of the faces
2938/// that were re-trimmed, for the ledger. A fragment left with no coedge at all
2939/// joins `dropped`.
2940fn retrim_drop_set(
2941    fragments: &[crate::fragment::FaceFragmentRecord],
2942    dropped: &mut Vec<usize>,
2943    retired: &mut Vec<u64>,
2944    touched: &rustc_hash::FxHashSet<u64>,
2945    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
2946) -> Result<(Vec<crate::fragment::FaceFragmentRecord>, Vec<u64>), KernelRefusal> {
2947    use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
2948
2949    let mut working = fragments.to_vec();
2950    let mut retrimmed: Vec<u64> = Vec::new();
2951    loop {
2952        let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
2953        for (index, fragment) in working.iter().enumerate() {
2954            if dropped.contains(&index) {
2955                continue;
2956            }
2957            for key in fragment
2958                .loops
2959                .iter()
2960                .flat_map(|fragment_loop| &fragment_loop.coedges)
2961                .filter_map(|coedge| edge_key(&coedge.source))
2962            {
2963                *uses.entry(key).or_default() += 1;
2964            }
2965        }
2966        // RETIRED FIRST, and before the early return below, because a
2967        // detached fragment does not have to hold an UNMATCHED edge to be
2968        // detached — one whose loop walks its own seam twice holds that seam
2969        // twice itself, and the census reads two. See [`fragment_is_detached`].
2970        let mut grew = false;
2971        for index in 0..working.len() {
2972            if dropped.contains(&index) {
2973                continue;
2974            }
2975            if fragment_is_detached(&working[index], &uses, degenerate) {
2976                dropped.push(index);
2977                retired.push(working[index].source_face_id);
2978                grew = true;
2979            }
2980        }
2981        if grew {
2982            // The census is stale the moment anything is retired.
2983            continue;
2984        }
2985        // A POLE edge is legitimately used once, as in [`close_drop_set`].
2986        let unmatched: HashSet<EdgeKey> = uses
2987            .iter()
2988            .filter(|(key, count)| **count == 1 && !degenerate.contains(key))
2989            .map(|(key, _)| *key)
2990            .collect();
2991        if unmatched.is_empty() {
2992            return Ok((working, retrimmed));
2993        }
2994        for index in 0..working.len() {
2995            if dropped.contains(&index) {
2996                continue;
2997            }
2998            let holds = working[index]
2999                .loops
3000                .iter()
3001                .flat_map(|fragment_loop| &fragment_loop.coedges)
3002                .filter_map(|coedge| edge_key(&coedge.source))
3003                .any(|key| unmatched.contains(&key));
3004            if !holds {
3005                continue;
3006            }
3007            // A STALE LOOP is the other way the old configuration leaves
3008            // something behind, and the [`touched`] bound does not own it —
3009            // see [`holds_only_stale_loops`].
3010            let stale = holds_only_stale_loops(&working[index], &unmatched, degenerate);
3011            if !touched.contains(&working[index].source_face_id) && !stale {
3012                return Err(KernelRefusal::internal(KernelStage::Validate, "retrim.uncut_face", format!(
3013                    "the re-trim reached face {}, which the imprint never cut — the drop has run \
3014                     past the material the crossing encloses",
3015                    working[index].source_face_id
3016                )));
3017            }
3018            let face = working[index].source_face_id;
3019            match retrim_fragment(&working[index], &unmatched)? {
3020                Some(next) => {
3021                    working[index] = next;
3022                    if stale {
3023                        retired.push(face);
3024                    } else if !retrimmed.contains(&face) {
3025                        retrimmed.push(face);
3026                    }
3027                }
3028                None => dropped.push(index),
3029            }
3030            grew = true;
3031        }
3032        if !grew {
3033            return Ok((working, retrimmed));
3034        }
3035    }
3036}
3037
3038/// Re-trim ONE fragment: take off every coedge in `remove` and re-link what is
3039/// left into closed loops.
3040///
3041/// The link is made in the face's OWN parameter domain, which is where a trim
3042/// loop is closed and the only frame that tells the two sides of a seam apart —
3043/// a cylinder's seam is one curve in 3D and two coedges a period apart here.
3044/// Nothing is fitted or re-parameterised: each surviving coedge keeps its
3045/// source, its sense and its pcurve, and only the ORDER and the grouping into
3046/// loops are rebuilt. A chain that does not close is a re-trim this repair
3047/// cannot prove, and it refuses rather than emitting an open face.
3048///
3049/// The loops come back in the order of their first surviving coedge, so a face
3050/// whose outer loop has just absorbed an inner one is still outer-loop-first.
3051/// `Ok(None)` when nothing at all is left to keep.
3052fn retrim_fragment(
3053    fragment: &crate::fragment::FaceFragmentRecord,
3054    remove: &rustc_hash::FxHashSet<EdgeKey>,
3055) -> Result<Option<crate::fragment::FaceFragmentRecord>, KernelRefusal> {
3056    use crate::fragment::{FaceFragmentRecord, FragmentCoedge, FragmentLoop};
3057
3058    let kept: Vec<FragmentCoedge> = fragment
3059        .loops
3060        .iter()
3061        .flat_map(|fragment_loop| &fragment_loop.coedges)
3062        .filter(|coedge| !edge_key(&coedge.source).is_some_and(|key| remove.contains(&key)))
3063        .cloned()
3064        .collect();
3065    if kept.is_empty() {
3066        return Ok(None);
3067    }
3068    let ends = kept
3069        .iter()
3070        .map(|coedge| {
3071            let [t0, t1] = coedge.pcurve.domain()?;
3072            Ok((coedge.pcurve.evaluate(t0)?, coedge.pcurve.evaluate(t1)?))
3073        })
3074        .collect::<Result<Vec<_>, String>>().or_refuse(KernelStage::Validate, "collect")?;
3075    // The domain's own diagonal sets the band a junction is matched in: a
3076    // plane's parameters are millimetres and a cylinder's are radians against
3077    // millimetres, so one fixed number would mean two different things on the
3078    // two carriers. The ends being matched were cut from ONE curve by the
3079    // imprint's own split, so they agree to the fit's precision and the band
3080    // only has to be small against the face.
3081    let [u0, u1] = fragment.surface.domain_u().or_refuse(KernelStage::Validate, "domain_u")?;
3082    let [v0, v1] = fragment.surface.domain_v().or_refuse(KernelStage::Validate, "domain_v")?;
3083    let tolerance = 1e-4 * (u1 - u0).hypot(v1 - v0);
3084    let mut used = vec![false; kept.len()];
3085    let mut loops: Vec<FragmentLoop> = Vec::new();
3086    for seed in 0..kept.len() {
3087        if used[seed] {
3088            continue;
3089        }
3090        used[seed] = true;
3091        let mut chain = vec![seed];
3092        let start = ends[seed].0;
3093        let mut cursor = ends[seed].1;
3094        while cursor.sub(start).length() > tolerance {
3095            let next = (0..kept.len())
3096                .filter(|index| !used[*index])
3097                .map(|index| (index, ends[index].0.sub(cursor).length()))
3098                .filter(|(_, gap)| *gap <= tolerance)
3099                .min_by(|left, right| left.1.total_cmp(&right.1))
3100                .map(|(index, _)| index);
3101            let Some(next) = next else {
3102                return Err(KernelRefusal::internal(KernelStage::Validate, "retrim.open_chain", format!(
3103                    "face {}'s re-trim left a chain open at ({:.6}, {:.6}) in its own parameter \
3104                     domain — what the drop took off it does not close into a loop",
3105                    fragment.source_face_id, cursor.x, cursor.y
3106                )));
3107            };
3108            used[next] = true;
3109            chain.push(next);
3110            cursor = ends[next].1;
3111        }
3112        loops.push(FragmentLoop {
3113            coedges: chain.into_iter().map(|index| kept[index].clone()).collect(),
3114        });
3115    }
3116    Ok(Some(FaceFragmentRecord {
3117        loops,
3118        ..fragment.clone()
3119    }))
3120}
3121
3122/// Is this fragment attached to NOTHING else the drop keeps?
3123///
3124/// The census `uses` counts every non-dropped fragment's edge keys, this one's
3125/// included. So a fragment is detached exactly when, for every non-degenerate
3126/// key it holds, the census count equals ITS OWN count of that key: nobody else
3127/// is holding any edge of it.
3128///
3129/// It is stated that way rather than as "every key it holds is used once"
3130/// because a fragment may legitimately hold one key TWICE on its own — a
3131/// cylinder band walks its seam up one side and down the other — and such a
3132/// fragment is still detached when the seam's only two uses are both its.
3133///
3134/// **What a detached fragment IS.** It is a piece of the old configuration that
3135/// the new one has no room for: on the turned-bore migration, the top face's
3136/// hole loop is the exit ring the bore no longer opens through, sitting out at
3137/// x = 44.6 where the top face's own square ends at 20. The fragmenter returns
3138/// it as its own island because the ring is disjoint from the square, and the
3139/// only other face holding that ring is the wall's overshoot tail. Drop the
3140/// tail and the island is joined to nothing.
3141///
3142/// **Why retiring it is bounded.** Keeping it can never produce a closed body:
3143/// every edge it holds is answered for only by itself, so the assembly is open
3144/// there however the rest of the drop goes. And retiring it removes only uses
3145/// nothing else holds, so it cannot leave a third face unmatched — it does not
3146/// cascade, and no candidate the enumeration would otherwise verify is lost.
3147fn fragment_is_detached(
3148    fragment: &crate::fragment::FaceFragmentRecord,
3149    uses: &rustc_hash::FxHashMap<EdgeKey, usize>,
3150    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
3151) -> bool {
3152    use rustc_hash::FxHashMap as HashMap;
3153    let mut own: HashMap<EdgeKey, usize> = HashMap::default();
3154    for key in fragment
3155        .loops
3156        .iter()
3157        .flat_map(|fragment_loop| &fragment_loop.coedges)
3158        .filter_map(|coedge| edge_key(&coedge.source))
3159        .filter(|key| !degenerate.contains(key))
3160    {
3161        *own.entry(key).or_default() += 1;
3162    }
3163    // A fragment carrying no identified edge at all is NOT called detached:
3164    // its boundary is all DERIVED, which `edge_key` deliberately does not
3165    // count, and the rebuild's own closure is what answers for those.
3166    !own.is_empty()
3167        && own
3168            .iter()
3169            .all(|(key, count)| uses.get(key).copied().unwrap_or(0) == *count)
3170}
3171
3172/// The OTHER way the old configuration leaves something behind: a WHOLE LOOP
3173/// of a face that nothing else answers for AND THAT HAS LEFT THAT FACE.
3174///
3175/// [`fragment_is_detached`] catches the leftover when the fragmenter returns it
3176/// as its own island — which it does when the stale ring is far enough from the
3177/// face's own trim to be plainly disjoint. When it is nearer, the same ring
3178/// comes back as a HOLE LOOP of the face it left: the turned bore's exit ring
3179/// at 40 degrees sits at `x = 22.9 - 30.7` against a top face that ends at 20,
3180/// and the top face is ONE fragment carrying it. There is no island to retire,
3181/// and taking the loop off is a RE-TRIM of a face the imprint never cut.
3182///
3183/// That is the move the earlier records named as missing — *"the departed face
3184/// keeps a hole loop the imprint never cut, and the drop enumeration has no
3185/// move for retiring one"*. This is the move, and it is bounded FOUR ways:
3186///
3187/// * **Unanswered for.** Every non-degenerate edge of the loop is used once,
3188///   by this fragment alone, so no other face the drop keeps is standing on it
3189///   and removing it takes nothing from anyone. A loop only PART of which is
3190///   unanswered for is a genuine re-trim of a neighbour that bordered the
3191///   dropped material, and the [`touched`] bound still owns that case.
3192/// * **It has LEFT the face**, by the containment floor's own test: every
3193///   sample of it lies outside the face's outer loop in that face's own
3194///   parameter plane. This is the condition that matters, and it was added
3195///   after measuring what happens without it. Unanswered-for alone is
3196///   SYMMETRIC — at 40 degrees it licensed retiring the bore's ENTRY mouth
3197///   just as readily as its departed exit ring, so the enumeration verified
3198///   two bodies and refused the pair as ambiguous. A loop still inside its own
3199///   face is trimming material and is not a leftover, whoever is holding it.
3200/// * **A loop survives.** This never empties a face; a fragment with nothing
3201///   left at all is the detached case above.
3202/// * **A PLANE only**, as the containment floor restricts itself, because on a
3203///   periodic surface "outside the outer loop" in the parameter plane is a
3204///   statement about the seam rather than about the trim.
3205///
3206/// And the body still has to rebuild, `validate()`, connect, scan clean and
3207/// pass the containment floor afterwards, exactly as before.
3208fn holds_only_stale_loops(
3209    fragment: &crate::fragment::FaceFragmentRecord,
3210    unmatched: &rustc_hash::FxHashSet<EdgeKey>,
3211    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
3212) -> bool {
3213    if !matches!(
3214        fragment.surface.analytic(),
3215        Some(crate::AnalyticSurface::Plane { .. })
3216    ) {
3217        return false;
3218    }
3219    let mut stale: Vec<usize> = Vec::new();
3220    let mut surviving = 0usize;
3221    for (index, fragment_loop) in fragment.loops.iter().enumerate() {
3222        let keys: Vec<EdgeKey> = fragment_loop
3223            .coedges
3224            .iter()
3225            .filter_map(|coedge| edge_key(&coedge.source))
3226            .filter(|key| !degenerate.contains(key))
3227            .collect();
3228        if !keys.is_empty() && keys.iter().all(|key| unmatched.contains(key)) {
3229            stale.push(index);
3230            continue;
3231        }
3232        if keys.iter().any(|key| unmatched.contains(key)) {
3233            return false;
3234        }
3235        surviving += 1;
3236    }
3237    if stale.is_empty() || surviving == 0 {
3238        return false;
3239    }
3240    let Ok(polylines) = fragment
3241        .loops
3242        .iter()
3243        .map(fragment_loop_polyline)
3244        .collect::<Result<Vec<_>, KernelRefusal>>()
3245    else {
3246        return false;
3247    };
3248    if polylines.iter().any(Vec::is_empty) {
3249        return false;
3250    }
3251    let outer_index = (0..polylines.len())
3252        .max_by(|&a, &b| {
3253            polyline_area(&polylines[a])
3254                .abs()
3255                .total_cmp(&polyline_area(&polylines[b]).abs())
3256        })
3257        .unwrap_or(0);
3258    // The face's own outer loop being the stale one is not a leftover to
3259    // retire; it is the whole face going, which is the detached case.
3260    if stale.contains(&outer_index) {
3261        return false;
3262    }
3263    let outer = &polylines[outer_index];
3264    let bands = BoundaryBands::build(outer);
3265    let tolerance = boundary_tolerance(outer);
3266    stale.iter().all(|index| {
3267        polylines[*index]
3268            .iter()
3269            .all(|&point| !bands.contains(point) && !bands.within(point, tolerance))
3270    })
3271}
3272
3273/// The other half of the census rule, and the one that had no enforcement.
3274///
3275/// A closed body uses every non-degenerate edge EXACTLY TWICE. The drop
3276/// closures above are the "fewer than twice" half — a face left holding a
3277/// boundary nobody answers is dropped or re-trimmed. Nothing checked the other
3278/// direction, and the direction matters here: an imprint piece is cut into BOTH
3279/// crossing faces, so before any drop it is held four times, once by each of
3280/// the four pieces. A drop that takes one piece away leaves it held three
3281/// times — three faces along one curve, which is not a body.
3282///
3283/// That is the candidate the turned-bore migration used to return: dropping the
3284/// exit mouth's disc alone leaves the wall's kept part, the wall's overshoot
3285/// tail and the mouth flat's remainder all on the mouth ring. Refusing it is
3286/// what leaves the one drop that is right.
3287///
3288/// **And nothing downstream catches it**, which is why the check belongs here
3289/// and not there. `assemble_open_fragments` resolves the third use by putting
3290/// the overshoot on a shell of its OWN: the body comes back with every edge
3291/// used exactly twice, `validate()` clean, `solid_connectivity` calling it
3292/// connected (the sheets share edges with the main shell), the scan clean and
3293/// the containment floor acquitting it — a three-shell solid whose first shell
3294/// is the right answer and whose other two are sheets stapled to it. Measured
3295/// on the turned bore at 60 degrees: shell 1 is the 7 correct faces at
3296/// 7673.516114379 against a closed form of 7673.516114438, shell 2 is the
3297/// overshoot alone at -536.326313 and shell 3 the departed ring at -6.7e-14.
3298///
3299/// Returns the offending key and its count, smallest key first so the refusal
3300/// reads the same on every run.
3301fn over_used_edge(
3302    fragments: &[crate::fragment::FaceFragmentRecord],
3303    dropped: &[usize],
3304    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
3305) -> Option<(EdgeKey, usize)> {
3306    use rustc_hash::FxHashMap as HashMap;
3307    let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
3308    for (index, fragment) in fragments.iter().enumerate() {
3309        if dropped.contains(&index) {
3310            continue;
3311        }
3312        for key in fragment
3313            .loops
3314            .iter()
3315            .flat_map(|fragment_loop| &fragment_loop.coedges)
3316            .filter_map(|coedge| edge_key(&coedge.source))
3317            .filter(|key| !degenerate.contains(key))
3318        {
3319            *uses.entry(key).or_default() += 1;
3320        }
3321    }
3322    let mut over: Vec<(EdgeKey, usize)> = uses
3323        .into_iter()
3324        .filter(|(_, count)| *count > 2)
3325        .collect();
3326    over.sort_unstable();
3327    over.into_iter().next()
3328}
3329
3330/// The identity of a fragment coedge's edge, for the use census. A DERIVED
3331/// coedge carries a freshly minted curve rather than an identity, so it is not
3332/// counted — the rebuild's own closure check is what answers for those.
3333fn edge_key(source: &crate::fragment::FragmentEdgeSource) -> Option<EdgeKey> {
3334    use crate::fragment::FragmentEdgeSource;
3335    match source {
3336        FragmentEdgeSource::Boundary { operand, edge_id }
3337        | FragmentEdgeSource::SharedBoundary { operand, edge_id } => {
3338            Some(EdgeKey::Boundary(*operand, *edge_id))
3339        }
3340        FragmentEdgeSource::Imprint { piece_id } => Some(EdgeKey::Imprint(*piece_id)),
3341        FragmentEdgeSource::Derived { .. } => None,
3342    }
3343}
3344
3345#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
3346enum EdgeKey {
3347    Boundary(u8, u64),
3348    Imprint(u64),
3349}
3350
3351/// Every subset of `pieces` that leaves at least one behind, the empty one
3352/// included — the ways one face's pieces can be dropped.
3353fn proper_subsets(pieces: &[usize]) -> Vec<Vec<usize>> {
3354    let mut subsets = Vec::new();
3355    for mask in 0..(1u32 << pieces.len()) {
3356        if mask == (1u32 << pieces.len()) - 1 {
3357            // Dropping every piece of a crossing face is not a re-trim of it.
3358            continue;
3359        }
3360        subsets.push(
3361            pieces
3362                .iter()
3363                .enumerate()
3364                .filter(|(bit, _)| mask & (1 << bit) != 0)
3365                .map(|(_, piece)| *piece)
3366                .collect(),
3367        );
3368    }
3369    subsets
3370}
3371
3372/// Reassemble one choice of kept fragments and prove it sound, or say why not.
3373fn rebuild(
3374    split: &BrepSolid,
3375    selected: Vec<crate::fragment::FaceFragmentRecord>,
3376    imprint: &crate::imprint::ImprintResultRecord,
3377    scale: f64,
3378) -> Result<BrepSolid, KernelRefusal> {
3379    use crate::boolean::{assemble_open_fragments, finalize_assembled_solid};
3380    use rustc_hash::FxHashMap as HashMap;
3381
3382    let mut sources: HashMap<u8, &BrepSolid> = HashMap::default();
3383    sources.insert(SELF_OPERAND, split);
3384    let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
3385    let solid = assemble_open_fragments(selected, &sources, imprint, assembly_tolerance)
3386        .map_err(|error| error.with_message(|error| format!("the kept pieces did not assemble: {error}")))?;
3387    let solid = finalize_assembled_solid(solid, assembly_tolerance)
3388        .map_err(|error| error.with_message(|error| format!("the assembly did not finalize: {error}")))?;
3389
3390    let issues = solid.validate();
3391    if !issues.is_empty() {
3392        return Err(KernelRefusal::internal(KernelStage::Validate, "rebuild.validate", format!(
3393            "the rebuilt body fails validate(): {}",
3394            issues[0].message
3395        )));
3396    }
3397    if crate::solid_connectivity(&solid).is_disconnected() {
3398        return Err(KernelRefusal::internal(KernelStage::Validate, "rebuild.connected", "the rebuilt body is not connected"));
3399    }
3400    let options = SelfIntersectionOptions::for_solid(&solid);
3401    let report = solid_self_intersections(&solid, options)
3402        .map_err(|message| format!("the rebuilt body could not be scanned: {message}"))
3403        .or_refuse(KernelStage::Validate, "solid_self_intersections")?;
3404    if report.is_flagged() {
3405        return Err(KernelRefusal::internal(KernelStage::Validate, "rebuild.self_intersects", format!(
3406            "the rebuilt body still self-intersects: {}",
3407            report.summary().unwrap_or_else(|| "no summary".into())
3408        )));
3409    }
3410    Ok(solid)
3411}
3412
3413/// One face of a body as its own open body, carrying the edges and vertices
3414/// that face uses and nothing else.
3415fn one_face_body(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, KernelRefusal> {
3416    use crate::topology::ShellRecord;
3417    use rustc_hash::FxHashSet as HashSet;
3418
3419    let face = solid
3420        .shells
3421        .iter()
3422        .flat_map(|shell| &shell.faces)
3423        .find(|face| face.id == face_id)
3424        .ok_or_else(|| {
3425            // The class its one caller gave it before this helper was typed.
3426            KernelRefusal::internal(KernelStage::Fragment, "one_face_body", format!("face {face_id} is not in this body"))
3427        })?
3428        .clone();
3429    let used: HashSet<u64> = face
3430        .loops
3431        .iter()
3432        .flat_map(|loop_record| &loop_record.coedges)
3433        .map(|coedge| coedge.edge_id)
3434        .collect();
3435    let edges: Vec<_> = solid
3436        .edges
3437        .iter()
3438        .filter(|edge| used.contains(&edge.id))
3439        .cloned()
3440        .collect();
3441    let vertices: HashSet<u64> = edges
3442        .iter()
3443        .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
3444        .collect();
3445    Ok(BrepSolid {
3446        mass_properties_cache: Default::default(),
3447        cells: Default::default(),
3448        id: solid.id,
3449        genus: 0,
3450        kind: crate::BodyKind::Solid,
3451        vertices: solid
3452            .vertices
3453            .iter()
3454            .filter(|vertex| vertices.contains(&vertex.id))
3455            .cloned()
3456            .collect(),
3457        edges,
3458        shells: vec![ShellRecord {
3459            id: 1,
3460            faces: vec![face],
3461        }],
3462    })
3463}
3464
3465// ---------------------------------------------------------------------------
3466// The ledger: a repaired result is reported as repaired
3467// ---------------------------------------------------------------------------
3468
3469/// One self-crossing this module REPAIRED rather than refused.
3470///
3471/// A repair changes the answer a lane returns, so it is never silent: the
3472/// feature that ran it carries this in its own result
3473/// (`FeatureResult::notes`), and the case gate records it per case. The
3474/// ledger is the carrier between the two — [`accept_sound`] is called from
3475/// lanes with no diagnostics record to write into, and threading one through
3476/// every lane exit would be a wider change than the repair itself. Same shape
3477/// as the boolean's `CONFORMANCE_REPAIRS` counter and the pcurve fit ledger.
3478#[derive(Clone, Debug)]
3479pub struct CrossingRepairRecord {
3480    /// The two faces that crossed.
3481    pub faces: (u64, u64),
3482    /// The crossing point the scan confirmed.
3483    pub point: crate::Vec3,
3484    /// How far past its band the crossing straddled.
3485    pub straddle: f64,
3486    /// Source faces whose pieces were dropped, with how many pieces of each.
3487    pub dropped: Vec<(u64, usize)>,
3488    /// Source faces the repair RETIRED a leftover from, with how many: a
3489    /// piece the drop left joined to nothing, or a whole loop of a face that
3490    /// nothing else answers for. Either way it is what the OLD configuration
3491    /// left on the body, and the ledger is the only place that says a repair
3492    /// took one off — "a piece was dropped" and "a leftover was retired" are
3493    /// different events.
3494    pub retired: Vec<(u64, usize)>,
3495    /// The faces the drop RE-TRIMMED: neighbours of the dropped material,
3496    /// which are neither of the crossing pair.
3497    pub retrimmed: Vec<u64>,
3498    /// Set when the repair was reached from the HOLE-CONTAINMENT FLOOR rather
3499    /// than from the scan: the face whose hole loop had left it, and that
3500    /// loop. `point` and `straddle` are then unmeasured and read zero, because
3501    /// no crossing was confirmed — the floor convicts a containment, not a
3502    /// crossing.
3503    pub breakout: Option<(u64, u64)>,
3504}
3505
3506impl std::fmt::Display for CrossingRepairRecord {
3507    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3508        if let Some((face, loop_id)) = self.breakout {
3509            return write!(
3510                formatter,
3511                "repaired a BREAKOUT: hole loop {loop_id} of face {face} had left that face \
3512                 onto face {}, so face {} — the wall it bounds — and face {} were split along \
3513                 their carriers' intersection; dropped {} and re-trimmed {}{}",
3514                self.faces.1,
3515                self.faces.0,
3516                self.faces.1,
3517                self.pieces_dropped(),
3518                self.faces_retrimmed(),
3519                self.pieces_retired(),
3520            );
3521        }
3522        write!(
3523            formatter,
3524            "repaired a self-crossing: faces {} and {} crossed near ({:.4}, {:.4}, {:.4}) \
3525             (straddle {:.3e}); split along their carriers' intersection, dropped {} \
3526             and re-trimmed {}{}",
3527            self.faces.0,
3528            self.faces.1,
3529            self.point.x,
3530            self.point.y,
3531            self.point.z,
3532            self.straddle,
3533            self.pieces_dropped(),
3534            self.faces_retrimmed(),
3535            self.pieces_retired(),
3536        )
3537    }
3538}
3539
3540impl CrossingRepairRecord {
3541    fn pieces_dropped(&self) -> String {
3542        self.dropped
3543            .iter()
3544            .map(|(face, count)| format!("{count} piece(s) of face {face}"))
3545            .collect::<Vec<_>>()
3546            .join(", ")
3547    }
3548
3549    fn faces_retrimmed(&self) -> String {
3550        if self.retrimmed.is_empty() {
3551            "no neighbouring face".to_string()
3552        } else {
3553            format!(
3554                "face(s) {}",
3555                self.retrimmed
3556                    .iter()
3557                    .map(|face| face.to_string())
3558                    .collect::<Vec<_>>()
3559                    .join(", ")
3560            )
3561        }
3562    }
3563
3564    /// The clause that says the OLD configuration's leftovers were taken off
3565    /// the body. Empty when nothing was retired, so every message this ledger
3566    /// already printed is unchanged; a repair that retires something says so
3567    /// and names the face, which is the only way the case gate's result line
3568    /// tells a repaired migration from the one it used to return.
3569    fn pieces_retired(&self) -> String {
3570        if self.retired.is_empty() {
3571            return String::new();
3572        }
3573        format!(
3574            ", and RETIRED {} the old configuration left, held by nothing else this repair keeps",
3575            self.retired
3576                .iter()
3577                .map(|(face, count)| format!("{count} leftover(s) of face {face}"))
3578                .collect::<Vec<_>>()
3579                .join(", ")
3580        )
3581    }
3582}
3583
3584thread_local! {
3585    static CROSSING_REPAIRS: std::cell::RefCell<Vec<CrossingRepairRecord>> =
3586        const { std::cell::RefCell::new(Vec::new()) };
3587}
3588
3589/// Take every repair made on this thread since the last call, leaving the
3590/// ledger empty. The history loop drains it after each feature so the repair
3591/// is reported against the feature that made it.
3592pub fn take_crossing_repairs() -> Vec<CrossingRepairRecord> {
3593    CROSSING_REPAIRS.with(|repairs| std::mem::take(&mut *repairs.borrow_mut()))
3594}
3595
3596/// Put a ledger back, replacing whatever is there.
3597///
3598/// The migration route TRIES candidate faces, and a trial that builds records
3599/// itself on the way past. Only the candidate whose body is returned may keep
3600/// its record, and a caller's own entries must survive the trials, so the
3601/// ledger is taken before and written back after. Nothing else may use this:
3602/// a repair reports itself once, where it happened.
3603fn restore_repairs(records: Vec<CrossingRepairRecord>) {
3604    CROSSING_REPAIRS.with(|repairs| *repairs.borrow_mut() = records);
3605}
3606
3607fn record_repair(
3608    fragments: &[crate::fragment::FaceFragmentRecord],
3609    dropped: &[usize],
3610    retired: &[u64],
3611    retrimmed_faces: &[u64],
3612    face_a: u64,
3613    face_b: u64,
3614    origin: &RepairOrigin<'_>,
3615) {
3616    let tally = |faces: &[u64]| {
3617        let mut by_face: Vec<(u64, usize)> = Vec::new();
3618        for face in faces {
3619            match by_face.iter_mut().find(|(existing, _)| existing == face) {
3620                Some((_, count)) => *count += 1,
3621                None => by_face.push((*face, 1)),
3622            }
3623        }
3624        by_face.sort_unstable();
3625        by_face
3626    };
3627    let by_face = tally(
3628        &dropped
3629            .iter()
3630            .map(|index| fragments[*index].source_face_id)
3631            .collect::<Vec<_>>(),
3632    );
3633    let retired = tally(retired);
3634    // A face that lost ONLY a detached piece was not re-trimmed — nothing of
3635    // it was re-cut, a leftover of the old configuration was taken off the
3636    // body — so it is reported under `retired` and not counted here twice.
3637    let mut retrimmed: Vec<u64> = by_face
3638        .iter()
3639        .filter(|(face, count)| {
3640            retired
3641                .iter()
3642                .find(|(retired_face, _)| retired_face == face)
3643                .map(|(_, retired_count)| retired_count < count)
3644                .unwrap_or(true)
3645        })
3646        .map(|(face, _)| *face)
3647        .chain(retrimmed_faces.iter().copied())
3648        .filter(|face| *face != face_a && *face != face_b)
3649        .collect();
3650    retrimmed.sort_unstable();
3651    retrimmed.dedup();
3652    let crossing = match origin {
3653        RepairOrigin::Scan(report) => report.confirmed.first(),
3654        RepairOrigin::Breakout { .. } => None,
3655    };
3656    let record = CrossingRepairRecord {
3657        faces: (face_a, face_b),
3658        point: crossing.map(|crossing| crossing.point).unwrap_or_default(),
3659        straddle: crossing.map(|crossing| crossing.straddle).unwrap_or_default(),
3660        dropped: by_face,
3661        retired,
3662        retrimmed,
3663        breakout: match origin {
3664            RepairOrigin::Scan(_) => None,
3665            RepairOrigin::Breakout { face, loop_id } => Some((*face, *loop_id)),
3666        },
3667    };
3668    if trace_on() {
3669        eprintln!("HEAL {record}");
3670    }
3671    CROSSING_REPAIRS.with(|repairs| repairs.borrow_mut().push(record));
3672}
3673
3674/// Write the flagged body out before the repair touches it, when
3675/// `BREP_HEAL_DUMP` names a directory. A crossing is measured off the body that
3676/// carries it — per-face areas, a mesh-arbitrated genus, the volume the repair
3677/// changes — and replaying a whole document to reach it again costs more than
3678/// the file does. Diagnostics only: the lane still repairs or refuses exactly
3679/// as it would have.
3680fn dump_flagged(solid: &BrepSolid, entry: &str) {
3681    let Ok(directory) = std::env::var("BREP_HEAL_DUMP") else {
3682        return;
3683    };
3684    if directory.is_empty() {
3685        return;
3686    }
3687    let name: String = entry
3688        .chars()
3689        .map(|character| if character.is_alphanumeric() { character } else { '_' })
3690        .collect();
3691    let path = std::path::Path::new(&directory).join(format!("flagged-{name}.json"));
3692    match serde_json::to_string(solid) {
3693        Ok(text) => {
3694            if let Err(error) = std::fs::write(&path, text) {
3695                eprintln!("HEAL dump {}: {error}", path.display());
3696            } else {
3697                eprintln!("HEAL dumped the flagged body to {}", path.display());
3698            }
3699        }
3700        Err(error) => eprintln!("HEAL dump: {error}"),
3701    }
3702}
3703
3704// ---------------------------------------------------------------------------
3705// Trace instrument (BREP_HEAL_TRACE=1)
3706// ---------------------------------------------------------------------------
3707
3708fn trace_on() -> bool {
3709    std::env::var("BREP_HEAL_TRACE").is_ok_and(|value| !value.is_empty() && value != "0")
3710}
3711
3712/// Measurement hatch for the section fit the repair's imprint asks for:
3713/// `BREP_HEAL_FIT_LANE=local|global` picks the lane its ladder starts in and
3714/// `BREP_HEAL_FIT_POINTS=<n>` its station budget. Both unset is the request as
3715/// built, so every lane and budget can be read off ONE binary against the
3716/// shipped one (`crossing_repair_probe body` with `BREP_DEBUG_POLYLINE_FIT=1`).
3717fn measured_fit_request(request: &mut crate::imprint::ImprintOptions) {
3718    match std::env::var("BREP_HEAL_FIT_LANE").as_deref() {
3719        Ok("local") => request.local_fit = true,
3720        Ok("global") => request.local_fit = false,
3721        _ => {}
3722    }
3723    if let Some(points) = std::env::var("BREP_HEAL_FIT_POINTS")
3724        .ok()
3725        .and_then(|value| value.parse::<usize>().ok())
3726        .filter(|points| *points >= 2)
3727    {
3728        request.maximum_fit_points = points;
3729    }
3730    if trace_on() {
3731        eprintln!(
3732            "HEAL imprint request: {} lane, {} stations, tolerance {:.3e}",
3733            if request.local_fit { "local" } else { "global" },
3734            request.maximum_fit_points,
3735            request.tolerance
3736        );
3737    }
3738}
3739
3740fn trace_solid(label: &str, solid: &BrepSolid) {
3741    use rustc_hash::FxHashMap as HashMap;
3742    let point: HashMap<u64, crate::Vec3> = solid
3743        .vertices
3744        .iter()
3745        .map(|vertex| (vertex.id, vertex.point))
3746        .collect();
3747    eprintln!(
3748        "HEAL {label}: {}F {}E {}V shells={}",
3749        solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>(),
3750        solid.edges.len(),
3751        solid.vertices.len(),
3752        solid.shells.len()
3753    );
3754    let mut uses: HashMap<u64, usize> = HashMap::default();
3755    for shell in &solid.shells {
3756        for face in &shell.faces {
3757            for loop_record in &face.loops {
3758                for coedge in &loop_record.coedges {
3759                    *uses.entry(coedge.edge_id).or_default() += 1;
3760                }
3761            }
3762        }
3763    }
3764    for edge in &solid.edges {
3765        let start = point.get(&edge.start_vertex_id).copied().unwrap_or_default();
3766        let end = point.get(&edge.end_vertex_id).copied().unwrap_or_default();
3767        eprintln!(
3768            "HEAL   edge {:>3} uses={} v{}->v{} ({:.4},{:.4},{:.4})->({:.4},{:.4},{:.4}) deg={} '{}'",
3769            edge.id,
3770            uses.get(&edge.id).copied().unwrap_or(0),
3771            edge.start_vertex_id,
3772            edge.end_vertex_id,
3773            start.x, start.y, start.z, end.x, end.y, end.z,
3774            edge.degenerate,
3775            edge.name.as_deref().unwrap_or("")
3776        );
3777    }
3778    for shell in &solid.shells {
3779        for face in &shell.faces {
3780            eprintln!(
3781                "HEAL   face {:>3} loops={} '{}'",
3782                face.id,
3783                face.loops.len(),
3784                face.name.as_deref().unwrap_or("")
3785            );
3786            for (index, loop_record) in face.loops.iter().enumerate() {
3787                let text = loop_record
3788                    .coedges
3789                    .iter()
3790                    .map(|coedge| format!("{}{}", if coedge.forward { '+' } else { '-' }, coedge.edge_id))
3791                    .collect::<Vec<_>>()
3792                    .join(" ");
3793                eprintln!("HEAL     loop {index}: {text}");
3794            }
3795        }
3796    }
3797}
3798
3799fn trace_imprint(imprint: &crate::imprint::ImprintResultRecord) {
3800    eprintln!(
3801        "HEAL imprint: {} piece(s) {} edge_split(s) by_face={:?}",
3802        imprint.pieces.len(),
3803        imprint.edge_splits.len(),
3804        imprint
3805            .by_face
3806            .iter()
3807            .map(|face| (face.operand, face.face_id, face.piece_ids.clone()))
3808            .collect::<Vec<_>>()
3809    );
3810    for piece in &imprint.pieces {
3811        let start = piece.curve.evaluate(piece.t0).unwrap_or_default();
3812        let end = piece.curve.evaluate(piece.t1).unwrap_or_default();
3813        let middle = piece.curve.evaluate((piece.t0 + piece.t1) / 2.0).unwrap_or_default();
3814        eprintln!(
3815            "HEAL   piece {} v{}->v{} ({:.4},{:.4},{:.4}) .. ({:.4},{:.4},{:.4}) .. ({:.4},{:.4},{:.4}) supports={:?} shared={:?}",
3816            piece.id,
3817            piece.start_vertex_id,
3818            piece.end_vertex_id,
3819            start.x, start.y, start.z,
3820            middle.x, middle.y, middle.z,
3821            end.x, end.y, end.z,
3822            piece.support_faces.iter().map(|key| (key.operand, key.face_id)).collect::<Vec<_>>(),
3823            piece.shared_edge,
3824        );
3825    }
3826    for split in &imprint.edge_splits {
3827        eprintln!(
3828            "HEAL   edge_split operand={} edge={} at {:?}",
3829            split.operand, split.edge_id, split.parameters
3830        );
3831    }
3832    for vertex in &imprint.vertices {
3833        eprintln!(
3834            "HEAL   imprint vertex {} ({:.4},{:.4},{:.4})",
3835            vertex.id, vertex.point.x, vertex.point.y, vertex.point.z
3836        );
3837    }
3838}
3839
3840fn trace_fragments(fragments: &[crate::fragment::FaceFragmentRecord]) {
3841    use crate::fragment::FragmentEdgeSource;
3842    eprintln!("HEAL fragments: {}", fragments.len());
3843    for (index, fragment) in fragments.iter().enumerate() {
3844        eprintln!(
3845            "HEAL   fragment {index} face {} loops={} test=({:.4},{:.4},{:.4})",
3846            fragment.source_face_id,
3847            fragment.loops.len(),
3848            fragment.test_point.x,
3849            fragment.test_point.y,
3850            fragment.test_point.z
3851        );
3852        for (loop_index, fragment_loop) in fragment.loops.iter().enumerate() {
3853            let text = fragment_loop
3854                .coedges
3855                .iter()
3856                .map(|coedge| {
3857                    let tag = match &coedge.source {
3858                        FragmentEdgeSource::Boundary { operand, edge_id } => format!("b{operand}:{edge_id}"),
3859                        FragmentEdgeSource::SharedBoundary { operand, edge_id } => format!("s{operand}:{edge_id}"),
3860                        FragmentEdgeSource::Imprint { piece_id } => format!("p{piece_id}"),
3861                        FragmentEdgeSource::Derived { start, end, .. } => format!(
3862                            "d({:.3},{:.3},{:.3})->({:.3},{:.3},{:.3})",
3863                            start.x, start.y, start.z, end.x, end.y, end.z
3864                        ),
3865                    };
3866                    format!("{}{tag}", if coedge.forward { '+' } else { '-' })
3867                })
3868                .collect::<Vec<_>>()
3869                .join(" ");
3870            eprintln!("HEAL     loop {loop_index}: {text}");
3871        }
3872    }
3873}
3874
3875