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::topology::{BrepSolid, LoopRecord};
152use crate::{solid_self_intersections, SelfIntersectionOptions, SelfIntersectionReport};
153
154/// Scan a finished result and either accept it, repair it, or refuse it by
155/// name.
156///
157/// `entry` is the lane's own name, so the refusal reads as that lane's
158/// refusal rather than as a soundness module's.
159pub fn accept_sound(solid: BrepSolid, entry: &str) -> Result<BrepSolid, String> {
160    let report = scan(&solid, entry)?;
161    if !report.is_flagged() {
162        return accept_or_break_out(solid, entry);
163    }
164    dump_flagged(&solid, entry);
165    if let Some(message) = fold_refusal(&report, entry) {
166        return Err(message);
167    }
168    // Two faces crossing: try to split and trim before refusing.
169    let repaired = match resolve_face_crossing(&solid) {
170        Ok(repaired) => repaired,
171        // The crossing lane declined. Where the containment floor ALSO convicts
172        // a hole loop that has left its face, this body may be a BREAKOUT the
173        // other route can take — a mouth that has migrated onto a neighbouring
174        // face crosses that neighbour, so the scan flags it, and the pair the
175        // scan picked is not always the pair the re-trim needs.
176        //
177        // Guarded three ways, because a second chance is exactly how a body
178        // that should refuse gets talked into building. The floor's conviction
179        // is the ticket — a body that merely self-intersects never reaches
180        // here. The success path above is untouched, so a body that repairs
181        // today repairs identically. And the result goes through the same
182        // re-scan and the same floor below as any other repair.
183        Err(reason) if first_hole_breach(&solid).is_some() => {
184            match break_out_until_contained(solid.clone(), entry) {
185                Ok(repaired) => repaired,
186                Err(second) => {
187                    return Err(crossing_refusal(
188                        &report,
189                        entry,
190                        &format!(
191                            "{reason}. The breakout route was offered it too and declined: \
192                             {second}"
193                        ),
194                    ))
195                }
196            }
197        }
198        Err(reason) => return Err(crossing_refusal(&report, entry, &reason)),
199    };
200    let after = scan(&repaired, entry)?;
201    if after.is_flagged() {
202        return Err(crossing_refusal(
203            &report,
204            entry,
205            &format!(
206                "the repair ran and the result still self-intersects — {}",
207                after.summary().unwrap_or_else(|| "no summary".into())
208            ),
209        ));
210    }
211    accept_or_break_out(repaired, entry)
212}
213
214/// How many hole-containment breaches one acceptance may break out, before it
215/// stops and refuses.
216///
217/// A bore has TWO mouths, and a motion big enough to walk one onto the next
218/// face can walk the other too — so one is not enough, and the whole point of a
219/// bound is that a repair which does not actually reduce the breaches cannot
220/// spin. Four leaves room for a two-mouth migration and a re-trim that opens
221/// one more, and stops well short of a body being talked into building by
222/// attrition.
223const MAX_BREAKOUT_ROUNDS: usize = 4;
224
225/// The containment floor, with the BREAKOUT tried before the refusal.
226///
227/// A hole loop that has left its face is not one case but two, and they are
228/// told apart by where it went. A hole that has left ONTO ANOTHER FACE has a
229/// right answer the kernel can build — the moved carrier re-trimmed against the
230/// face it now opens through — and this builds it when the mouth STRADDLES its
231/// face's boundary, which is the reported case. A hole that has left the body
232/// has no such answer, and this refuses exactly as the floor always did, with
233/// the repair's own reason for declining carried after the floor's so the
234/// refusal says which of the two it was.
235///
236/// **A mouth that has migrated WHOLLY off its face is built by the CROSSING
237/// LANE, not by this route.** Until 2026-09-22 it was built by neither: the
238/// departed face kept a hole loop the imprint never cut and the drop
239/// enumeration had no move for retiring one, so every candidate face this
240/// route offered the carrier to declined. The drop closures have that move now
241/// — [`fragment_is_detached`] and [`holds_only_stale_loops`] — and a turned
242/// bore whose exit migrates onto the next face is repaired to its closed form
243/// through [`resolve_face_crossing`], which `accept_sound` reaches first.
244///
245/// What is left for this route to decide is the mouth that has arrived on
246/// NOTHING: a carrier clear of the body cuts no face of it, every candidate
247/// declines, and the refusal names each face it tried and the distance it
248/// tried it at. That is measured, not assumed, by
249/// `a_mouth_that_has_left_the_body_reaches_the_migration_route_and_is_refused_by_name`.
250fn accept_or_break_out(solid: BrepSolid, entry: &str) -> Result<BrepSolid, String> {
251    if refuse_holes_leaving_their_faces(&solid, entry).is_ok() {
252        return Ok(solid);
253    }
254    break_out_until_contained(solid, entry)
255}
256
257/// Break out every hole the containment floor convicts, one at a time, and hand
258/// back a body that BOTH scans clean and passes the floor.
259///
260/// The rounds are what a two-mouth migration needs: repairing one mouth leaves
261/// the other still off its face, and — while it is — the body legitimately
262/// still passes through itself, because the second mouth's wall is still
263/// standing where the material is not. So an intermediate round is allowed to
264/// be flagged; the END of it is not. Nothing is returned that would not have
265/// been accepted had it been built that way in the first place.
266fn break_out_until_contained(solid: BrepSolid, entry: &str) -> Result<BrepSolid, String> {
267    let mut current = solid;
268    for _ in 0..MAX_BREAKOUT_ROUNDS {
269        let Some(breach) = first_hole_breach(&current) else {
270            // Contained. It still has to be SOUND to be returned.
271            let report = scan(&current, entry)?;
272            if report.is_flagged() {
273                return Err(format!(
274                    "{entry}: the breakout repair built a body and it SELF-INTERSECTS — {}",
275                    report.summary().unwrap_or_else(|| "no summary".into()),
276                ));
277            }
278            return Ok(current);
279        };
280        let floor = hole_breach_refusal(&current, &breach, entry);
281        current = repair_hole_breakout(&current, &breach).map_err(|reason| {
282            format!("{floor}. The breakout repair was offered this body and declined it: {reason}")
283        })?;
284    }
285    let breach = first_hole_breach(&current).expect("the loop only falls through on a breach");
286    Err(format!(
287        "{}. {MAX_BREAKOUT_ROUNDS} breakouts were built and a hole is still off its face, so \
288         this stops rather than repairing a body into one nobody asked for",
289        hole_breach_refusal(&current, &breach, entry),
290    ))
291}
292
293/// [`accept_sound`] for a lane whose INPUTS may themselves self-intersect.
294///
295/// A boolean of two bodies that already pass through themselves is entitled to
296/// return a result that does: the defect came in with the operand and refusing
297/// it here would blame the wrong operation. What must never pass is a crossing
298/// the operation ITSELF introduced, so the inputs are scanned only when the
299/// output is flagged — the common case pays for one scan, not three.
300pub fn accept_sound_against(
301    inputs: &[&BrepSolid],
302    solid: BrepSolid,
303    entry: &str,
304) -> Result<BrepSolid, String> {
305    let report = scan(&solid, entry)?;
306    if !report.is_flagged() {
307        return Ok(solid);
308    }
309    for input in inputs {
310        let before = scan(input, entry)?;
311        if before.is_flagged() {
312            // Said plainly rather than silently: the operand arrived broken.
313            return Ok(solid);
314        }
315    }
316    accept_sound(solid, entry)
317}
318
319/// The scan, with a read failure reported as a refusal rather than swallowed:
320/// a result that could not be scanned is not a result that scanned clean.
321fn scan(solid: &BrepSolid, entry: &str) -> Result<SelfIntersectionReport, String> {
322    let options = SelfIntersectionOptions::for_solid(solid);
323    solid_self_intersections(solid, options)
324        .map_err(|message| format!("{entry}: the soundness scan could not run: {message}"))
325}
326
327/// The refusal for a face folded through itself — named, with both feet, the
328/// point and the margins, and pointing at the lane that owns the fix.
329fn fold_refusal(report: &SelfIntersectionReport, entry: &str) -> Option<String> {
330    let fold = report.folds.first()?;
331    Some(format!(
332        "{entry}: the result's face {}{} FOLDS THROUGH ITSELF in 3D — its surface carries \
333         ({:.6}, {:.6}) and ({:.6}, {:.6}), {:.3e} apart in its own domain and both inside \
334         its trim, to the same point ({:.4}, {:.4}, {:.4}) (residual {:.3e}, branches {:.1} \
335         degrees apart){}. The face is wrong in AREA where its volume is not, so nothing that \
336         measures volume notices and `validate()` cannot see it at all. A fold is a trim that \
337         was never constructed: the folded part of the parameter region has to be carved away \
338         where the carrier is built, not repaired out of the finished body.",
339        fold.face,
340        fold.face_name
341            .as_ref()
342            .map(|name| format!(" '{name}'"))
343            .unwrap_or_default(),
344        fold.uv_a[0],
345        fold.uv_a[1],
346        fold.uv_b[0],
347        fold.uv_b[1],
348        fold.uv_separation,
349        fold.point.x,
350        fold.point.y,
351        fold.point.z,
352        fold.residual,
353        fold.normal_angle.to_degrees(),
354        if report.folds.len() > 1 {
355            format!(" ({} folded faces in all)", report.folds.len())
356        } else {
357            String::new()
358        },
359    ))
360}
361
362/// The refusal for two faces crossing, naming every pair and why the repair
363/// did not take it.
364fn crossing_refusal(report: &SelfIntersectionReport, entry: &str, reason: &str) -> String {
365    let pairs = report
366        .confirmed
367        .iter()
368        .map(|crossing| {
369            format!(
370                "faces {} and {} near ({:.4}, {:.4}, {:.4}) (straddle {:.3e} against a band of {:.1e})",
371                crossing.face_a,
372                crossing.face_b,
373                crossing.point.x,
374                crossing.point.y,
375                crossing.point.z,
376                crossing.straddle,
377                report.options.straddle_band
378            )
379        })
380        .collect::<Vec<_>>()
381        .join("; ");
382    format!(
383        "{entry}: the result SELF-INTERSECTS — {pairs}. The enclosed space can still measure \
384         right, so neither a volume oracle nor `validate()` would have caught this. The repair \
385         that splits both faces along their carriers' intersection and drops the enclosed \
386         material did not take it: {reason}"
387    )
388}
389
390// ---------------------------------------------------------------------------
391// The hole-containment floor
392// ---------------------------------------------------------------------------
393
394/// Stations per curved pcurve span when a loop is traced in its face's own
395/// parameters. A straight pcurve contributes its start point only — the next
396/// coedge begins where it ends.
397const LOOP_SAMPLES_PER_SPAN: usize = 32;
398
399/// A loop of a face as a closed polyline in that face's own parameter plane,
400/// traced coedge by coedge along each pcurve (already oriented to its coedge).
401fn loop_parameter_polyline(loop_record: &LoopRecord) -> Result<Vec<(f64, f64)>, String> {
402    pcurve_polyline(loop_record.coedges.iter().map(|coedge| &coedge.pcurve))
403}
404
405/// The same walk for a FRAGMENT's loop, which carries the same pcurves under a
406/// different record. One sampler, so a loop traced during the repair and the
407/// same loop traced by the containment floor are the same polyline.
408fn fragment_loop_polyline(
409    fragment_loop: &crate::fragment::FragmentLoop,
410) -> Result<Vec<(f64, f64)>, String> {
411    pcurve_polyline(fragment_loop.coedges.iter().map(|coedge| &coedge.pcurve))
412}
413
414fn pcurve_polyline<'a>(
415    pcurves: impl Iterator<Item = &'a crate::NurbsCurve>,
416) -> Result<Vec<(f64, f64)>, String> {
417    let mut points = Vec::new();
418    for pcurve in pcurves {
419        let [d0, d1] = pcurve.domain()?;
420        let steps = if pcurve.degree <= 1 && pcurve.control_points.len() == 2 {
421            1
422        } else {
423            let mut spans = 0usize;
424            for window in pcurve.knots.windows(2) {
425                if window[1] > window[0] {
426                    spans += 1;
427                }
428            }
429            spans.max(1) * LOOP_SAMPLES_PER_SPAN
430        };
431        for step in 0..steps {
432            let uv = pcurve.evaluate(d0 + (d1 - d0) * (step as f64 / steps as f64))?;
433            points.push((uv.x, uv.y));
434        }
435    }
436    Ok(points)
437}
438
439fn polyline_area(points: &[(f64, f64)]) -> f64 {
440    let mut twice = 0.0;
441    for index in 0..points.len() {
442        let (x0, y0) = points[index];
443        let (x1, y1) = points[(index + 1) % points.len()];
444        twice += x0 * y1 - x1 * y0;
445    }
446    0.5 * twice
447}
448
449/// The outer loop's segments, bucketed by the band of `v` each one spans.
450///
451/// The floor asks a hole point two questions — even-odd parity against the
452/// outer loop, and whether the point sits within `tolerance` of it — and both
453/// read ONLY the segments whose own `v` range reaches the point's `v`. A
454/// segment that does not reach it has both endpoints on the same side of the
455/// horizontal ray, so it cannot toggle the parity; and every point of it is
456/// further away than `tolerance` in `v` alone, so it cannot be the witness
457/// that the point is on the boundary. Answering out of the band is therefore
458/// the SAME answer as walking the whole loop, not an approximation of it: the
459/// set of segments that can toggle is identical, parity is an XOR and does not
460/// care in what order they come, and the nearest segment — if there is one
461/// within `tolerance` at all — is among those searched.
462///
463/// What changes is the COST. The brute-force walk is `hole points × outer
464/// points`, which on an imported 100-tooth gear's flat is 1.1 million outer
465/// stations against 11 904 hole stations — 1.3e10 pairs on ONE face, 24 s of a
466/// 30 s replay. Through the bands it is `hole points × segments per band`,
467/// plus one linear pass to build.
468///
469/// Laid out CSR — one offsets array and one flat segment array — because a
470/// loop of a million points would otherwise want a million `Vec` headers.
471struct BoundaryBands<'a> {
472    points: &'a [(f64, f64)],
473    /// The bottom of the first band: the loop's lowest `v`.
474    v_lo: f64,
475    /// Bands per unit of `v`. Zero when the loop has no `v` extent to divide,
476    /// in which case there is one band and everything lands in it.
477    bands_per_v: f64,
478    /// `starts[band] .. starts[band + 1]` indexes `segments`.
479    starts: Vec<u32>,
480    /// Segment `i` is `points[i] -> points[(i + 1) % len]`, the closing
481    /// segment included.
482    segments: Vec<u32>,
483}
484
485impl<'a> BoundaryBands<'a> {
486    fn build(points: &'a [(f64, f64)]) -> Self {
487        let count = points.len();
488        debug_assert!(count < u32::MAX as usize, "segment indices are u32");
489        let (mut v_lo, mut v_hi) = (f64::INFINITY, f64::NEG_INFINITY);
490        let mut variation = 0.0;
491        for index in 0..count {
492            let (_, y0) = points[index];
493            let (_, y1) = points[(index + 1) % count];
494            v_lo = v_lo.min(y0);
495            v_hi = v_hi.max(y0);
496            variation += (y1 - y0).abs();
497        }
498        // One band per `variation / count` of `v`. That width is what makes the
499        // index linear in the loop whatever the loop's shape: a segment lands
500        // in `its own v extent / band width + 2` bands, and those sum to about
501        // `3 × count` however the vertical variation is distributed — a gear
502        // tooth's thousands of short segments and a box's four long ones alike.
503        let span = v_hi - v_lo;
504        let bands = if span.is_finite() && span > 0.0 && variation > 0.0 {
505            let wanted = span * count as f64 / variation;
506            if wanted.is_finite() && wanted >= 1.0 {
507                (wanted as usize).min(count.max(1))
508            } else {
509                1
510            }
511        } else {
512            1
513        };
514        let bands_per_v = if bands > 1 { bands as f64 / span } else { 0.0 };
515        let band_of = |v: f64| -> usize {
516            if bands_per_v == 0.0 {
517                return 0;
518            }
519            let raw = (v - v_lo) * bands_per_v;
520            // Written so a NaN takes the first branch rather than falling
521            // through to an `as usize` cast of it.
522            if !(raw > 0.0) {
523                0
524            } else if raw >= (bands - 1) as f64 {
525                bands - 1
526            } else {
527                raw as usize
528            }
529        };
530        let mut starts = vec![0u32; bands + 1];
531        for index in 0..count {
532            let (_, y0) = points[index];
533            let (_, y1) = points[(index + 1) % count];
534            let (lo, hi) = (band_of(y0.min(y1)), band_of(y0.max(y1)));
535            for start in starts[lo + 1..=hi + 1].iter_mut() {
536                *start += 1;
537            }
538        }
539        for band in 1..=bands {
540            starts[band] += starts[band - 1];
541        }
542        let mut cursor = starts.clone();
543        let mut segments = vec![0u32; starts[bands] as usize];
544        for index in 0..count {
545            let (_, y0) = points[index];
546            let (_, y1) = points[(index + 1) % count];
547            let (lo, hi) = (band_of(y0.min(y1)), band_of(y0.max(y1)));
548            for band in lo..=hi {
549                segments[cursor[band] as usize] = index as u32;
550                cursor[band] += 1;
551            }
552        }
553        Self {
554            points,
555            v_lo,
556            bands_per_v,
557            starts,
558            segments,
559        }
560    }
561
562    fn band_count(&self) -> usize {
563        self.starts.len() - 1
564    }
565
566    /// The band `v` falls in, clamped. A `v` outside the loop's own extent
567    /// clamps to an end band, whose segments then fail the tests on their own
568    /// terms — nothing straddles a ray the whole loop sits above or below.
569    fn band_of(&self, v: f64) -> usize {
570        if self.bands_per_v == 0.0 {
571            return 0;
572        }
573        let bands = self.band_count();
574        let raw = (v - self.v_lo) * self.bands_per_v;
575        if !(raw > 0.0) {
576            0
577        } else if raw >= (bands - 1) as f64 {
578            bands - 1
579        } else {
580            raw as usize
581        }
582    }
583
584    fn segment(&self, index: u32) -> ((f64, f64), (f64, f64)) {
585        let index = index as usize;
586        (
587            self.points[index],
588            self.points[(index + 1) % self.points.len()],
589        )
590    }
591
592    /// Even-odd parity: is `(u, v)` inside the closed loop?
593    fn contains(&self, point: (f64, f64)) -> bool {
594        let (u, v) = point;
595        let band = self.band_of(v);
596        let mut inside = false;
597        for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize] {
598            let ((x0, y0), (x1, y1)) = self.segment(index);
599            if (y0 > v) != (y1 > v) && u < x0 + (v - y0) * (x1 - x0) / (y1 - y0) {
600                inside = !inside;
601            }
602        }
603        inside
604    }
605
606    /// Does `(u, v)` sit within `tolerance` of the loop — is it ON the
607    /// boundary rather than off the face?
608    ///
609    /// The bands searched are widened by one on each side of the `tolerance`
610    /// band, so the margin against a segment being excluded by a rounding of
611    /// its own `v` extent is a whole band rather than an ulp.
612    fn within(&self, point: (f64, f64), tolerance: f64) -> bool {
613        let (u, v) = point;
614        let lo = self.band_of(v - tolerance).saturating_sub(1);
615        let hi = (self.band_of(v + tolerance) + 1).min(self.band_count() - 1);
616        for band in lo..=hi {
617            for &index in &self.segments[self.starts[band] as usize..self.starts[band + 1] as usize]
618            {
619                let ((x0, y0), (x1, y1)) = self.segment(index);
620                let (dx, dy) = (x1 - x0, y1 - y0);
621                let length_squared = dx * dx + dy * dy;
622                let t = if length_squared > 0.0 {
623                    (((u - x0) * dx + (v - y0) * dy) / length_squared).clamp(0.0, 1.0)
624                } else {
625                    0.0
626                };
627                let (px, py) = (x0 + t * dx, y0 + t * dy);
628                if ((u - px).powi(2) + (v - py).powi(2)).sqrt() <= tolerance {
629                    return true;
630                }
631            }
632        }
633        false
634    }
635}
636
637/// THE HOLE-CONTAINMENT FLOOR: a PLANAR face whose hole loop has left that
638/// face's own outer loop is refused, naming the hole.
639///
640/// A hole is a hole IN something. Carry a bore's mouth off the flat it opens
641/// through — a direct edit dragging the wall clear of the body — and what the
642/// fixed-topology road emits is the mouth loop hanging in the plane beside the
643/// face it is supposed to perforate, with the wall it bounds somewhere else
644/// entirely. Every other floor is blind to it: `validate()` tests INCIDENCE and
645/// the incidences are all intact; the scan needs two faces to CROSS and the
646/// runaway bore crosses nothing; and the divergence-theorem volume of a
647/// cylinder does not depend on where its axis sits, so the body can measure to
648/// the last bit of the right answer while being the wrong body. The correct
649/// result needs the hole to break through a neighbouring face — a topology
650/// change the direct edits do not make — so this refuses instead of shipping it.
651///
652/// Decided in the face's own parameter plane, which is affine to 3D for a
653/// plane, by parity against the outer loop (the one enclosing the largest
654/// parameter area). A point that fails parity but sits within `tolerance` of
655/// the outer loop is treated as ON it, so a mouth tangent to its face's own
656/// boundary is not convicted by sampling noise. The predicate is only as fine
657/// as the sampling: a curved hole poking out by less than its sampled chord sag
658/// can still pass.
659///
660/// Both questions go through [`BoundaryBands`], which is an INDEX over the
661/// outer loop and not a coarser reading of it: the answers are the ones a walk
662/// of the whole traced polyline gives, off the whole traced polyline, at a
663/// cost that is no longer the product of the two loops' lengths.
664///
665/// This runs at the END of [`accept_sound`], on the body the repair left, so a
666/// crossing the repair can take is still taken — the reason an earlier
667/// primitive-level copy of this floor was removed.
668fn refuse_holes_leaving_their_faces(solid: &BrepSolid, entry: &str) -> Result<(), String> {
669    match first_hole_breach(solid) {
670        Some(breach) => Err(hole_breach_refusal(solid, &breach, entry)),
671        None => Ok(()),
672    }
673}
674
675/// A hole loop the floor has convicted: WHICH face, which loop of it, and the
676/// sample that fell outside, in that face's own parameters.
677///
678/// The floor's verdict is a yes or no; this is the same verdict with the
679/// witness kept, so a lane that can BUILD the right body has something to
680/// build it from. Nothing about the predicate changes by recording it.
681struct HoleBreach {
682    face: u64,
683    loop_index: usize,
684    loop_id: u64,
685    at: (f64, f64),
686}
687
688/// THE FLOOR'S PREDICATE, verbatim — the first hole loop on a planar face that
689/// has left that face's own outer loop, or `None`.
690///
691/// This is the whole of the decision [`refuse_holes_leaving_their_faces`] used
692/// to make inline; the refusal is now written by [`hole_breach_refusal`] off
693/// what this returns. Same loops, same trace, same bands, same tolerance, same
694/// first-hit order.
695fn first_hole_breach(solid: &BrepSolid) -> Option<HoleBreach> {
696    for shell in &solid.shells {
697        for face in &shell.faces {
698            if face.loops.len() < 2 {
699                continue;
700            }
701            if !matches!(
702                face.surface.analytic(),
703                Some(crate::AnalyticSurface::Plane { .. })
704            ) {
705                continue;
706            }
707            let polylines = face
708                .loops
709                .iter()
710                .map(loop_parameter_polyline)
711                .collect::<Result<Vec<_>, String>>()
712                // A loop this cannot be traced from is not a loop this can
713                // convict: the acceptance's own floors already ran.
714                .unwrap_or_default();
715            if polylines.len() != face.loops.len() || polylines.iter().any(Vec::is_empty) {
716                continue;
717            }
718            let outer_index = (0..polylines.len())
719                .max_by(|&a, &b| {
720                    polyline_area(&polylines[a])
721                        .abs()
722                        .total_cmp(&polyline_area(&polylines[b]).abs())
723                })
724                .unwrap_or(0);
725            let outer = &polylines[outer_index];
726            // Every hole point is asked the same two questions against this
727            // one loop, so the loop is indexed ONCE and each question then
728            // reads the band it falls in rather than the whole polyline. The
729            // answers are the ones the whole-polyline walk gives; see
730            // [`BoundaryBands`].
731            let bands = BoundaryBands::build(outer);
732            let tolerance = boundary_tolerance(outer);
733            for (index, hole) in polylines.iter().enumerate() {
734                if index == outer_index {
735                    continue;
736                }
737                let Some(&at) = hole
738                    .iter()
739                    .find(|&&point| !bands.contains(point) && !bands.within(point, tolerance))
740                else {
741                    continue;
742                };
743                return Some(HoleBreach {
744                    face: face.id,
745                    loop_index: index,
746                    loop_id: face.loops[index].id,
747                    at,
748                });
749            }
750        }
751    }
752    None
753}
754
755/// The "on the boundary" band, measured against the outer loop's own parameter
756/// extent so it means the same thing on a 1 mm face and on a 1 m one.
757fn boundary_tolerance(outer: &[(f64, f64)]) -> f64 {
758    let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
759    for &(u, v) in outer {
760        lo = (lo.0.min(u), lo.1.min(v));
761        hi = (hi.0.max(u), hi.1.max(v));
762    }
763    let extent = (hi.0 - lo.0).max(hi.1 - lo.1);
764    (extent * 1e-9).max(1e-12)
765}
766
767/// The floor's refusal, written off the breach it convicted.
768fn hole_breach_refusal(solid: &BrepSolid, breach: &HoleBreach, entry: &str) -> String {
769    let face = solid
770        .shells
771        .iter()
772        .flat_map(|shell| &shell.faces)
773        .find(|face| face.id == breach.face);
774    let label = match face.and_then(|face| face.name.as_deref()) {
775        Some(name) => format!("`{name}` (face {})", breach.face),
776        None => format!("face {}", breach.face),
777    };
778    let (u, v) = breach.at;
779    let where_in_space = face
780        .and_then(|face| face.surface.evaluate(u, v).ok())
781        .map(|point| format!(" — ({:.4}, {:.4}, {:.4}) in space", point.x, point.y, point.z))
782        .unwrap_or_default();
783    format!(
784        "{entry}: hole loop {} of {label} has LEFT that face — it runs outside the \
785         face's own outer boundary, at ({u:.6}, {v:.6}) in the face's parameters{}. \
786         A hole is a hole IN a face; this one bounds a wall that no longer opens \
787         through it, so the right body needs the hole to break through a \
788         neighbouring face, a topology change this operation does not make. \
789         `validate()` cannot see it (every incidence is intact), the \
790         self-intersection scan cannot (nothing crosses), and the volume need not \
791         change at all — refusing rather than returning the wrong body",
792        breach.loop_id, where_in_space,
793    )
794}
795
796// ---------------------------------------------------------------------------
797// The breakout route: a hole that has left its face onto a NAMED neighbour
798// ---------------------------------------------------------------------------
799
800/// The faces a breakout is between: the WALL the convicted hole loop bounds,
801/// and the ONE neighbour of the breached face the hole has run out onto.
802struct BreakoutPair {
803    wall: u64,
804    neighbour: u64,
805}
806
807/// WHICH neighbour the runaway hole has gone onto — read in the breached
808/// face's own parameter plane, where the question is a segment crossing and
809/// not a guess.
810///
811/// The hole loop bounds a wall (for a bore, the bore's own cylindrical face):
812/// exactly one other face answers for its edges, and that is the face whose
813/// carrier has to be re-trimmed. The hole has left through some stretch of the
814/// breached face's OUTER loop, and each outer coedge is shared with exactly one
815/// neighbouring face, so the coedges the hole polyline crosses name the faces
816/// it has gone out onto.
817///
818/// This refuses rather than choosing whenever the answer is not a single pair:
819/// a hole bounding more than one face, a hole that crosses no outer coedge at
820/// all (it has left the face's own parameter patch entirely — the runaway), and
821/// a hole crossing into MORE THAN ONE neighbour, which is a breakout over a
822/// corner and needs both re-trims resolved together rather than one at a time.
823fn breakout_pair(solid: &BrepSolid, breach: &HoleBreach) -> Result<BreakoutPair, String> {
824    let wall = breakout_wall(solid, breach)?;
825    match breakout_neighbours_crossed(solid, breach, wall)?[..] {
826        [neighbour] => Ok(BreakoutPair { wall, neighbour }),
827        [] => Err(NOT_A_STRADDLE.to_string()),
828        ref many => Err(format!(
829            "the runaway hole crosses into {} neighbouring faces at once ({many:?}) — a \
830             breakout over a corner, where the two re-trims bound each other and resolving one \
831             at a time is a different problem from resolving both",
832            many.len(),
833        )),
834    }
835}
836
837/// The marker [`breakout_pair`] returns when the hole does not STRADDLE its
838/// face's boundary. It has either migrated wholly onto another face or left the
839/// body; [`repair_hole_breakout`] tells those apart, by trying.
840const NOT_A_STRADDLE: &str = "the hole does not straddle its own face's boundary";
841
842/// The face the convicted hole loop BOUNDS — the moved carrier, for a bore the
843/// bore's own wall. Exactly one other face may answer for its edges.
844fn breakout_wall(solid: &BrepSolid, breach: &HoleBreach) -> Result<u64, String> {
845    let face = face_by_id(solid, breach.face)?;
846    let mut walls: Vec<u64> = Vec::new();
847    for coedge in &face.loops[breach.loop_index].coedges {
848        for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
849            if other.id == face.id {
850                continue;
851            }
852            if other
853                .loops
854                .iter()
855                .any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
856                && !walls.contains(&other.id)
857            {
858                walls.push(other.id);
859            }
860        }
861    }
862    match walls[..] {
863        [wall] => Ok(wall),
864        _ => Err(format!(
865            "the runaway hole loop bounds {} face(s) ({walls:?}); this route re-trims ONE moved \
866             carrier against the body and cannot decide which of several is the one that moved",
867            walls.len(),
868        )),
869    }
870}
871
872fn face_by_id<'a>(
873    solid: &'a BrepSolid,
874    id: u64,
875) -> Result<&'a crate::FaceRecord, String> {
876    solid
877        .shells
878        .iter()
879        .flat_map(|shell| &shell.faces)
880        .find(|face| face.id == id)
881        .ok_or_else(|| format!("face {id} is not on the body"))
882}
883
884/// The neighbours the hole has run out ONTO, read as segment crossings in the
885/// breached face's own parameter plane.
886///
887/// This answers the STRADDLE: part of the mouth still inside the face it was
888/// recorded on, part outside. It returns nothing for a mouth that has migrated
889/// wholly onto another face — such a loop is entirely outside the outer loop
890/// and crosses none of its coedges — and nothing for one that has left the body
891/// altogether. Those two are told apart downstream, by geometry, in
892/// [`repair_hole_breakout`].
893fn breakout_neighbours_crossed(
894    solid: &BrepSolid,
895    breach: &HoleBreach,
896    wall: u64,
897) -> Result<Vec<u64>, String> {
898    let face = face_by_id(solid, breach.face)?;
899    let polylines = face
900        .loops
901        .iter()
902        .map(loop_parameter_polyline)
903        .collect::<Result<Vec<_>, String>>()
904        .map_err(|error| format!("the breached face's loops could not be traced: {error}"))?;
905    let outer_index = (0..polylines.len())
906        .max_by(|&a, &b| {
907            polyline_area(&polylines[a])
908                .abs()
909                .total_cmp(&polyline_area(&polylines[b]).abs())
910        })
911        .unwrap_or(0);
912    if outer_index == breach.loop_index {
913        return Err("the convicted loop is the face's own outer loop".to_string());
914    }
915    let hole = &polylines[breach.loop_index];
916
917    let mut neighbours: Vec<u64> = Vec::new();
918    for coedge in &face.loops[outer_index].coedges {
919        let side = coedge_parameter_polyline(coedge)
920            .map_err(|error| format!("an outer coedge could not be traced: {error}"))?;
921        if !polylines_cross(hole, &side) {
922            continue;
923        }
924        for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
925            if other.id == face.id || other.id == wall {
926                continue;
927            }
928            if other
929                .loops
930                .iter()
931                .any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
932                && !neighbours.contains(&other.id)
933            {
934                neighbours.push(other.id);
935            }
936        }
937    }
938    Ok(neighbours)
939}
940
941/// Every face across the breached face's OUTER boundary — the faces a mouth
942/// leaving that face can have arrived on.
943///
944/// A migration is a mouth that has walked off one face and onto the next. The
945/// next one is over an edge, so this is the candidate set, and it is small: the
946/// outer loop's own neighbours, the wall excepted. A carrier that has reached
947/// something further away than that is not a case this route claims, and it
948/// says so rather than widening until it finds a face it can cut.
949fn faces_over_the_outer_boundary(
950    solid: &BrepSolid,
951    breach: &HoleBreach,
952    wall: u64,
953) -> Result<Vec<u64>, String> {
954    let face = face_by_id(solid, breach.face)?;
955    let polylines = face
956        .loops
957        .iter()
958        .map(loop_parameter_polyline)
959        .collect::<Result<Vec<_>, String>>()
960        .unwrap_or_default();
961    let outer_index = (0..polylines.len())
962        .max_by(|&a, &b| {
963            polyline_area(&polylines[a])
964                .abs()
965                .total_cmp(&polyline_area(&polylines[b]).abs())
966        })
967        .unwrap_or(0);
968    let mut over: Vec<u64> = Vec::new();
969    for coedge in &face.loops[outer_index].coedges {
970        for other in solid.shells.iter().flat_map(|shell| &shell.faces) {
971            if other.id == face.id || other.id == wall {
972                continue;
973            }
974            if other
975                .loops
976                .iter()
977                .any(|wire| wire.coedges.iter().any(|use_| use_.edge_id == coedge.edge_id))
978                && !over.contains(&other.id)
979            {
980                over.push(other.id);
981            }
982        }
983    }
984    Ok(over)
985}
986
987/// One coedge as a polyline in its face's parameters, END POINT INCLUDED, so
988/// the segments it yields are the whole of that coedge.
989///
990/// [`loop_parameter_polyline`] deliberately leaves each coedge's end point to
991/// the next coedge's start; a single coedge asked about on its own has no next.
992fn coedge_parameter_polyline(
993    coedge: &crate::topology::CoedgeRecord,
994) -> Result<Vec<(f64, f64)>, String> {
995    let pcurve = &coedge.pcurve;
996    let [d0, d1] = pcurve.domain()?;
997    let steps = if pcurve.degree <= 1 && pcurve.control_points.len() == 2 {
998        1
999    } else {
1000        let mut spans = 0usize;
1001        for window in pcurve.knots.windows(2) {
1002            if window[1] > window[0] {
1003                spans += 1;
1004            }
1005        }
1006        spans.max(1) * LOOP_SAMPLES_PER_SPAN
1007    };
1008    let mut points = Vec::with_capacity(steps + 1);
1009    for step in 0..=steps {
1010        let uv = pcurve.evaluate(d0 + (d1 - d0) * (step as f64 / steps as f64))?;
1011        points.push((uv.x, uv.y));
1012    }
1013    Ok(points)
1014}
1015
1016/// Does the CLOSED polyline `loop_points` cross the OPEN polyline `side`?
1017/// Proper segment intersection, in the face's own parameters.
1018fn polylines_cross(loop_points: &[(f64, f64)], side: &[(f64, f64)]) -> bool {
1019    for index in 0..loop_points.len() {
1020        let a = loop_points[index];
1021        let b = loop_points[(index + 1) % loop_points.len()];
1022        for window in side.windows(2) {
1023            if segments_cross(a, b, window[0], window[1]) {
1024                return true;
1025            }
1026        }
1027    }
1028    false
1029}
1030
1031fn segments_cross(a: (f64, f64), b: (f64, f64), c: (f64, f64), d: (f64, f64)) -> bool {
1032    let orient = |p: (f64, f64), q: (f64, f64), r: (f64, f64)| {
1033        (q.0 - p.0) * (r.1 - p.1) - (q.1 - p.1) * (r.0 - p.0)
1034    };
1035    let (d1, d2) = (orient(c, d, a), orient(c, d, b));
1036    let (d3, d4) = (orient(a, b, c), orient(a, b, d));
1037    ((d1 > 0.0) != (d2 > 0.0)) && ((d3 > 0.0) != (d4 > 0.0))
1038}
1039
1040/// THE BREAKOUT, reached from the containment floor instead of from the scan.
1041///
1042/// The floor convicts a hole loop that has left the planar face it is recorded
1043/// on. Where that hole has left ONTO a named neighbour — the case the operator
1044/// reported, an exit hole dragged across the edge between two faces — the right
1045/// body is the one the self-crossing repair already builds for a bore carried
1046/// out through a side wall: the moved carrier and the neighbour split along
1047/// their exact section, the pieces that ended up on the wrong side dropped, and
1048/// the faces left holding an unanswered boundary RE-TRIMMED, which merges the
1049/// mouth into the outer loops of the faces it now opens through.
1050///
1051/// What makes it the SAME repair and not a second one is that the extent is
1052/// still derived and not invented: the tool is the moved carrier trimmed
1053/// against the body, and where it meets the body is what the imprint computes.
1054/// The route in is what changes — the scan needs a mesh crossing it can
1055/// confirm, and at the near edge of a breakout the crossing is a sliver the
1056/// tessellation does not resolve, so the floor sees it first.
1057///
1058/// The genuine runaway is excluded by GEOMETRY and not by a threshold. A bore
1059/// carried clean off the body crosses no boundary of the face it left, so
1060/// [`breakout_pair`] finds no neighbour and this refuses before a single face
1061/// is split; and even given a pair, the imprint of a carrier that misses the
1062/// body cuts neither trim and [`split_and_drop`] declines on its own terms.
1063fn repair_hole_breakout(solid: &BrepSolid, breach: &HoleBreach) -> Result<BrepSolid, String> {
1064    let origin = || RepairOrigin::Breakout {
1065        face: breach.face,
1066        loop_id: breach.loop_id,
1067    };
1068    // THE STRADDLE: part of the mouth still inside the face it was recorded on,
1069    // part outside. Which neighbour is read off the crossing, exactly.
1070    match breakout_pair(solid, breach) {
1071        Ok(BreakoutPair { wall, neighbour }) => {
1072            return split_and_drop(solid, wall, neighbour, origin())
1073        }
1074        Err(reason) if reason != NOT_A_STRADDLE => return Err(reason),
1075        Err(_) => {}
1076    }
1077
1078    // THE MIGRATION, or the RUNAWAY. The mouth is wholly outside the face it
1079    // was recorded on, so nothing in that face's own parameter plane can say
1080    // where it went — a mouth that has walked onto the next face and a mouth
1081    // that has left the solid look the same from there.
1082    //
1083    // They do not look the same to the IMPRINT. The question that separates
1084    // them is whether the moved carrier still meets a face of the body, and
1085    // `split_and_drop` answers exactly that: handed a face the carrier misses
1086    // it declines with "the carriers' intersection reaches ... neither", before
1087    // anything is dropped. So the candidates are tried, and the answer is
1088    // decided BY THE RESULT — the same principle the drop enumeration inside
1089    // already runs on, and a discriminator that cannot drift, because a carrier
1090    // clear of the body cuts nothing anywhere.
1091    //
1092    // More than one candidate building is not an answer: the mouth would be on
1093    // two faces at once with no reason to prefer either, and choosing would be
1094    // the guess this module exists not to make.
1095    let wall = breakout_wall(solid, breach)?;
1096    let mut candidates = faces_over_the_outer_boundary(solid, breach, wall)?;
1097    if candidates.is_empty() {
1098        return Err(RUNAWAY.to_string());
1099    }
1100
1101    // WHICH face the mouth moved onto, when its own face's parameter plane can
1102    // no longer say. The carrier is a whole surface and generally meets the
1103    // body in more than one place — a through bore leaves by one face and
1104    // enters by another, and both are faces this carrier can be re-trimmed
1105    // against — so "some face it cuts" is not the answer. The answer is the
1106    // face the MIGRATED MOUTH IS AT: the candidates are ordered by the least
1107    // 3D distance from the convicted hole loop to the face's own trim, and the
1108    // nearest one that the repair can actually build is taken.
1109    //
1110    // A TIE is refused rather than broken. Two faces the same distance from the
1111    // mouth means the mouth sits on the edge between them, which is a corner
1112    // breakout — the case whose two re-trims bound each other.
1113    let mouth = hole_loop_in_space(solid, breach)?;
1114    let scale = crate::solid_scale(solid);
1115    let mut ranked: Vec<(f64, u64)> = Vec::new();
1116    for candidate in candidates.drain(..) {
1117        ranked.push((distance_to_face(solid, candidate, &mouth)?, candidate));
1118    }
1119    ranked.sort_by(|a, b| a.0.total_cmp(&b.0));
1120    if ranked.len() > 1 && (ranked[1].0 - ranked[0].0).abs() <= scale * 1e-9 {
1121        return Err(format!(
1122            "the migrated mouth is the same distance ({:.6}) from faces {} and {}, so the body \
1123             does not say which of them it moved onto — a breakout over the corner between \
1124             them, where the two re-trims bound each other",
1125            ranked[0].0, ranked[0].1, ranked[1].1,
1126        ));
1127    }
1128
1129    let carried = take_crossing_repairs();
1130    let mut declined: Vec<String> = Vec::new();
1131    for (distance, candidate) in ranked {
1132        match split_and_drop(solid, wall, candidate, origin()) {
1133            Ok(body) => {
1134                // The trial that built pushed a record; the ones that declined
1135                // pushed none. Keep that one and put the caller's ledger back.
1136                let mine = take_crossing_repairs();
1137                restore_repairs(carried.into_iter().chain(mine).collect());
1138                return Ok(body);
1139            }
1140            Err(reason) => {
1141                declined.push(format!("face {candidate} at {distance:.6}: {reason}"))
1142            }
1143        }
1144    }
1145    restore_repairs(carried);
1146    Err(format!(
1147        "{RUNAWAY} — the moved carrier was offered every face across that boundary, nearest \
1148         first, and cut none of them ({})",
1149        declined.join(" | ")
1150    ))
1151}
1152
1153/// The convicted hole loop as 3D points: its pcurves are in the breached face's
1154/// own parameters, so they are read back through that face's surface — the same
1155/// road the floor takes, and the reason the floor was never wrong about where a
1156/// loop is.
1157fn hole_loop_in_space(solid: &BrepSolid, breach: &HoleBreach) -> Result<Vec<crate::Vec3>, String> {
1158    let face = face_by_id(solid, breach.face)?;
1159    let uv = loop_parameter_polyline(&face.loops[breach.loop_index])?;
1160    uv.into_iter()
1161        .map(|(u, v)| face.surface.evaluate(u, v))
1162        .collect::<Result<Vec<_>, _>>()
1163        .map_err(|error| format!("the convicted hole loop could not be read in space: {error}"))
1164}
1165
1166/// The least distance from a set of points to a face's own TRIM — sampled, in
1167/// the face's parameters, so a point beside a face's carrier but off the end of
1168/// the face does not read as being on it.
1169fn distance_to_face(
1170    solid: &BrepSolid,
1171    face_id: u64,
1172    points: &[crate::Vec3],
1173) -> Result<f64, String> {
1174    const STATIONS: usize = 32;
1175    let face = face_by_id(solid, face_id)?;
1176    let polylines = face
1177        .loops
1178        .iter()
1179        .map(loop_parameter_polyline)
1180        .collect::<Result<Vec<_>, String>>()?;
1181    let outer_index = (0..polylines.len())
1182        .max_by(|&a, &b| {
1183            polyline_area(&polylines[a])
1184                .abs()
1185                .total_cmp(&polyline_area(&polylines[b]).abs())
1186        })
1187        .unwrap_or(0);
1188    let bands = BoundaryBands::build(&polylines[outer_index]);
1189    let (mut lo, mut hi) = ((f64::MAX, f64::MAX), (f64::MIN, f64::MIN));
1190    for &(u, v) in &polylines[outer_index] {
1191        lo = (lo.0.min(u), lo.1.min(v));
1192        hi = (hi.0.max(u), hi.1.max(v));
1193    }
1194    let mut best = f64::INFINITY;
1195    for iu in 0..=STATIONS {
1196        for iv in 0..=STATIONS {
1197            let u = lo.0 + (hi.0 - lo.0) * iu as f64 / STATIONS as f64;
1198            let v = lo.1 + (hi.1 - lo.1) * iv as f64 / STATIONS as f64;
1199            if !bands.contains((u, v)) {
1200                continue;
1201            }
1202            if polylines
1203                .iter()
1204                .enumerate()
1205                .any(|(index, hole)| index != outer_index && inside_polyline((u, v), hole))
1206            {
1207                continue;
1208            }
1209            let Ok(point) = face.surface.evaluate(u, v) else {
1210                continue;
1211            };
1212            for probe in points {
1213                best = best.min(probe.sub(point).length());
1214            }
1215        }
1216    }
1217    Ok(best)
1218}
1219
1220/// Even-odd parity against a closed polyline, for the hole loops the band index
1221/// is not built over.
1222fn inside_polyline(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
1223    let (u, v) = point;
1224    let mut parity = false;
1225    for index in 0..polygon.len() {
1226        let (ax, ay) = polygon[index];
1227        let (bx, by) = polygon[(index + 1) % polygon.len()];
1228        if (ay > v) != (by > v) {
1229            let x = ax + (v - ay) / (by - ay) * (bx - ax);
1230            if x > u {
1231                parity = !parity;
1232            }
1233        }
1234    }
1235    parity
1236}
1237
1238/// The sentence a hole that has left the BODY earns, as opposed to one that has
1239/// moved onto another face. It is the one thing standing between the operator
1240/// and a silently ruined model, so it is a constant and not a phrasing.
1241const RUNAWAY: &str = "the runaway hole has left the face it was recorded on without arriving \
1242     on another: the moved carrier meets no face of the body, so there is nothing for it to be \
1243     re-trimmed against and no cut to perform at any extent";
1244
1245/// [`resolve_face_crossing`] for a pair this module has named itself rather
1246/// than read off the scan.
1247///
1248/// The scan is one way to find two faces of one body that have to be split
1249/// along their section, and the containment floor is another. Both hand the
1250/// same pair to the same repair.
1251pub fn resolve_face_crossing_between(
1252    solid: &BrepSolid,
1253    face_a: u64,
1254    face_b: u64,
1255) -> Result<BrepSolid, String> {
1256    let report = scan(solid, "resolve_face_crossing_between")?;
1257    split_and_drop(solid, face_a, face_b, RepairOrigin::Scan(&report))
1258}
1259
1260// ---------------------------------------------------------------------------
1261// The repair
1262// ---------------------------------------------------------------------------
1263
1264/// WHAT FOUND the pair this repair is about to split.
1265///
1266/// Two floors reach the same repair. The scan finds two faces whose meshes
1267/// cross and can say where and by how much; the hole-containment floor finds a
1268/// hole loop that has left its face and can say which loop and which face. The
1269/// repair does the same work either way, and the ledger says which asked for
1270/// it rather than printing a crossing point that was never measured.
1271enum RepairOrigin<'a> {
1272    /// The self-intersection scan confirmed a crossing pair.
1273    Scan(&'a SelfIntersectionReport),
1274    /// The containment floor convicted a hole loop that has broken out onto a
1275    /// neighbouring face.
1276    Breakout { face: u64, loop_id: u64 },
1277}
1278
1279/// The operand tag this repair works under. The solid is its own and only
1280/// operand: what is being resolved is a body crossing ITSELF, so there is no
1281/// second operand to be the other side of.
1282const SELF_OPERAND: u8 = 0;
1283
1284/// How many pieces the crossing may cut the two faces into before this repair
1285/// declines. Every way of dropping some of them is REBUILT and re-checked, so
1286/// the enumeration has to stay small enough to run in full: at eight pieces
1287/// there are at most 225 candidate drops, nearly all of which the re-trim
1288/// closure ends before anything is assembled. The variable-radius fillet's
1289/// crossing cuts four pieces and the two-bore fixture's cuts six.
1290const MAXIMUM_CROSSING_PIECES: usize = 8;
1291
1292/// Split two faces of ONE body that cross each other along their carriers'
1293/// exact intersection, drop the material the crossing encloses, and re-sew.
1294///
1295/// This is the bounded half of the self-intersection answer. Where two walls of
1296/// one body overshoot past each other instead of meeting at an edge, the
1297/// crossing curve IS the edge they should have met at: imprinting it onto both
1298/// faces cuts each into the part that belongs and the tail that overshot, and
1299/// dropping both tails leaves a body sewn along that curve.
1300///
1301/// **What it refuses, and why each refusal is a bound rather than a shrug:**
1302///
1303/// * A FOLD inside one face. There is no second face to imprint against, and
1304///   the answer is a trim that was never constructed rather than material to
1305///   drop — `offset/carve.rs` owns it.
1306/// * More than one crossing pair. Each repair changes the faces the next one
1307///   would be measured against, so resolving them together is a different
1308///   problem from resolving one.
1309/// * A crossing whose carriers' intersection does not cut BOTH trims. Then the
1310///   mesh crossing is not a curve the faces share and there is nothing to split
1311///   along.
1312/// * More pieces than [`MAXIMUM_CROSSING_PIECES`], because every candidate drop
1313///   is rebuilt in full and the enumeration has to run to the end rather than
1314///   be sampled.
1315/// * Any split whose reassembly does not come back SOUND. Every way of dropping
1316///   some pieces of each face — keeping at least one of each — is enumerated,
1317///   closed under the re-trim, rebuilt and re-checked in full (`validate()`
1318///   clean, connected, and scanning clean), so the piece choice is decided by
1319///   the result rather than guessed from a normal. If none verifies, the solid
1320///   is returned untouched and the caller refuses: a repair that cannot prove
1321///   its own answer must not ship one.
1322/// * A split that MORE THAN ONE drop resolves. Two sound answers is not an
1323///   answer, and choosing between them would be the guess this repair exists
1324///   not to make.
1325///
1326/// **The re-trim is the other half of the split.** Dropping a tail also removes
1327/// that tail's boundary uses on the NEIGHBOURING faces it bordered, and a body
1328/// cannot close over an edge only one face answers for. So the drop is closed
1329/// under "a face left holding an unmatched boundary goes too" — which, since
1330/// the imprint already cut those neighbours where the crossing curve met their
1331/// edges, drops the part of each that the crossing cut off. The closure is
1332/// bounded by the faces the imprint TOUCHED: reaching any other face ends that
1333/// candidate. Nothing here edits a loop by hand; every piece it chooses between
1334/// came out of the imprint/fragment machinery.
1335///
1336/// **And the drop RETIRES what the old configuration left.** The touched bound
1337/// is about material the crossing encloses, and it does not reach the third
1338/// thing a moved carrier leaves on a body: a piece, or a whole loop, that the
1339/// NEW configuration has no room for — a bore's exit ring still recorded on the
1340/// top face after the exit has migrated onto the side. Neither is material this
1341/// drop ran past; both are held by nothing the drop keeps, and keeping either
1342/// cannot close. [`fragment_is_detached`] retires the one the fragmenter
1343/// returns as its own island and [`holds_only_stale_loops`] the one it returns
1344/// as a hole loop that has left its face, and the ledger says a repair retired
1345/// something rather than leaving it to be read off the cell counts.
1346///
1347/// **A census has two directions and only one was enforced.** An edge used ONCE
1348/// is a face left holding an unanswered boundary, above. An edge used THREE
1349/// times is three faces along one curve, which is not a body either, and the
1350/// assembly does not refuse it — it puts the third face on a shell of its own
1351/// and every floor in this repository then passes the result.
1352/// [`over_used_edge`] refuses that candidate where the third use still exists,
1353/// which is here. It is what stopped the turned-bore migration returning a body
1354/// 23% out on area.
1355pub fn resolve_face_crossing(solid: &BrepSolid) -> Result<BrepSolid, String> {
1356    let options = SelfIntersectionOptions::for_solid(solid);
1357    let report = solid_self_intersections(solid, options)
1358        .map_err(|message| format!("the soundness scan could not run: {message}"))?;
1359    if let Some(fold) = report.folds.first() {
1360        return Err(format!(
1361            "face {} folds through ITSELF; a fold is a trim that was never constructed and \
1362             belongs to the carrier carve, not to this repair",
1363            fold.face
1364        ));
1365    }
1366    if report.confirmed.is_empty() {
1367        return Ok(solid.clone());
1368    }
1369    if report.confirmed.len() > 1 {
1370        return Err(format!(
1371            "{} crossing face pairs; each repair changes the faces the next would be measured \
1372             against, so this repair takes one pair at a time",
1373            report.confirmed.len()
1374        ));
1375    }
1376    let crossing = &report.confirmed[0];
1377    split_and_drop(solid, crossing.face_a, crossing.face_b, RepairOrigin::Scan(&report))
1378}
1379
1380/// One crossing pair, split and resolved. Separated from the scan so the
1381/// refusals above read as one list.
1382fn split_and_drop(
1383    solid: &BrepSolid,
1384    face_a: u64,
1385    face_b: u64,
1386    origin: RepairOrigin<'_>,
1387) -> Result<BrepSolid, String> {
1388    use crate::fragment::fragment_solid;
1389    use crate::imprint::{FaceKey, ImprintOptions};
1390    use crate::{apply_edge_splits, build_imprints};
1391
1392    let scale = crate::solid_scale(solid);
1393    let tolerance = (scale * 1e-7).max(1e-9);
1394    if trace_on() {
1395        eprintln!("HEAL split_and_drop: faces {face_a} | {face_b}, scale {scale:.6}");
1396        trace_solid("entry solid", solid);
1397    }
1398
1399    // Each face as its own one-face open body, which is what the imprint
1400    // driver pairs: it walks operand A's faces against operand B's, and a
1401    // solid paired with itself would pair every face with itself first.
1402    let open_a = one_face_body(solid, face_a)?;
1403    let open_b = one_face_body(solid, face_b)?;
1404    let mut request = ImprintOptions {
1405        tolerance,
1406        // The GLOBAL interpolation lane: the section the repair cuts along is
1407        // the faithful one. On the only row this repair takes
1408        // (`FilletFailureWith2RasiusValues`) the global lane's section lies
1409        // 1.6e-6 from its two carriers where the local lane's lies 7.8e-4, and
1410        // it moves every face the section bounds onto its closed form (`Pin_S`
1411        // 1.6e-4 -> 4.4e-7). The local lane was asked for until
1412        // `process_curve` split an edge a section ENDS on whatever the angle:
1413        // that section is tangent to the bore's mouth ring at both ends, the
1414        // global fit reproduces the tangency, and the old gates left the ring
1415        // whole, so the repair succeeded only on the local fit's 1.4-degree
1416        // end-tangent error. `BREP_HEAL_FIT_LANE` re-measures it.
1417        local_fit: false,
1418        // The station budget is the imprint's own (the ladder climbs it to
1419        // twice that). `96` was `offset_shell`'s: every section this repair
1420        // reaches is a sparse march (68 points on the fillet row, 7 and 8 on
1421        // `BadBoolean`) that saturates either budget, bit-identically.
1422        ..ImprintOptions::default()
1423    };
1424    measured_fit_request(&mut request);
1425    let mut imprint = build_imprints(&open_a, &open_b, &request)
1426        .map_err(|error| format!("the two carriers could not be imprinted: {error}"))?;
1427
1428    // Both faces belong to ONE body, so both sides of the imprint are rewritten
1429    // onto that body's operand. The face ids are the solid's own and cannot
1430    // collide, so this is a relabel and not a merge.
1431    for face in &mut imprint.by_face {
1432        face.operand = SELF_OPERAND;
1433    }
1434    for split in &mut imprint.edge_splits {
1435        split.operand = SELF_OPERAND;
1436    }
1437    for piece in &mut imprint.pieces {
1438        for support in &mut piece.support_faces {
1439            *support = FaceKey {
1440                operand: SELF_OPERAND,
1441                face_id: support.face_id,
1442            };
1443        }
1444        for pcurve in &mut piece.pcurves {
1445            pcurve.operand = SELF_OPERAND;
1446        }
1447    }
1448    for barrier in &mut imprint.barrier_edges {
1449        barrier.0 = SELF_OPERAND;
1450    }
1451
1452    if trace_on() {
1453        trace_imprint(&imprint);
1454    }
1455
1456    let cut_a = imprint
1457        .by_face
1458        .iter()
1459        .any(|face| face.face_id == face_a && !face.piece_ids.is_empty());
1460    let cut_b = imprint
1461        .by_face
1462        .iter()
1463        .any(|face| face.face_id == face_b && !face.piece_ids.is_empty());
1464    if !cut_a || !cut_b {
1465        return Err(format!(
1466            "the carriers' intersection reaches {} of the two trims, so there is no shared \
1467             curve to split along",
1468            match (cut_a, cut_b) {
1469                (true, false) => format!("only face {face_a}"),
1470                (false, true) => format!("only face {face_b}"),
1471                _ => "neither".into(),
1472            }
1473        ));
1474    }
1475
1476    let split = apply_edge_splits(solid, SELF_OPERAND, &imprint)
1477        .map_err(|error| format!("the imprinted edge splits could not be applied: {error}"))?;
1478    let fragments = fragment_solid(&split, SELF_OPERAND, &imprint)
1479        .map_err(|error| format!("the split faces could not be fragmented: {error}"))?;
1480
1481    if trace_on() {
1482        trace_solid("split solid", &split);
1483        trace_fragments(&fragments);
1484    }
1485
1486    // The faces the imprint TOUCHED: the two that cross, plus every face whose
1487    // own loop carries an edge the imprint split. Those are exactly the faces
1488    // the dropped material can border — the crossing curve ends ON a boundary
1489    // edge of each crossing face, and that edge is shared with a neighbour — so
1490    // they are the only faces this repair may re-trim. Anything beyond them is
1491    // material this repair has not been shown to own, and reaching one ends the
1492    // candidate rather than widening the drop.
1493    let touched = touched_faces(solid, &imprint, face_a, face_b);
1494    // A POLE edge is legitimately used once — it is a collapsed boundary, not a
1495    // shared one — so it never means the face beside it bordered dropped
1496    // material. Left in the census it would drop the fragment holding it, and
1497    // the cascade would refuse a body the repair could have resolved.
1498    let degenerate: rustc_hash::FxHashSet<EdgeKey> = split
1499        .edges
1500        .iter()
1501        .filter(|edge| edge.degenerate)
1502        .map(|edge| EdgeKey::Boundary(SELF_OPERAND, edge.id))
1503        .collect();
1504
1505    let pieces_of = |face: u64| -> Vec<usize> {
1506        fragments
1507            .iter()
1508            .enumerate()
1509            .filter_map(|(index, fragment)| (fragment.source_face_id == face).then_some(index))
1510            .collect()
1511    };
1512    let pieces_a = pieces_of(face_a);
1513    let pieces_b = pieces_of(face_b);
1514    // How many pieces the section has to cut, and it is not the same question
1515    // on both roads.
1516    //
1517    // From the SCAN, two faces overshoot PAST each other and the crossing curve
1518    // is the edge they should have met at, so it runs across both trims and
1519    // each has a tail: both sides must come out in two or more pieces, and one
1520    // piece is a section that grazed a trim rather than cut it.
1521    //
1522    // From the containment floor, the shape is different and the difference is
1523    // the whole of a COMPLETED MIGRATION. When a bore's mouth has walked
1524    // entirely onto the next face, the moved carrier meets that face in a
1525    // CLOSED loop inside its trim — the new mouth — which cuts nothing off it;
1526    // the face comes back as one piece carrying a new hole, and the only tail
1527    // is the wall's. Demanding two pieces of both would refuse exactly the case
1528    // the operator asked for, so this road asks for two pieces on ONE side and
1529    // at least one on the other. Nothing about the enumeration changes: a face
1530    // with one piece has one drop set, the empty one.
1531    let (need_a, need_b) = match &origin {
1532        RepairOrigin::Scan(_) => (2, 2),
1533        RepairOrigin::Breakout { .. } => {
1534            if pieces_a.len().max(pieces_b.len()) >= 2 {
1535                (1, 1)
1536            } else {
1537                (2, 2)
1538            }
1539        }
1540    };
1541    if pieces_a.len() < need_a || pieces_b.len() < need_b {
1542        return Err(format!(
1543            "the crossing cut face {face_a} into {} piece(s) and face {face_b} into {} — this \
1544             repair resolves the case where the curve cuts BOTH trims, so one part of each is \
1545             the tail that overshot",
1546            pieces_a.len(),
1547            pieces_b.len()
1548        ));
1549    }
1550    if pieces_a.len() + pieces_b.len() > MAXIMUM_CROSSING_PIECES {
1551        return Err(format!(
1552            "the crossing cut face {face_a} into {} piece(s) and face {face_b} into {}, which is \
1553             {} pieces against this repair's budget of {MAXIMUM_CROSSING_PIECES} — every way to \
1554             drop some of them is rebuilt and re-checked, so the enumeration is bounded rather \
1555             than sampled",
1556            pieces_a.len(),
1557            pieces_b.len(),
1558            pieces_a.len() + pieces_b.len(),
1559        ));
1560    }
1561
1562    // WHICH pieces are the tails is decided by the RESULT, not by a normal.
1563    // Every way to drop some pieces of each crossing face (keeping at least one
1564    // of each) is closed under the re-trim below, rebuilt in full and
1565    // re-checked; the repair is the answer only when exactly ONE of them
1566    // verifies.
1567    //
1568    // The enumeration runs under TWO closures, and the second only where the
1569    // first found nothing at all. [`DropClosure::Drop`] drops a neighbour the
1570    // drop leaves holding an unmatched boundary — the rule this repair has
1571    // always used, and the one every body it already resolves is decided by.
1572    // [`DropClosure::Retrim`] RE-TRIMS that neighbour instead. Ordering them
1573    // this way makes the second closure strictly additive: a body the first
1574    // resolves never reaches it, so no answer this repair already returns can
1575    // change. The price is the enumeration twice over on a body that refuses.
1576    let plan = DropPlan {
1577        split: &split,
1578        fragments: &fragments,
1579        imprint: &imprint,
1580        touched: &touched,
1581        degenerate: &degenerate,
1582        pieces_a: &pieces_a,
1583        pieces_b: &pieces_b,
1584        face_a,
1585        face_b,
1586        scale,
1587    };
1588    let (mut verified, refusals) = plan.enumerate(DropClosure::Drop);
1589    if verified.is_empty() {
1590        verified = plan.enumerate(DropClosure::Retrim).0;
1591    }
1592
1593    if verified.len() > 1 {
1594        return Err(format!(
1595            "{} different drops of the crossing pieces all rebuilt into a sound body, so the \
1596             result does not decide which material the crossing encloses; this repair answers \
1597             only where one does",
1598            verified.len()
1599        ));
1600    }
1601    let Some(Candidate {
1602        dropped,
1603        retired,
1604        retrimmed,
1605        solid: repaired,
1606    }) = verified.pop()
1607    else {
1608        // The first few reasons, not all of them: eight pieces is up to 225
1609        // candidates and a refusal nobody can read is a refusal nobody uses.
1610        let listed = refusals
1611            .iter()
1612            .take(4)
1613            .cloned()
1614            .collect::<Vec<_>>()
1615            .join(" | ");
1616        return Err(format!(
1617            "none of the {} ways of dropping the crossing pieces rebuilt into a sound body \
1618             ({listed}{}); the worst crossing was {:.3e} past its band, so the overshoot is \
1619             real and it is the SPLIT that did not resolve it",
1620            refusals.len(),
1621            if refusals.len() > 4 {
1622                format!(" | and {} more", refusals.len() - 4)
1623            } else {
1624                String::new()
1625            },
1626            match &origin {
1627                RepairOrigin::Scan(report) => report
1628                    .confirmed
1629                    .first()
1630                    .map(|crossing| crossing.straddle)
1631                    .unwrap_or_default(),
1632                RepairOrigin::Breakout { .. } => 0.0,
1633            }
1634        ));
1635    };
1636
1637    record_repair(
1638        &fragments,
1639        &dropped,
1640        &retired,
1641        &retrimmed,
1642        face_a,
1643        face_b,
1644        &origin,
1645    );
1646    Ok(repaired)
1647}
1648
1649/// One candidate answer: the fragments the drop removed, the ones it RETIRED
1650/// as detached, the faces it RE-TRIMMED rather than removed, and the body
1651/// those choices rebuilt into.
1652struct Candidate {
1653    dropped: Vec<usize>,
1654    retired: Vec<u64>,
1655    retrimmed: Vec<u64>,
1656    solid: BrepSolid,
1657}
1658
1659/// How a candidate drop closes over a neighbour it leaves holding a boundary
1660/// no other face answers for.
1661#[derive(Clone, Copy, Eq, PartialEq)]
1662enum DropClosure {
1663    /// DROP that neighbour too, iterated to a fixed point — [`close_drop_set`].
1664    Drop,
1665    /// RE-TRIM it — [`retrim_drop_set`]: take the unmatched coedges off it and
1666    /// re-link what remains into closed loops on the same carrier, with the
1667    /// same pcurves. The bound is the same one (a face the imprint never cut
1668    /// still ends the candidate); what changes is that a face it DID cut is
1669    /// re-trimmed rather than consumed.
1670    Retrim,
1671}
1672
1673/// One crossing pair's split and everything a candidate drop is measured
1674/// against. Bundled so the enumeration can run once per closure.
1675struct DropPlan<'a> {
1676    split: &'a BrepSolid,
1677    fragments: &'a [crate::fragment::FaceFragmentRecord],
1678    imprint: &'a crate::imprint::ImprintResultRecord,
1679    touched: &'a rustc_hash::FxHashSet<u64>,
1680    degenerate: &'a rustc_hash::FxHashSet<EdgeKey>,
1681    pieces_a: &'a [usize],
1682    pieces_b: &'a [usize],
1683    face_a: u64,
1684    face_b: u64,
1685    scale: f64,
1686}
1687
1688impl DropPlan<'_> {
1689    /// Every way of dropping some pieces of each crossing face, closed under
1690    /// `closure`, rebuilt in full and re-checked. The candidates that verified
1691    /// come back first, the reasons the rest did not second.
1692    fn enumerate(&self, closure: DropClosure) -> (Vec<Candidate>, Vec<String>) {
1693        let (face_a, face_b) = (self.face_a, self.face_b);
1694        let mut refusals: Vec<String> = Vec::new();
1695        let mut verified: Vec<Candidate> = Vec::new();
1696        let mut seen: Vec<(Vec<usize>, Vec<Vec<usize>>)> = Vec::new();
1697        for drop_a in proper_subsets(self.pieces_a) {
1698            for drop_b in proper_subsets(self.pieces_b) {
1699                let mut dropped: Vec<usize> =
1700                    drop_a.iter().chain(drop_b.iter()).copied().collect();
1701                if dropped.is_empty() {
1702                    continue;
1703                }
1704                let label = format!(
1705                    "dropping piece(s) {:?} of face {face_a} and {:?} of face {face_b}",
1706                    drop_a
1707                        .iter()
1708                        .map(|index| self
1709                            .pieces_a
1710                            .iter()
1711                            .position(|piece| piece == index)
1712                            .unwrap_or(0))
1713                        .collect::<Vec<_>>(),
1714                    drop_b
1715                        .iter()
1716                        .map(|index| self
1717                            .pieces_b
1718                            .iter()
1719                            .position(|piece| piece == index)
1720                            .unwrap_or(0))
1721                        .collect::<Vec<_>>(),
1722                );
1723                // The re-trim: a face that bordered dropped material is left
1724                // with a boundary use no other face answers, and a body cannot
1725                // close over one. Closing the drop under that rule is what
1726                // turns a two-face split into a re-trim of every face the
1727                // dropped material bordered.
1728                // The dropping closure leaves every fragment as it was, so it
1729                // borrows the list rather than copying it per candidate.
1730                let mut retired: Vec<u64> = Vec::new();
1731                let closed = match closure {
1732                    DropClosure::Drop => close_drop_set(
1733                        self.fragments,
1734                        &mut dropped,
1735                        &mut retired,
1736                        self.touched,
1737                        self.degenerate,
1738                    )
1739                    .map(|()| (None, Vec::new())),
1740                    DropClosure::Retrim => retrim_drop_set(
1741                        self.fragments,
1742                        &mut dropped,
1743                        &mut retired,
1744                        self.touched,
1745                        self.degenerate,
1746                    )
1747                    .map(|(working, retrimmed)| (Some(working), retrimmed)),
1748                };
1749                let (working, retrimmed) = match closed {
1750                    Ok(closed) => closed,
1751                    Err(reason) => {
1752                        if trace_on() {
1753                            println!("    candidate died at closure {label}: {reason}");
1754                        }
1755                        refusals.push(format!("{label}: {reason}"));
1756                        continue;
1757                    }
1758                };
1759                let working: &[crate::fragment::FaceFragmentRecord] =
1760                    working.as_deref().unwrap_or(self.fragments);
1761                dropped.sort_unstable();
1762                // Two seeds can close onto the same answer. The signature is
1763                // the drop AND what the re-trim left of each kept fragment, so
1764                // a second closure that re-trimmed differently is a different
1765                // candidate rather than a hidden duplicate.
1766                let signature = (
1767                    dropped.clone(),
1768                    working
1769                        .iter()
1770                        .enumerate()
1771                        .filter(|(index, _)| !dropped.contains(index))
1772                        .map(|(_, fragment)| {
1773                            fragment
1774                                .loops
1775                                .iter()
1776                                .map(|fragment_loop| fragment_loop.coedges.len())
1777                                .collect()
1778                        })
1779                        .collect(),
1780                );
1781                if seen.contains(&signature) {
1782                    continue;
1783                }
1784                seen.push(signature);
1785                if dropped.len() == working.len() {
1786                    refusals.push(format!("{label}: the re-trim consumed every face"));
1787                    continue;
1788                }
1789                // The census's OTHER direction — see [`over_used_edge`].
1790                if let Some((key, count)) = over_used_edge(working, &dropped, self.degenerate) {
1791                    refusals.push(format!(
1792                        "{label}: {} is held {count} times by the pieces this drop keeps, and a \
1793                         closed body holds every edge exactly twice",
1794                        match key {
1795                            EdgeKey::Boundary(_, edge) => format!("edge {edge}"),
1796                            EdgeKey::Imprint(piece) => format!("imprint piece {piece}"),
1797                        }
1798                    ));
1799                    if trace_on() {
1800                        println!(
1801                            "    candidate refused   {label}: {key:?} held {count} times"
1802                        );
1803                    }
1804                    continue;
1805                }
1806                let selected: Vec<_> = working
1807                    .iter()
1808                    .enumerate()
1809                    .filter(|(index, _)| !dropped.contains(index))
1810                    .map(|(_, fragment)| fragment.clone())
1811                    .collect();
1812                match rebuild(self.split, selected, self.imprint, self.scale) {
1813                    // `BREP_HEAL_TRACE=1` prints every candidate the
1814                    // enumeration considered, whether it verified, and what
1815                    // volume it produced. This is the instrument that found the
1816                    // 2026-09-22 stale-fragment defect: the `touched` bound
1817                    // kills most candidates before `verified.len() == 1` picks
1818                    // a survivor, and without the list there is no way to see
1819                    // that the survivor was not the right one.
1820                    Ok(solid) => {
1821                        if trace_on() {
1822                            println!(
1823                                "    CANDIDATE VERIFIED  {label}  volume {:.6}  dropped {:?}",
1824                                crate::solid_mass_properties(&solid)
1825                                    .map(|mass| mass.volume)
1826                                    .unwrap_or(f64::NAN),
1827                                dropped
1828                            );
1829                        }
1830                        verified.push(Candidate {
1831                            dropped,
1832                            retired,
1833                            retrimmed,
1834                            solid,
1835                        })
1836                    }
1837                    Err(reason) => {
1838                        if trace_on() {
1839                            println!("    candidate refused   {label}: {reason}");
1840                        }
1841                        refusals.push(format!("{label}: {reason}"))
1842                    }
1843                }
1844            }
1845        }
1846        (verified, refusals)
1847    }
1848}
1849
1850/// The faces this repair may re-trim: the two that cross, and every face whose
1851/// own loops use an edge the imprint split.
1852fn touched_faces(
1853    solid: &BrepSolid,
1854    imprint: &crate::imprint::ImprintResultRecord,
1855    face_a: u64,
1856    face_b: u64,
1857) -> rustc_hash::FxHashSet<u64> {
1858    use rustc_hash::FxHashSet as HashSet;
1859    let split_edges: HashSet<u64> = imprint
1860        .edge_splits
1861        .iter()
1862        .filter(|split| split.operand == SELF_OPERAND)
1863        .map(|split| split.edge_id)
1864        .collect();
1865    let mut touched: HashSet<u64> = [face_a, face_b].into_iter().collect();
1866    for face in solid.shells.iter().flat_map(|shell| &shell.faces) {
1867        let uses_split = face
1868            .loops
1869            .iter()
1870            .flat_map(|loop_record| &loop_record.coedges)
1871            .any(|coedge| split_edges.contains(&coedge.edge_id));
1872        if uses_split {
1873            touched.insert(face.id);
1874        }
1875    }
1876    touched
1877}
1878
1879/// Close a drop set under the rule a closed body cannot break: every edge is
1880/// used exactly twice. Dropping a tail takes its uses with it, so any face left
1881/// holding a boundary no other face answers bordered the dropped material and
1882/// has to be re-trimmed — which, since the imprint already cut it, means
1883/// dropping the part of it that the crossing cut off. Iterated to a fixed
1884/// point, and bounded by the touched set: reaching a face the imprint never cut
1885/// means the drop has left the material the crossing encloses, and the
1886/// candidate is refused rather than grown.
1887///
1888/// The one thing that bound does not own is a fragment joined to NOTHING the
1889/// drop keeps — see [`fragment_is_detached`]. That is not material the drop ran
1890/// past; it is what the old configuration left, and it is retired wherever it
1891/// came from.
1892fn close_drop_set(
1893    fragments: &[crate::fragment::FaceFragmentRecord],
1894    dropped: &mut Vec<usize>,
1895    retired: &mut Vec<u64>,
1896    touched: &rustc_hash::FxHashSet<u64>,
1897    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
1898) -> Result<(), String> {
1899    use rustc_hash::FxHashMap as HashMap;
1900    loop {
1901        let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
1902        for (index, fragment) in fragments.iter().enumerate() {
1903            if dropped.contains(&index) {
1904                continue;
1905            }
1906            for key in fragment
1907                .loops
1908                .iter()
1909                .flat_map(|fragment_loop| &fragment_loop.coedges)
1910                .filter_map(|coedge| edge_key(&coedge.source))
1911            {
1912                *uses.entry(key).or_default() += 1;
1913            }
1914        }
1915        let mut grew = false;
1916        for (index, fragment) in fragments.iter().enumerate() {
1917            if dropped.contains(&index) {
1918                continue;
1919            }
1920            // RETIRED, not re-trimmed: a fragment holding no edge that any
1921            // other kept fragment holds is attached to nothing this drop keeps.
1922            // The [`touched`] bound does not apply to it, because that bound
1923            // asks whether the drop has run past the material the crossing
1924            // encloses — and a piece joined to the body by nothing is not
1925            // material this drop reached, it is material the OLD configuration
1926            // left behind. Retiring it cannot cascade: the only uses it removes
1927            // are uses nothing else holds. See [`fragment_is_detached`].
1928            if fragment_is_detached(fragment, &uses, degenerate) {
1929                dropped.push(index);
1930                retired.push(fragment.source_face_id);
1931                grew = true;
1932                continue;
1933            }
1934            let unmatched = fragment
1935                .loops
1936                .iter()
1937                .flat_map(|fragment_loop| &fragment_loop.coedges)
1938                .filter_map(|coedge| edge_key(&coedge.source))
1939                .filter(|key| !degenerate.contains(key))
1940                .any(|key| uses.get(&key) == Some(&1));
1941            if !unmatched {
1942                continue;
1943            }
1944            if !touched.contains(&fragment.source_face_id) {
1945                return Err(format!(
1946                    "the re-trim reached face {}, which the imprint never cut — the drop has run \
1947                     past the material the crossing encloses",
1948                    fragment.source_face_id
1949                ));
1950            }
1951            dropped.push(index);
1952            grew = true;
1953        }
1954        if !grew {
1955            return Ok(());
1956        }
1957    }
1958}
1959
1960/// [`close_drop_set`]'s other closure: a face left holding a boundary no other
1961/// face answers for is RE-TRIMMED rather than dropped.
1962///
1963/// Dropping it is the right answer when the unanswered boundary is the whole
1964/// of what that face contributed to the dropped material — a tail beside a
1965/// tail. It is the WRONG answer when the face is one the body keeps and only
1966/// part of its boundary went: a bore carried out through a side wall leaves
1967/// the mouth flat holding the stretch of its own corner edge that now runs
1968/// inside the bore, and the arc of the mouth ring that now runs outside the
1969/// body, and neither is material to drop — they are two coedges to take OFF a
1970/// face that otherwise stays exactly as it is. Taking them off merges that
1971/// face's mouth loop into its outer one, which is precisely the topology a
1972/// breakout has and a drop-only closure cannot reach.
1973///
1974/// The bound is [`close_drop_set`]'s: a face the imprint never cut still ends
1975/// the candidate rather than widening the re-trim, with the two exceptions that
1976/// are not re-trims at all — a fragment attached to nothing
1977/// ([`fragment_is_detached`]) and a whole loop that has left its face
1978/// ([`holds_only_stale_loops`]), both of which are retired. What comes back
1979/// is the fragment list to rebuild from — the untouched fragments as they were,
1980/// the re-trimmed ones with their loops re-linked — and the ids of the faces
1981/// that were re-trimmed, for the ledger. A fragment left with no coedge at all
1982/// joins `dropped`.
1983fn retrim_drop_set(
1984    fragments: &[crate::fragment::FaceFragmentRecord],
1985    dropped: &mut Vec<usize>,
1986    retired: &mut Vec<u64>,
1987    touched: &rustc_hash::FxHashSet<u64>,
1988    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
1989) -> Result<(Vec<crate::fragment::FaceFragmentRecord>, Vec<u64>), String> {
1990    use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
1991
1992    let mut working = fragments.to_vec();
1993    let mut retrimmed: Vec<u64> = Vec::new();
1994    loop {
1995        let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
1996        for (index, fragment) in working.iter().enumerate() {
1997            if dropped.contains(&index) {
1998                continue;
1999            }
2000            for key in fragment
2001                .loops
2002                .iter()
2003                .flat_map(|fragment_loop| &fragment_loop.coedges)
2004                .filter_map(|coedge| edge_key(&coedge.source))
2005            {
2006                *uses.entry(key).or_default() += 1;
2007            }
2008        }
2009        // RETIRED FIRST, and before the early return below, because a
2010        // detached fragment does not have to hold an UNMATCHED edge to be
2011        // detached — one whose loop walks its own seam twice holds that seam
2012        // twice itself, and the census reads two. See [`fragment_is_detached`].
2013        let mut grew = false;
2014        for index in 0..working.len() {
2015            if dropped.contains(&index) {
2016                continue;
2017            }
2018            if fragment_is_detached(&working[index], &uses, degenerate) {
2019                dropped.push(index);
2020                retired.push(working[index].source_face_id);
2021                grew = true;
2022            }
2023        }
2024        if grew {
2025            // The census is stale the moment anything is retired.
2026            continue;
2027        }
2028        // A POLE edge is legitimately used once, as in [`close_drop_set`].
2029        let unmatched: HashSet<EdgeKey> = uses
2030            .iter()
2031            .filter(|(key, count)| **count == 1 && !degenerate.contains(key))
2032            .map(|(key, _)| *key)
2033            .collect();
2034        if unmatched.is_empty() {
2035            return Ok((working, retrimmed));
2036        }
2037        for index in 0..working.len() {
2038            if dropped.contains(&index) {
2039                continue;
2040            }
2041            let holds = working[index]
2042                .loops
2043                .iter()
2044                .flat_map(|fragment_loop| &fragment_loop.coedges)
2045                .filter_map(|coedge| edge_key(&coedge.source))
2046                .any(|key| unmatched.contains(&key));
2047            if !holds {
2048                continue;
2049            }
2050            // A STALE LOOP is the other way the old configuration leaves
2051            // something behind, and the [`touched`] bound does not own it —
2052            // see [`holds_only_stale_loops`].
2053            let stale = holds_only_stale_loops(&working[index], &unmatched, degenerate);
2054            if !touched.contains(&working[index].source_face_id) && !stale {
2055                return Err(format!(
2056                    "the re-trim reached face {}, which the imprint never cut — the drop has run \
2057                     past the material the crossing encloses",
2058                    working[index].source_face_id
2059                ));
2060            }
2061            let face = working[index].source_face_id;
2062            match retrim_fragment(&working[index], &unmatched)? {
2063                Some(next) => {
2064                    working[index] = next;
2065                    if stale {
2066                        retired.push(face);
2067                    } else if !retrimmed.contains(&face) {
2068                        retrimmed.push(face);
2069                    }
2070                }
2071                None => dropped.push(index),
2072            }
2073            grew = true;
2074        }
2075        if !grew {
2076            return Ok((working, retrimmed));
2077        }
2078    }
2079}
2080
2081/// Re-trim ONE fragment: take off every coedge in `remove` and re-link what is
2082/// left into closed loops.
2083///
2084/// The link is made in the face's OWN parameter domain, which is where a trim
2085/// loop is closed and the only frame that tells the two sides of a seam apart —
2086/// a cylinder's seam is one curve in 3D and two coedges a period apart here.
2087/// Nothing is fitted or re-parameterised: each surviving coedge keeps its
2088/// source, its sense and its pcurve, and only the ORDER and the grouping into
2089/// loops are rebuilt. A chain that does not close is a re-trim this repair
2090/// cannot prove, and it refuses rather than emitting an open face.
2091///
2092/// The loops come back in the order of their first surviving coedge, so a face
2093/// whose outer loop has just absorbed an inner one is still outer-loop-first.
2094/// `Ok(None)` when nothing at all is left to keep.
2095fn retrim_fragment(
2096    fragment: &crate::fragment::FaceFragmentRecord,
2097    remove: &rustc_hash::FxHashSet<EdgeKey>,
2098) -> Result<Option<crate::fragment::FaceFragmentRecord>, String> {
2099    use crate::fragment::{FaceFragmentRecord, FragmentCoedge, FragmentLoop};
2100
2101    let kept: Vec<FragmentCoedge> = fragment
2102        .loops
2103        .iter()
2104        .flat_map(|fragment_loop| &fragment_loop.coedges)
2105        .filter(|coedge| !edge_key(&coedge.source).is_some_and(|key| remove.contains(&key)))
2106        .cloned()
2107        .collect();
2108    if kept.is_empty() {
2109        return Ok(None);
2110    }
2111    let ends = kept
2112        .iter()
2113        .map(|coedge| {
2114            let [t0, t1] = coedge.pcurve.domain()?;
2115            Ok((coedge.pcurve.evaluate(t0)?, coedge.pcurve.evaluate(t1)?))
2116        })
2117        .collect::<Result<Vec<_>, String>>()?;
2118    // The domain's own diagonal sets the band a junction is matched in: a
2119    // plane's parameters are millimetres and a cylinder's are radians against
2120    // millimetres, so one fixed number would mean two different things on the
2121    // two carriers. The ends being matched were cut from ONE curve by the
2122    // imprint's own split, so they agree to the fit's precision and the band
2123    // only has to be small against the face.
2124    let [u0, u1] = fragment.surface.domain_u()?;
2125    let [v0, v1] = fragment.surface.domain_v()?;
2126    let tolerance = 1e-4 * (u1 - u0).hypot(v1 - v0);
2127    let mut used = vec![false; kept.len()];
2128    let mut loops: Vec<FragmentLoop> = Vec::new();
2129    for seed in 0..kept.len() {
2130        if used[seed] {
2131            continue;
2132        }
2133        used[seed] = true;
2134        let mut chain = vec![seed];
2135        let start = ends[seed].0;
2136        let mut cursor = ends[seed].1;
2137        while cursor.sub(start).length() > tolerance {
2138            let next = (0..kept.len())
2139                .filter(|index| !used[*index])
2140                .map(|index| (index, ends[index].0.sub(cursor).length()))
2141                .filter(|(_, gap)| *gap <= tolerance)
2142                .min_by(|left, right| left.1.total_cmp(&right.1))
2143                .map(|(index, _)| index);
2144            let Some(next) = next else {
2145                return Err(format!(
2146                    "face {}'s re-trim left a chain open at ({:.6}, {:.6}) in its own parameter \
2147                     domain — what the drop took off it does not close into a loop",
2148                    fragment.source_face_id, cursor.x, cursor.y
2149                ));
2150            };
2151            used[next] = true;
2152            chain.push(next);
2153            cursor = ends[next].1;
2154        }
2155        loops.push(FragmentLoop {
2156            coedges: chain.into_iter().map(|index| kept[index].clone()).collect(),
2157        });
2158    }
2159    Ok(Some(FaceFragmentRecord {
2160        loops,
2161        ..fragment.clone()
2162    }))
2163}
2164
2165/// Is this fragment attached to NOTHING else the drop keeps?
2166///
2167/// The census `uses` counts every non-dropped fragment's edge keys, this one's
2168/// included. So a fragment is detached exactly when, for every non-degenerate
2169/// key it holds, the census count equals ITS OWN count of that key: nobody else
2170/// is holding any edge of it.
2171///
2172/// It is stated that way rather than as "every key it holds is used once"
2173/// because a fragment may legitimately hold one key TWICE on its own — a
2174/// cylinder band walks its seam up one side and down the other — and such a
2175/// fragment is still detached when the seam's only two uses are both its.
2176///
2177/// **What a detached fragment IS.** It is a piece of the old configuration that
2178/// the new one has no room for: on the turned-bore migration, the top face's
2179/// hole loop is the exit ring the bore no longer opens through, sitting out at
2180/// x = 44.6 where the top face's own square ends at 20. The fragmenter returns
2181/// it as its own island because the ring is disjoint from the square, and the
2182/// only other face holding that ring is the wall's overshoot tail. Drop the
2183/// tail and the island is joined to nothing.
2184///
2185/// **Why retiring it is bounded.** Keeping it can never produce a closed body:
2186/// every edge it holds is answered for only by itself, so the assembly is open
2187/// there however the rest of the drop goes. And retiring it removes only uses
2188/// nothing else holds, so it cannot leave a third face unmatched — it does not
2189/// cascade, and no candidate the enumeration would otherwise verify is lost.
2190fn fragment_is_detached(
2191    fragment: &crate::fragment::FaceFragmentRecord,
2192    uses: &rustc_hash::FxHashMap<EdgeKey, usize>,
2193    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
2194) -> bool {
2195    use rustc_hash::FxHashMap as HashMap;
2196    let mut own: HashMap<EdgeKey, usize> = HashMap::default();
2197    for key in fragment
2198        .loops
2199        .iter()
2200        .flat_map(|fragment_loop| &fragment_loop.coedges)
2201        .filter_map(|coedge| edge_key(&coedge.source))
2202        .filter(|key| !degenerate.contains(key))
2203    {
2204        *own.entry(key).or_default() += 1;
2205    }
2206    // A fragment carrying no identified edge at all is NOT called detached:
2207    // its boundary is all DERIVED, which `edge_key` deliberately does not
2208    // count, and the rebuild's own closure is what answers for those.
2209    !own.is_empty()
2210        && own
2211            .iter()
2212            .all(|(key, count)| uses.get(key).copied().unwrap_or(0) == *count)
2213}
2214
2215/// The OTHER way the old configuration leaves something behind: a WHOLE LOOP
2216/// of a face that nothing else answers for AND THAT HAS LEFT THAT FACE.
2217///
2218/// [`fragment_is_detached`] catches the leftover when the fragmenter returns it
2219/// as its own island — which it does when the stale ring is far enough from the
2220/// face's own trim to be plainly disjoint. When it is nearer, the same ring
2221/// comes back as a HOLE LOOP of the face it left: the turned bore's exit ring
2222/// at 40 degrees sits at `x = 22.9 - 30.7` against a top face that ends at 20,
2223/// and the top face is ONE fragment carrying it. There is no island to retire,
2224/// and taking the loop off is a RE-TRIM of a face the imprint never cut.
2225///
2226/// That is the move the earlier records named as missing — *"the departed face
2227/// keeps a hole loop the imprint never cut, and the drop enumeration has no
2228/// move for retiring one"*. This is the move, and it is bounded FOUR ways:
2229///
2230/// * **Unanswered for.** Every non-degenerate edge of the loop is used once,
2231///   by this fragment alone, so no other face the drop keeps is standing on it
2232///   and removing it takes nothing from anyone. A loop only PART of which is
2233///   unanswered for is a genuine re-trim of a neighbour that bordered the
2234///   dropped material, and the [`touched`] bound still owns that case.
2235/// * **It has LEFT the face**, by the containment floor's own test: every
2236///   sample of it lies outside the face's outer loop in that face's own
2237///   parameter plane. This is the condition that matters, and it was added
2238///   after measuring what happens without it. Unanswered-for alone is
2239///   SYMMETRIC — at 40 degrees it licensed retiring the bore's ENTRY mouth
2240///   just as readily as its departed exit ring, so the enumeration verified
2241///   two bodies and refused the pair as ambiguous. A loop still inside its own
2242///   face is trimming material and is not a leftover, whoever is holding it.
2243/// * **A loop survives.** This never empties a face; a fragment with nothing
2244///   left at all is the detached case above.
2245/// * **A PLANE only**, as the containment floor restricts itself, because on a
2246///   periodic surface "outside the outer loop" in the parameter plane is a
2247///   statement about the seam rather than about the trim.
2248///
2249/// And the body still has to rebuild, `validate()`, connect, scan clean and
2250/// pass the containment floor afterwards, exactly as before.
2251fn holds_only_stale_loops(
2252    fragment: &crate::fragment::FaceFragmentRecord,
2253    unmatched: &rustc_hash::FxHashSet<EdgeKey>,
2254    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
2255) -> bool {
2256    if !matches!(
2257        fragment.surface.analytic(),
2258        Some(crate::AnalyticSurface::Plane { .. })
2259    ) {
2260        return false;
2261    }
2262    let mut stale: Vec<usize> = Vec::new();
2263    let mut surviving = 0usize;
2264    for (index, fragment_loop) in fragment.loops.iter().enumerate() {
2265        let keys: Vec<EdgeKey> = fragment_loop
2266            .coedges
2267            .iter()
2268            .filter_map(|coedge| edge_key(&coedge.source))
2269            .filter(|key| !degenerate.contains(key))
2270            .collect();
2271        if !keys.is_empty() && keys.iter().all(|key| unmatched.contains(key)) {
2272            stale.push(index);
2273            continue;
2274        }
2275        if keys.iter().any(|key| unmatched.contains(key)) {
2276            return false;
2277        }
2278        surviving += 1;
2279    }
2280    if stale.is_empty() || surviving == 0 {
2281        return false;
2282    }
2283    let Ok(polylines) = fragment
2284        .loops
2285        .iter()
2286        .map(fragment_loop_polyline)
2287        .collect::<Result<Vec<_>, String>>()
2288    else {
2289        return false;
2290    };
2291    if polylines.iter().any(Vec::is_empty) {
2292        return false;
2293    }
2294    let outer_index = (0..polylines.len())
2295        .max_by(|&a, &b| {
2296            polyline_area(&polylines[a])
2297                .abs()
2298                .total_cmp(&polyline_area(&polylines[b]).abs())
2299        })
2300        .unwrap_or(0);
2301    // The face's own outer loop being the stale one is not a leftover to
2302    // retire; it is the whole face going, which is the detached case.
2303    if stale.contains(&outer_index) {
2304        return false;
2305    }
2306    let outer = &polylines[outer_index];
2307    let bands = BoundaryBands::build(outer);
2308    let tolerance = boundary_tolerance(outer);
2309    stale.iter().all(|index| {
2310        polylines[*index]
2311            .iter()
2312            .all(|&point| !bands.contains(point) && !bands.within(point, tolerance))
2313    })
2314}
2315
2316/// The other half of the census rule, and the one that had no enforcement.
2317///
2318/// A closed body uses every non-degenerate edge EXACTLY TWICE. The drop
2319/// closures above are the "fewer than twice" half — a face left holding a
2320/// boundary nobody answers is dropped or re-trimmed. Nothing checked the other
2321/// direction, and the direction matters here: an imprint piece is cut into BOTH
2322/// crossing faces, so before any drop it is held four times, once by each of
2323/// the four pieces. A drop that takes one piece away leaves it held three
2324/// times — three faces along one curve, which is not a body.
2325///
2326/// That is the candidate the turned-bore migration used to return: dropping the
2327/// exit mouth's disc alone leaves the wall's kept part, the wall's overshoot
2328/// tail and the mouth flat's remainder all on the mouth ring. Refusing it is
2329/// what leaves the one drop that is right.
2330///
2331/// **And nothing downstream catches it**, which is why the check belongs here
2332/// and not there. `assemble_open_fragments` resolves the third use by putting
2333/// the overshoot on a shell of its OWN: the body comes back with every edge
2334/// used exactly twice, `validate()` clean, `solid_connectivity` calling it
2335/// connected (the sheets share edges with the main shell), the scan clean and
2336/// the containment floor acquitting it — a three-shell solid whose first shell
2337/// is the right answer and whose other two are sheets stapled to it. Measured
2338/// on the turned bore at 60 degrees: shell 1 is the 7 correct faces at
2339/// 7673.516114379 against a closed form of 7673.516114438, shell 2 is the
2340/// overshoot alone at -536.326313 and shell 3 the departed ring at -6.7e-14.
2341///
2342/// Returns the offending key and its count, smallest key first so the refusal
2343/// reads the same on every run.
2344fn over_used_edge(
2345    fragments: &[crate::fragment::FaceFragmentRecord],
2346    dropped: &[usize],
2347    degenerate: &rustc_hash::FxHashSet<EdgeKey>,
2348) -> Option<(EdgeKey, usize)> {
2349    use rustc_hash::FxHashMap as HashMap;
2350    let mut uses: HashMap<EdgeKey, usize> = HashMap::default();
2351    for (index, fragment) in fragments.iter().enumerate() {
2352        if dropped.contains(&index) {
2353            continue;
2354        }
2355        for key in fragment
2356            .loops
2357            .iter()
2358            .flat_map(|fragment_loop| &fragment_loop.coedges)
2359            .filter_map(|coedge| edge_key(&coedge.source))
2360            .filter(|key| !degenerate.contains(key))
2361        {
2362            *uses.entry(key).or_default() += 1;
2363        }
2364    }
2365    let mut over: Vec<(EdgeKey, usize)> = uses
2366        .into_iter()
2367        .filter(|(_, count)| *count > 2)
2368        .collect();
2369    over.sort_unstable();
2370    over.into_iter().next()
2371}
2372
2373/// The identity of a fragment coedge's edge, for the use census. A DERIVED
2374/// coedge carries a freshly minted curve rather than an identity, so it is not
2375/// counted — the rebuild's own closure check is what answers for those.
2376fn edge_key(source: &crate::fragment::FragmentEdgeSource) -> Option<EdgeKey> {
2377    use crate::fragment::FragmentEdgeSource;
2378    match source {
2379        FragmentEdgeSource::Boundary { operand, edge_id }
2380        | FragmentEdgeSource::SharedBoundary { operand, edge_id } => {
2381            Some(EdgeKey::Boundary(*operand, *edge_id))
2382        }
2383        FragmentEdgeSource::Imprint { piece_id } => Some(EdgeKey::Imprint(*piece_id)),
2384        FragmentEdgeSource::Derived { .. } => None,
2385    }
2386}
2387
2388#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
2389enum EdgeKey {
2390    Boundary(u8, u64),
2391    Imprint(u64),
2392}
2393
2394/// Every subset of `pieces` that leaves at least one behind, the empty one
2395/// included — the ways one face's pieces can be dropped.
2396fn proper_subsets(pieces: &[usize]) -> Vec<Vec<usize>> {
2397    let mut subsets = Vec::new();
2398    for mask in 0..(1u32 << pieces.len()) {
2399        if mask == (1u32 << pieces.len()) - 1 {
2400            // Dropping every piece of a crossing face is not a re-trim of it.
2401            continue;
2402        }
2403        subsets.push(
2404            pieces
2405                .iter()
2406                .enumerate()
2407                .filter(|(bit, _)| mask & (1 << bit) != 0)
2408                .map(|(_, piece)| *piece)
2409                .collect(),
2410        );
2411    }
2412    subsets
2413}
2414
2415/// Reassemble one choice of kept fragments and prove it sound, or say why not.
2416fn rebuild(
2417    split: &BrepSolid,
2418    selected: Vec<crate::fragment::FaceFragmentRecord>,
2419    imprint: &crate::imprint::ImprintResultRecord,
2420    scale: f64,
2421) -> Result<BrepSolid, String> {
2422    use crate::boolean::{assemble_open_fragments, finalize_assembled_solid};
2423    use rustc_hash::FxHashMap as HashMap;
2424
2425    let mut sources: HashMap<u8, &BrepSolid> = HashMap::default();
2426    sources.insert(SELF_OPERAND, split);
2427    let assembly_tolerance = 2e-3f64.max(scale * 5e-5);
2428    let solid = assemble_open_fragments(selected, &sources, imprint, assembly_tolerance)
2429        .map_err(|error| format!("the kept pieces did not assemble: {error}"))?;
2430    let solid = finalize_assembled_solid(solid, assembly_tolerance)
2431        .map_err(|error| format!("the assembly did not finalize: {error}"))?;
2432
2433    let issues = solid.validate();
2434    if !issues.is_empty() {
2435        return Err(format!(
2436            "the rebuilt body fails validate(): {}",
2437            issues[0].message
2438        ));
2439    }
2440    if crate::solid_connectivity(&solid).is_disconnected() {
2441        return Err("the rebuilt body is not connected".into());
2442    }
2443    let options = SelfIntersectionOptions::for_solid(&solid);
2444    let report = solid_self_intersections(&solid, options)
2445        .map_err(|message| format!("the rebuilt body could not be scanned: {message}"))?;
2446    if report.is_flagged() {
2447        return Err(format!(
2448            "the rebuilt body still self-intersects: {}",
2449            report.summary().unwrap_or_else(|| "no summary".into())
2450        ));
2451    }
2452    Ok(solid)
2453}
2454
2455/// One face of a body as its own open body, carrying the edges and vertices
2456/// that face uses and nothing else.
2457fn one_face_body(solid: &BrepSolid, face_id: u64) -> Result<BrepSolid, String> {
2458    use crate::topology::ShellRecord;
2459    use rustc_hash::FxHashSet as HashSet;
2460
2461    let face = solid
2462        .shells
2463        .iter()
2464        .flat_map(|shell| &shell.faces)
2465        .find(|face| face.id == face_id)
2466        .ok_or_else(|| format!("face {face_id} is not in this body"))?
2467        .clone();
2468    let used: HashSet<u64> = face
2469        .loops
2470        .iter()
2471        .flat_map(|loop_record| &loop_record.coedges)
2472        .map(|coedge| coedge.edge_id)
2473        .collect();
2474    let edges: Vec<_> = solid
2475        .edges
2476        .iter()
2477        .filter(|edge| used.contains(&edge.id))
2478        .cloned()
2479        .collect();
2480    let vertices: HashSet<u64> = edges
2481        .iter()
2482        .flat_map(|edge| [edge.start_vertex_id, edge.end_vertex_id])
2483        .collect();
2484    Ok(BrepSolid {
2485        id: solid.id,
2486        genus: 0,
2487        vertices: solid
2488            .vertices
2489            .iter()
2490            .filter(|vertex| vertices.contains(&vertex.id))
2491            .cloned()
2492            .collect(),
2493        edges,
2494        shells: vec![ShellRecord {
2495            id: 1,
2496            faces: vec![face],
2497        }],
2498    })
2499}
2500
2501// ---------------------------------------------------------------------------
2502// The ledger: a repaired result is reported as repaired
2503// ---------------------------------------------------------------------------
2504
2505/// One self-crossing this module REPAIRED rather than refused.
2506///
2507/// A repair changes the answer a lane returns, so it is never silent: the
2508/// feature that ran it carries this in its own result
2509/// (`FeatureResult::notes`), and the case gate records it per case. The
2510/// ledger is the carrier between the two — [`accept_sound`] is called from
2511/// lanes with no diagnostics record to write into, and threading one through
2512/// every lane exit would be a wider change than the repair itself. Same shape
2513/// as the boolean's `CONFORMANCE_REPAIRS` counter and the pcurve fit ledger.
2514#[derive(Clone, Debug)]
2515pub struct CrossingRepairRecord {
2516    /// The two faces that crossed.
2517    pub faces: (u64, u64),
2518    /// The crossing point the scan confirmed.
2519    pub point: crate::Vec3,
2520    /// How far past its band the crossing straddled.
2521    pub straddle: f64,
2522    /// Source faces whose pieces were dropped, with how many pieces of each.
2523    pub dropped: Vec<(u64, usize)>,
2524    /// Source faces the repair RETIRED a leftover from, with how many: a
2525    /// piece the drop left joined to nothing, or a whole loop of a face that
2526    /// nothing else answers for. Either way it is what the OLD configuration
2527    /// left on the body, and the ledger is the only place that says a repair
2528    /// took one off — "a piece was dropped" and "a leftover was retired" are
2529    /// different events.
2530    pub retired: Vec<(u64, usize)>,
2531    /// The faces the drop RE-TRIMMED: neighbours of the dropped material,
2532    /// which are neither of the crossing pair.
2533    pub retrimmed: Vec<u64>,
2534    /// Set when the repair was reached from the HOLE-CONTAINMENT FLOOR rather
2535    /// than from the scan: the face whose hole loop had left it, and that
2536    /// loop. `point` and `straddle` are then unmeasured and read zero, because
2537    /// no crossing was confirmed — the floor convicts a containment, not a
2538    /// crossing.
2539    pub breakout: Option<(u64, u64)>,
2540}
2541
2542impl std::fmt::Display for CrossingRepairRecord {
2543    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2544        if let Some((face, loop_id)) = self.breakout {
2545            return write!(
2546                formatter,
2547                "repaired a BREAKOUT: hole loop {loop_id} of face {face} had left that face \
2548                 onto face {}, so face {} — the wall it bounds — and face {} were split along \
2549                 their carriers' intersection; dropped {} and re-trimmed {}{}",
2550                self.faces.1,
2551                self.faces.0,
2552                self.faces.1,
2553                self.pieces_dropped(),
2554                self.faces_retrimmed(),
2555                self.pieces_retired(),
2556            );
2557        }
2558        write!(
2559            formatter,
2560            "repaired a self-crossing: faces {} and {} crossed near ({:.4}, {:.4}, {:.4}) \
2561             (straddle {:.3e}); split along their carriers' intersection, dropped {} \
2562             and re-trimmed {}{}",
2563            self.faces.0,
2564            self.faces.1,
2565            self.point.x,
2566            self.point.y,
2567            self.point.z,
2568            self.straddle,
2569            self.pieces_dropped(),
2570            self.faces_retrimmed(),
2571            self.pieces_retired(),
2572        )
2573    }
2574}
2575
2576impl CrossingRepairRecord {
2577    fn pieces_dropped(&self) -> String {
2578        self.dropped
2579            .iter()
2580            .map(|(face, count)| format!("{count} piece(s) of face {face}"))
2581            .collect::<Vec<_>>()
2582            .join(", ")
2583    }
2584
2585    fn faces_retrimmed(&self) -> String {
2586        if self.retrimmed.is_empty() {
2587            "no neighbouring face".to_string()
2588        } else {
2589            format!(
2590                "face(s) {}",
2591                self.retrimmed
2592                    .iter()
2593                    .map(|face| face.to_string())
2594                    .collect::<Vec<_>>()
2595                    .join(", ")
2596            )
2597        }
2598    }
2599
2600    /// The clause that says the OLD configuration's leftovers were taken off
2601    /// the body. Empty when nothing was retired, so every message this ledger
2602    /// already printed is unchanged; a repair that retires something says so
2603    /// and names the face, which is the only way the case gate's result line
2604    /// tells a repaired migration from the one it used to return.
2605    fn pieces_retired(&self) -> String {
2606        if self.retired.is_empty() {
2607            return String::new();
2608        }
2609        format!(
2610            ", and RETIRED {} the old configuration left, held by nothing else this repair keeps",
2611            self.retired
2612                .iter()
2613                .map(|(face, count)| format!("{count} leftover(s) of face {face}"))
2614                .collect::<Vec<_>>()
2615                .join(", ")
2616        )
2617    }
2618}
2619
2620thread_local! {
2621    static CROSSING_REPAIRS: std::cell::RefCell<Vec<CrossingRepairRecord>> =
2622        const { std::cell::RefCell::new(Vec::new()) };
2623}
2624
2625/// Take every repair made on this thread since the last call, leaving the
2626/// ledger empty. The history loop drains it after each feature so the repair
2627/// is reported against the feature that made it.
2628pub fn take_crossing_repairs() -> Vec<CrossingRepairRecord> {
2629    CROSSING_REPAIRS.with(|repairs| std::mem::take(&mut *repairs.borrow_mut()))
2630}
2631
2632/// Put a ledger back, replacing whatever is there.
2633///
2634/// The migration route TRIES candidate faces, and a trial that builds records
2635/// itself on the way past. Only the candidate whose body is returned may keep
2636/// its record, and a caller's own entries must survive the trials, so the
2637/// ledger is taken before and written back after. Nothing else may use this:
2638/// a repair reports itself once, where it happened.
2639fn restore_repairs(records: Vec<CrossingRepairRecord>) {
2640    CROSSING_REPAIRS.with(|repairs| *repairs.borrow_mut() = records);
2641}
2642
2643fn record_repair(
2644    fragments: &[crate::fragment::FaceFragmentRecord],
2645    dropped: &[usize],
2646    retired: &[u64],
2647    retrimmed_faces: &[u64],
2648    face_a: u64,
2649    face_b: u64,
2650    origin: &RepairOrigin<'_>,
2651) {
2652    let tally = |faces: &[u64]| {
2653        let mut by_face: Vec<(u64, usize)> = Vec::new();
2654        for face in faces {
2655            match by_face.iter_mut().find(|(existing, _)| existing == face) {
2656                Some((_, count)) => *count += 1,
2657                None => by_face.push((*face, 1)),
2658            }
2659        }
2660        by_face.sort_unstable();
2661        by_face
2662    };
2663    let by_face = tally(
2664        &dropped
2665            .iter()
2666            .map(|index| fragments[*index].source_face_id)
2667            .collect::<Vec<_>>(),
2668    );
2669    let retired = tally(retired);
2670    // A face that lost ONLY a detached piece was not re-trimmed — nothing of
2671    // it was re-cut, a leftover of the old configuration was taken off the
2672    // body — so it is reported under `retired` and not counted here twice.
2673    let mut retrimmed: Vec<u64> = by_face
2674        .iter()
2675        .filter(|(face, count)| {
2676            retired
2677                .iter()
2678                .find(|(retired_face, _)| retired_face == face)
2679                .map(|(_, retired_count)| retired_count < count)
2680                .unwrap_or(true)
2681        })
2682        .map(|(face, _)| *face)
2683        .chain(retrimmed_faces.iter().copied())
2684        .filter(|face| *face != face_a && *face != face_b)
2685        .collect();
2686    retrimmed.sort_unstable();
2687    retrimmed.dedup();
2688    let crossing = match origin {
2689        RepairOrigin::Scan(report) => report.confirmed.first(),
2690        RepairOrigin::Breakout { .. } => None,
2691    };
2692    let record = CrossingRepairRecord {
2693        faces: (face_a, face_b),
2694        point: crossing.map(|crossing| crossing.point).unwrap_or_default(),
2695        straddle: crossing.map(|crossing| crossing.straddle).unwrap_or_default(),
2696        dropped: by_face,
2697        retired,
2698        retrimmed,
2699        breakout: match origin {
2700            RepairOrigin::Scan(_) => None,
2701            RepairOrigin::Breakout { face, loop_id } => Some((*face, *loop_id)),
2702        },
2703    };
2704    if trace_on() {
2705        eprintln!("HEAL {record}");
2706    }
2707    CROSSING_REPAIRS.with(|repairs| repairs.borrow_mut().push(record));
2708}
2709
2710/// Write the flagged body out before the repair touches it, when
2711/// `BREP_HEAL_DUMP` names a directory. A crossing is measured off the body that
2712/// carries it — per-face areas, a mesh-arbitrated genus, the volume the repair
2713/// changes — and replaying a whole document to reach it again costs more than
2714/// the file does. Diagnostics only: the lane still repairs or refuses exactly
2715/// as it would have.
2716fn dump_flagged(solid: &BrepSolid, entry: &str) {
2717    let Ok(directory) = std::env::var("BREP_HEAL_DUMP") else {
2718        return;
2719    };
2720    if directory.is_empty() {
2721        return;
2722    }
2723    let name: String = entry
2724        .chars()
2725        .map(|character| if character.is_alphanumeric() { character } else { '_' })
2726        .collect();
2727    let path = std::path::Path::new(&directory).join(format!("flagged-{name}.json"));
2728    match serde_json::to_string(solid) {
2729        Ok(text) => {
2730            if let Err(error) = std::fs::write(&path, text) {
2731                eprintln!("HEAL dump {}: {error}", path.display());
2732            } else {
2733                eprintln!("HEAL dumped the flagged body to {}", path.display());
2734            }
2735        }
2736        Err(error) => eprintln!("HEAL dump: {error}"),
2737    }
2738}
2739
2740// ---------------------------------------------------------------------------
2741// Trace instrument (BREP_HEAL_TRACE=1)
2742// ---------------------------------------------------------------------------
2743
2744fn trace_on() -> bool {
2745    std::env::var("BREP_HEAL_TRACE").is_ok_and(|value| !value.is_empty() && value != "0")
2746}
2747
2748/// Measurement hatch for the section fit the repair's imprint asks for:
2749/// `BREP_HEAL_FIT_LANE=local|global` picks the lane its ladder starts in and
2750/// `BREP_HEAL_FIT_POINTS=<n>` its station budget. Both unset is the request as
2751/// built, so every lane and budget can be read off ONE binary against the
2752/// shipped one (`crossing_repair_probe body` with `BREP_DEBUG_POLYLINE_FIT=1`).
2753fn measured_fit_request(request: &mut crate::imprint::ImprintOptions) {
2754    match std::env::var("BREP_HEAL_FIT_LANE").as_deref() {
2755        Ok("local") => request.local_fit = true,
2756        Ok("global") => request.local_fit = false,
2757        _ => {}
2758    }
2759    if let Some(points) = std::env::var("BREP_HEAL_FIT_POINTS")
2760        .ok()
2761        .and_then(|value| value.parse::<usize>().ok())
2762        .filter(|points| *points >= 2)
2763    {
2764        request.maximum_fit_points = points;
2765    }
2766    if trace_on() {
2767        eprintln!(
2768            "HEAL imprint request: {} lane, {} stations, tolerance {:.3e}",
2769            if request.local_fit { "local" } else { "global" },
2770            request.maximum_fit_points,
2771            request.tolerance
2772        );
2773    }
2774}
2775
2776fn trace_solid(label: &str, solid: &BrepSolid) {
2777    use rustc_hash::FxHashMap as HashMap;
2778    let point: HashMap<u64, crate::Vec3> = solid
2779        .vertices
2780        .iter()
2781        .map(|vertex| (vertex.id, vertex.point))
2782        .collect();
2783    eprintln!(
2784        "HEAL {label}: {}F {}E {}V shells={}",
2785        solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>(),
2786        solid.edges.len(),
2787        solid.vertices.len(),
2788        solid.shells.len()
2789    );
2790    let mut uses: HashMap<u64, usize> = HashMap::default();
2791    for shell in &solid.shells {
2792        for face in &shell.faces {
2793            for loop_record in &face.loops {
2794                for coedge in &loop_record.coedges {
2795                    *uses.entry(coedge.edge_id).or_default() += 1;
2796                }
2797            }
2798        }
2799    }
2800    for edge in &solid.edges {
2801        let start = point.get(&edge.start_vertex_id).copied().unwrap_or_default();
2802        let end = point.get(&edge.end_vertex_id).copied().unwrap_or_default();
2803        eprintln!(
2804            "HEAL   edge {:>3} uses={} v{}->v{} ({:.4},{:.4},{:.4})->({:.4},{:.4},{:.4}) deg={} '{}'",
2805            edge.id,
2806            uses.get(&edge.id).copied().unwrap_or(0),
2807            edge.start_vertex_id,
2808            edge.end_vertex_id,
2809            start.x, start.y, start.z, end.x, end.y, end.z,
2810            edge.degenerate,
2811            edge.name.as_deref().unwrap_or("")
2812        );
2813    }
2814    for shell in &solid.shells {
2815        for face in &shell.faces {
2816            eprintln!(
2817                "HEAL   face {:>3} loops={} '{}'",
2818                face.id,
2819                face.loops.len(),
2820                face.name.as_deref().unwrap_or("")
2821            );
2822            for (index, loop_record) in face.loops.iter().enumerate() {
2823                let text = loop_record
2824                    .coedges
2825                    .iter()
2826                    .map(|coedge| format!("{}{}", if coedge.forward { '+' } else { '-' }, coedge.edge_id))
2827                    .collect::<Vec<_>>()
2828                    .join(" ");
2829                eprintln!("HEAL     loop {index}: {text}");
2830            }
2831        }
2832    }
2833}
2834
2835fn trace_imprint(imprint: &crate::imprint::ImprintResultRecord) {
2836    eprintln!(
2837        "HEAL imprint: {} piece(s) {} edge_split(s) by_face={:?}",
2838        imprint.pieces.len(),
2839        imprint.edge_splits.len(),
2840        imprint
2841            .by_face
2842            .iter()
2843            .map(|face| (face.operand, face.face_id, face.piece_ids.clone()))
2844            .collect::<Vec<_>>()
2845    );
2846    for piece in &imprint.pieces {
2847        let start = piece.curve.evaluate(piece.t0).unwrap_or_default();
2848        let end = piece.curve.evaluate(piece.t1).unwrap_or_default();
2849        let middle = piece.curve.evaluate((piece.t0 + piece.t1) / 2.0).unwrap_or_default();
2850        eprintln!(
2851            "HEAL   piece {} v{}->v{} ({:.4},{:.4},{:.4}) .. ({:.4},{:.4},{:.4}) .. ({:.4},{:.4},{:.4}) supports={:?} shared={:?}",
2852            piece.id,
2853            piece.start_vertex_id,
2854            piece.end_vertex_id,
2855            start.x, start.y, start.z,
2856            middle.x, middle.y, middle.z,
2857            end.x, end.y, end.z,
2858            piece.support_faces.iter().map(|key| (key.operand, key.face_id)).collect::<Vec<_>>(),
2859            piece.shared_edge,
2860        );
2861    }
2862    for split in &imprint.edge_splits {
2863        eprintln!(
2864            "HEAL   edge_split operand={} edge={} at {:?}",
2865            split.operand, split.edge_id, split.parameters
2866        );
2867    }
2868    for vertex in &imprint.vertices {
2869        eprintln!(
2870            "HEAL   imprint vertex {} ({:.4},{:.4},{:.4})",
2871            vertex.id, vertex.point.x, vertex.point.y, vertex.point.z
2872        );
2873    }
2874}
2875
2876fn trace_fragments(fragments: &[crate::fragment::FaceFragmentRecord]) {
2877    use crate::fragment::FragmentEdgeSource;
2878    eprintln!("HEAL fragments: {}", fragments.len());
2879    for (index, fragment) in fragments.iter().enumerate() {
2880        eprintln!(
2881            "HEAL   fragment {index} face {} loops={} test=({:.4},{:.4},{:.4})",
2882            fragment.source_face_id,
2883            fragment.loops.len(),
2884            fragment.test_point.x,
2885            fragment.test_point.y,
2886            fragment.test_point.z
2887        );
2888        for (loop_index, fragment_loop) in fragment.loops.iter().enumerate() {
2889            let text = fragment_loop
2890                .coedges
2891                .iter()
2892                .map(|coedge| {
2893                    let tag = match &coedge.source {
2894                        FragmentEdgeSource::Boundary { operand, edge_id } => format!("b{operand}:{edge_id}"),
2895                        FragmentEdgeSource::SharedBoundary { operand, edge_id } => format!("s{operand}:{edge_id}"),
2896                        FragmentEdgeSource::Imprint { piece_id } => format!("p{piece_id}"),
2897                        FragmentEdgeSource::Derived { start, end, .. } => format!(
2898                            "d({:.3},{:.3},{:.3})->({:.3},{:.3},{:.3})",
2899                            start.x, start.y, start.z, end.x, end.y, end.z
2900                        ),
2901                    };
2902                    format!("{}{tag}", if coedge.forward { '+' } else { '-' })
2903                })
2904                .collect::<Vec<_>>()
2905                .join(" ");
2906            eprintln!("HEAL     loop {loop_index}: {text}");
2907        }
2908    }
2909}
2910
2911