Skip to main content

brep_kernel/csg/imprint/
driver.rs

1use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass, MAX_FIT_STATIONS};
2
3/// The degree a marched section is interpolated at (2026-09-27). The march
4/// puts every station on both carriers; the fit decides how far the curve
5/// strays BETWEEN them, and a cubic's O(h^4) there left `BadBoolean`'s
6/// sphere×bore rim 2.5e-7 off the bore with a mean bias of −1.2e-8 that both
7/// trims inherited (+1.14e-6 on the solid). A quintic through the same
8/// stations reads 8.5e-9 and +6e-11 with the same knots and control-point
9/// count. More stations bought the same accuracy on 2026-09-26 and were
10/// reverted for their cost: the watertight tessellator samples every interior
11/// knot, so every scan downstream scales with station count, and a degree does
12/// not add knots. `BREP_SECTION_FIT_DEGREE=3` restores the cubic. The gate on
13/// the mid-span refinement, as a multiple of the fit tolerance (2026-09-27): a
14/// marched section whose unrefined fit misses a carrier between stations by
15/// more than this is UNDERSAMPLED — the 20° cylinder crossing of
16/// `fillet-skew-cylinder-seam-split-20deg` sits 4e-5..8e-5 off, 470 times the
17/// tolerance, at any degree — and only such a section is given true
18/// intersection points until its mid-spans hold. Refining every section cost
19/// more downstream than it returned (the tessellator samples every knot).
20/// `BREP_SECTION_REFINE_GATE` sets the multiple (`on` for
21/// [`SECTION_REFINE_GATE`]). OFF BY DEFAULT: at k = 100 the sequential
22/// blend/direct_edit/imprint/boolean lib set costs +64 % and
23/// `oblique_multi_rim_cone_cap_tearing_push_refuses` goes red; k = 1000 costs
24/// +32 % and leaves the skew union +5.3e-4 off. This global switch stays off.
25/// Independently, a pair of grazing crossings on one trim edge requests a
26/// fit-floor check for that face pair below: those two nearby junctions and
27/// the intervening cap must survive the fit.
28fn section_refine_gate() -> Option<f64> {
29    static GATE: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
30    *GATE.get_or_init(|| match std::env::var("BREP_SECTION_REFINE_GATE") {
31        Ok(value) if value == "on" => Some(SECTION_REFINE_GATE),
32        Ok(value) => value.parse::<f64>().ok().filter(|gate| *gate >= 0.0),
33        Err(_) => None,
34    })
35}
36
37/// The multiple `BREP_SECTION_REFINE_GATE=on` uses: the smallest measured
38/// that trips the skew union's undersampled sections and leaves fixture 22 as
39/// it was (k = 10 flips it).
40const SECTION_REFINE_GATE: f64 = 100.0;
41
42/// A clipped run's END span can be a sliver: the clip endpoint lands beside a
43/// march station, a spacing ratio to the run's median of 0.011 on
44/// `anotherBooleanFail`, and a global interpolant rings next to such a jump —
45/// the mid-spans near the ends miss their carriers by 1e-3, and bisecting them
46/// only makes the spacing more uneven (2026-09-27). Below
47/// `SLIVER_END_FRACTION` of the median spacing the station beside the endpoint
48/// is RE-SOLVED at the middle of the merged gap as a fresh intersection point
49/// (`BREP_SECTION_SLIVER_END=resolve`) or dropped (`drop`). OFF BY DEFAULT:
50/// resolve evens every `anotherBooleanFail` section but reddens three
51/// fixture-25 pins, one a closure residual 2.32e-3 against its 1.41e-4 bar.
52/// Returns how many ends were evened.
53fn even_sliver_ends(
54    section: &mut Vec<Vec3>,
55    first: &NurbsSurface,
56    second: &NurbsSurface,
57    tolerance: f64,
58) -> Result<usize, String> {
59    static MODE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
60    let mode = MODE.get_or_init(|| std::env::var("BREP_SECTION_SLIVER_END").unwrap_or_default());
61    if !(mode == "drop" || mode == "resolve") {
62        return Ok(0);
63    }
64    let mut evened = 0;
65    // One sliver at a time: each removal changes the spans it sits between.
66    for _ in 0..4 {
67        let count = section.len();
68        if count < 6 {
69            break;
70        }
71        let mut chords: Vec<f64> = section.windows(2).map(|pair| pair[1].sub(pair[0]).length()).collect();
72        let lengths = chords.clone();
73        chords.sort_by(f64::total_cmp);
74        let median = chords[chords.len() / 2];
75        let spans = lengths.len();
76        let near_ends = [0, 1, 2, spans - 3, spans - 2, spans - 1];
77        let Some(&sliver) = near_ends
78            .iter()
79            .filter(|&&span| lengths[span] < sliver_end_fraction() * median)
80            .min_by(|a, b| lengths[**a].total_cmp(&lengths[**b]))
81        else {
82            break;
83        };
84        // The sliver's bounding stations are `sliver` and `sliver + 1`; a run
85        // endpoint is never moved. Of the interior ones, remove the one whose
86        // removal leaves the shorter merged span.
87        let mut candidates = Vec::new();
88        for station in [sliver, sliver + 1] {
89            if station == 0 || station == count - 1 {
90                continue;
91            }
92            let merged = section[station + 1].sub(section[station - 1]).length();
93            candidates.push((station, merged));
94        }
95        let Some(&(station, _)) = candidates.iter().min_by(|a, b| a.1.total_cmp(&b.1)) else {
96            break;
97        };
98        if mode == "resolve" {
99            let (before, after) = (section[station - 1], section[station + 1]);
100            let seed = before.add(after).scale(0.5);
101            let reach = 0.5 * after.sub(before).length();
102            match crate::surface_surface_intersection::refine_to_intersection(first, second, seed, tolerance, reach)? {
103                Some(point) => section[station] = point,
104                None => {
105                    section.remove(station);
106                }
107            }
108        } else {
109            section.remove(station);
110        }
111        evened += 1;
112    }
113    Ok(evened)
114}
115
116/// At 0.25 six of `anotherBooleanFail`'s eight sections stay NoProgress
117/// (worst miss 2.7e-4); at 0.5 all eight are evened and fall below the
118/// refinement gate (worst miss 2.8e-3 -> 5.4e-8).
119const SLIVER_END_FRACTION: f64 = 0.5;
120
121/// `BREP_SECTION_SLIVER_FRACTION` overrides [`SLIVER_END_FRACTION`] for the
122/// measurement that picks it.
123fn sliver_end_fraction() -> f64 {
124    static FRACTION: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
125    *FRACTION.get_or_init(|| {
126        std::env::var("BREP_SECTION_SLIVER_FRACTION")
127            .ok()
128            .and_then(|value| value.parse::<f64>().ok())
129            .filter(|fraction| *fraction > 0.0 && *fraction < 1.0)
130            .unwrap_or(SLIVER_END_FRACTION)
131    })
132}
133
134fn section_fit_degree() -> usize {
135    static DEGREE: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
136    *DEGREE.get_or_init(|| {
137        std::env::var("BREP_SECTION_FIT_DEGREE")
138            .ok()
139            .and_then(|value| value.parse::<usize>().ok())
140            .filter(|degree| (1..=7).contains(degree))
141            .unwrap_or(5)
142    })
143}
144
145/// A marched section is held to the fit tolerance against the CARRIERS, not
146/// only against the polyline it was fitted through (2026-09-26). The march
147/// refines every station onto both surfaces to `tolerance * 0.01`, but the
148/// cubic interpolant between stations is measured by `fit_polyline` against the
149/// polyline's own chords, which are further from the true curve than the
150/// interpolant is — so a systematic bias of the interpolant (2.8e-8 mean,
151/// 4.7e-7 worst, inside the bore on `BadBoolean`'s sphere/bore rim at 135
152/// stations) passed every check the fit could make, and both trims inherited it
153/// (+1.14e-6 on the solid's volume, against two independent readings agreeing
154/// to 1e-8). The check below evaluates the fitted curve at every mid-span,
155/// measures it against both carriers, and where it misses `fit tolerance *
156/// SECTION_MIDSPAN_FRACTION` inserts a TRUE intersection point there and
157/// refits. The interpolant's bias falls as h^4, so one round usually suffices.
158/// A round that neither halves the worst miss nor lowers the count of spans
159/// missing is not in that regime — a carrier's C0 crease puts a corner in the
160/// section (fixture 25's pierce solid), where every round only doubles the
161/// stations — so that round is discarded and the loop stops. It is also capped
162/// in rounds and stations, and names how it left.
163/// `BREP_SECTION_REFINE_GATE=off` switches it off, for the before/after reading
164/// and nothing else.
165pub(super) const SECTION_MIDSPAN_FRACTION: f64 = 0.1;
166const SECTION_MIDSPAN_ROUNDS: usize = 4;
167/// A round earns another if it cuts the worst mid-span miss by this factor
168/// (h^4 promises 16 for a halved spacing) OR lowers the count of spans
169/// missing: near the floor a few isolated spans hold the worst while the count
170/// falls, and on a corner the count GROWS round on round.
171const SECTION_MIDSPAN_PROGRESS: f64 = 0.5;
172
173/// Squared distance from `point` to the segment `from`-`to`.
174fn distance_to_segment_sq(point: Vec3, from: Vec3, to: Vec3) -> f64 {
175    let d = to.sub(from);
176    let l2 = d.dot(d);
177    let t = if l2 > 0.0 { (point.sub(from).dot(d) / l2).clamp(0.0, 1.0) } else { 0.0 };
178    let q = from.add(d.scale(t));
179    point.sub(q).dot(point.sub(q))
180}
181
182/// How a mid-span refinement left (`SECTION_MIDSPAN_*`), for the pair trace.
183#[derive(Clone, Copy, Debug, PartialEq)]
184enum MidspanExit {
185    /// Every mid-span within the floor of both carriers.
186    Converged,
187    /// The unrefined fit's worst mid-span is within the gate: the section is
188    /// sampled well enough that its degree carries it, and stations would
189    /// cost more downstream than they return.
190    BelowGate,
191    /// Some mid-span still misses and no inserted point could improve it
192    /// (the Newton failed, wandered off the span, or landed on a station
193    /// already present).
194    Stalled,
195    /// A round neither halved the worst miss nor lowered the count of spans
196    /// missing: not the h^4 regime, so the section has a corner the
197    /// interpolant cannot follow. That round's fit is discarded.
198    NoProgress,
199    RoundBudget,
200    StationCeiling,
201}
202
203/// Measure `fit` at every mid-span against both carriers; return the worst
204/// distance, the count of spans that miss `floor` and, for each of them the
205/// Newton could place, the refined intersection point to insert (with the
206/// span's index).
207fn midspan_misses(
208    fit: &PolylineFit,
209    first: &NurbsSurface,
210    second: &NurbsSurface,
211    floor: f64,
212    tolerance: f64,
213) -> Result<(f64, usize, Vec<(usize, Vec3)>), String> {
214    let mut worst = 0.0f64;
215    let mut missing = 0usize;
216    let mut inserts = Vec::new();
217    for span in 0..fit.parameters.len().saturating_sub(1) {
218        let mid = 0.5 * (fit.parameters[span] + fit.parameters[span + 1]);
219        let point = fit.curve.evaluate(mid)?;
220        let off = project_point_to_surface(first, point)?
221            .distance
222            .max(project_point_to_surface(second, point)?.distance);
223        worst = worst.max(off);
224        if off <= floor {
225            continue;
226        }
227        missing += 1;
228        // The refined point must stay on THIS span: half its chord is the reach.
229        let reach = 0.5 * fit.kept[span + 1].sub(fit.kept[span]).length();
230        if let Some(refined) = crate::surface_surface_intersection::refine_to_intersection(
231            first, second, point, tolerance, reach,
232        )? {
233            inserts.push((span, refined));
234        }
235    }
236    Ok((worst, missing, inserts))
237}
238
239/// Refit `section` until its mid-spans sit within `floor` of both carriers,
240/// inserting true intersection points where they do not. Returns the fit that
241/// stands, the worst mid-span miss it still carries, the unrefined fit's worst
242/// (what the gate read), the rounds spent and the exit.
243fn refine_section_against_carriers(
244    section: &mut Vec<Vec3>,
245    fit: PolylineFit,
246    first: &NurbsSurface,
247    second: &NurbsSurface,
248    fit_tolerance: f64,
249    tolerance: f64,
250    local_fit: bool,
251    gate: f64,
252) -> Result<(PolylineFit, f64, f64, usize, MidspanExit), String> {
253    let floor = fit_tolerance * SECTION_MIDSPAN_FRACTION;
254    let mut unrefined = f64::NAN;
255    let mut fit = fit;
256    let mut rounds = 0usize;
257    let mut previous: Option<(PolylineFit, f64, usize)> = None;
258    loop {
259        let (worst, missing, inserts) = midspan_misses(&fit, first, second, floor, tolerance)?;
260        if rounds == 0 {
261            unrefined = worst;
262        }
263        if worst <= floor {
264            return Ok((fit, worst, unrefined, rounds, MidspanExit::Converged));
265        }
266        if rounds == 0 && worst <= gate {
267            return Ok((fit, worst, unrefined, rounds, MidspanExit::BelowGate));
268        }
269        if let Some((earlier, earlier_worst, earlier_missing)) = previous.take() {
270            // A round that did not earn its keep is discarded whole: the fit
271            // of the last round that did stands (on a corner, the unrefined
272            // fit), not whichever of the two happens to read a smaller max.
273            if worst > earlier_worst * SECTION_MIDSPAN_PROGRESS && missing >= earlier_missing {
274                return Ok((earlier, earlier_worst, unrefined, rounds, MidspanExit::NoProgress));
275            }
276        }
277        if inserts.is_empty() {
278            return Ok((fit, worst, unrefined, rounds, MidspanExit::Stalled));
279        }
280        if rounds >= SECTION_MIDSPAN_ROUNDS {
281            return Ok((fit, worst, unrefined, rounds, MidspanExit::RoundBudget));
282        }
283        if section.len() + inserts.len() > MAX_FIT_STATIONS {
284            return Ok((fit, worst, unrefined, rounds, MidspanExit::StationCeiling));
285        }
286        // Insert each refined point into the run between its span's stations,
287        // on the nearest segment of the run there. Positions are located
288        // against the run BEFORE any insertion of this round, then applied
289        // from the back so earlier indices stay valid. The kept stations are
290        // found by a FORWARD search from the previous one: a closed section
291        // ends on its first station, and a search from the start would put
292        // the last span's far end at index 0 and skip that span forever.
293        let station_index = |point: Vec3, from: usize| -> Option<usize> {
294            section[from..]
295                .iter()
296                .position(|p| p.sub(point).length() <= 1e-15 * (1.0 + point.length()))
297                .map(|offset| from + offset)
298        };
299        let mut placed: Vec<(usize, Vec3)> = Vec::new();
300        for (span, point) in inserts {
301            let Some(lo) = station_index(fit.kept[span], 0) else {
302                continue;
303            };
304            let Some(hi) = station_index(fit.kept[span + 1], lo + 1) else {
305                continue;
306            };
307            if hi <= lo {
308                continue;
309            }
310            if section[lo..=hi].iter().any(|p| p.sub(point).length() <= floor) {
311                continue; // already a station: inserting it again refines nothing
312            }
313            let mut best = (lo, f64::INFINITY);
314            for index in lo..hi {
315                let d2 = distance_to_segment_sq(point, section[index], section[index + 1]);
316                if d2 < best.1 {
317                    best = (index, d2);
318                }
319            }
320            placed.push((best.0 + 1, point));
321        }
322        if placed.is_empty() {
323            return Ok((fit, worst, unrefined, rounds, MidspanExit::Stalled));
324        }
325        placed.sort_by(|a, b| b.0.cmp(&a.0));
326        for (at, point) in placed {
327            section.insert(at, point);
328        }
329        // Every station of the denser run is entitled to be an interpolation
330        // station: the ladder starts at the run's own count.
331        let maximum_points = section.len().min(MAX_FIT_STATIONS);
332        let refit = fit_polyline_of_degree(section, fit_tolerance, maximum_points, local_fit, section_fit_degree())?;
333        previous = Some((std::mem::replace(&mut fit, refit), worst, missing));
334        rounds += 1;
335    }
336}
337use super::*;
338use super::gate_census::{census_pair, fit_census_enabled};
339
340pub fn build_imprints(
341    solid_a: &BrepSolid,
342    solid_b: &BrepSolid,
343    options: &ImprintOptions,
344) -> Result<ImprintResultRecord, KernelRefusal> {
345    build_imprints_impl(solid_a, solid_b, options, false)
346}
347
348/// Imprint temporary open offset sheets. A section riding one sheet's trim
349/// still cuts the other sheet when no boundary copy supplied that section.
350pub(crate) fn build_carrier_imprints(
351    solid_a: &BrepSolid,
352    solid_b: &BrepSolid,
353    options: &ImprintOptions,
354) -> Result<ImprintResultRecord, KernelRefusal> {
355    build_imprints_impl(solid_a, solid_b, options, true)
356}
357
358fn build_imprints_impl(
359    solid_a: &BrepSolid,
360    solid_b: &BrepSolid,
361    options: &ImprintOptions,
362    open_carriers: bool,
363) -> Result<ImprintResultRecord, KernelRefusal> {
364    let mut section_evidence = false;
365    let edges = edge_map(solid_a, solid_b);
366    let mut builder = ImprintBuilder {
367        open_carriers,
368        edges,
369        tolerance: options.tolerance,
370        barrier_edges: HashSet::default(),
371        overlap_ridden_edges: HashSet::default(),
372        scale: solid_scale(solid_a).max(solid_scale(solid_b)),
373        vertices: Vec::new(),
374        vertex_radii: HashMap::default(),
375        pieces: Vec::new(),
376        by_face: HashMap::default(),
377        edge_splits: HashMap::default(),
378        marched_pieces: HashSet::default(),
379        next_id: 1,
380    };
381    let mut first_faces = faces(solid_a, 0);
382    let mut second_faces = faces(solid_b, 1);
383    // Per-face trim-window carriers, computed once: tighter BVH bounds, and
384    // the seed/march stages walk the window instead of the full carrier
385    // (identical geometry and parameterization inside the window).
386    //
387    // A window LIFTED across a closed direction's seam (`restricted_carrier`)
388    // is the exception to "same parameterization": it is the trim's frame, a
389    // period off the carrier's on one side. Its face carries it as its
390    // `chart`, and every uv the imprint reads for that face — the clip's
391    // containment, the sections' pcurves, a seed's normal — is read there.
392    let first_restricted: Vec<Option<MarchWindow>> = first_faces
393        .iter()
394        .map(|tagged| restricted_carrier(tagged.face))
395        .collect();
396    let second_restricted: Vec<Option<MarchWindow>> = second_faces
397        .iter()
398        .map(|tagged| restricted_carrier(tagged.face))
399        .collect();
400    for (tagged, window) in first_faces.iter_mut().zip(&first_restricted) {
401        tagged.lifted = window.as_ref().filter(|window| window.lifted).map(|window| &window.surface);
402    }
403    for (tagged, window) in second_faces.iter_mut().zip(&second_restricted) {
404        tagged.lifted = window.as_ref().filter(|window| window.lifted).map(|window| &window.surface);
405    }
406    let first_faces = first_faces;
407    let second_faces = second_faces;
408    // The lifted charts, by key, for the post-passes that rebuild a pcurve
409    // after the mint (junction canonicalization, the origin dissolve, the
410    // truncation bridge). A face absent here is read on its own carrier.
411    let charts: FaceCharts<'_> = first_faces
412        .iter()
413        .chain(second_faces.iter())
414        .filter_map(|tagged| tagged.lifted.map(|surface| (tagged.key(), surface)))
415        .collect();
416    let first_bounds = first_faces
417        .iter()
418        .zip(&first_restricted)
419        .map(|(tagged, restricted)| {
420            face_bounds(
421                tagged.face,
422                restricted.as_ref().map(|window| &window.surface),
423                &builder.edges,
424                tagged.operand,
425                options.tolerance,
426            )
427        })
428        .collect::<Result<Vec<_>, _>>()?;
429    let second_bounds = second_faces
430        .iter()
431        .zip(&second_restricted)
432        .map(|(tagged, restricted)| {
433            face_bounds(
434                tagged.face,
435                restricted.as_ref().map(|window| &window.surface),
436                &builder.edges,
437                tagged.operand,
438                options.tolerance,
439            )
440        })
441        .collect::<Result<Vec<_>, _>>()?;
442    let second_bvh = Bvh::build(&second_bounds);
443    // Per-face classifier data and edge lists/subcurves, computed once and
444    // reused across every pair the face participates in.
445    let first_classify = first_faces
446        .iter()
447        .map(|tagged| SurfaceClassifyData::build(&tagged.face.surface, options.tolerance))
448        .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
449    let second_classify = second_faces
450        .iter()
451        .map(|tagged| SurfaceClassifyData::build(&tagged.face.surface, options.tolerance))
452        .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
453    let mut face_edge_lists: HashMap<FaceKey, Vec<&EdgeRecord>> = HashMap::default();
454    for tagged in first_faces.iter().chain(second_faces.iter()) {
455        face_edge_lists.insert(tagged.key(), face_edges(*tagged, &builder.edges)?);
456    }
457    let mut subcurves: HashMap<(u8, u64), NurbsCurve> = HashMap::default();
458    for (&key, edge) in &builder.edges {
459        if !edge.degenerate {
460            // Failures fall through: the use sites recompute and surface the
461            // original error exactly where the uncached code did.
462            if let Ok(curve) = edge_subcurve(edge) {
463                subcurves.insert(key, curve);
464            }
465        }
466    }
467    let cached_subcurve = |operand: u8, edge: &EdgeRecord| -> Result<NurbsCurve, KernelRefusal> {
468        match subcurves.get(&(operand, edge.id)) {
469            Some(curve) => Ok(curve.clone()),
470            None => edge_subcurve(edge),
471        }
472    };
473    let mut profile = ImprintProfile::new();
474    let mut tangent_nodes: Vec<Vec3> = Vec::new();
475    let mut cosurface_pairs: Vec<(FaceKey, FaceKey)> = Vec::new();
476    let mut paired = Vec::new();
477    for (first_index, first) in first_faces.iter().enumerate() {
478        let first = *first;
479        paired.clear();
480        second_bvh.overlapping(
481            first_bounds[first_index],
482            options.tolerance * 100.0,
483            &mut paired,
484        );
485        paired.sort_unstable();
486        let debug_pairs = std::env::var("BREP_DEBUG_PAIRS").is_ok();
487        if debug_pairs {
488            eprintln!(
489                "bvh first_face={} -> {} candidate pairs {:?}",
490                first.face.id,
491                paired.len(),
492                paired
493                    .iter()
494                    .map(|&index| second_faces[index].face.id)
495                    .collect::<Vec<_>>()
496            );
497        }
498        for &second_index in &paired {
499            let second = second_faces[second_index];
500            profile.pairs += 1;
501            // Set when this pair's section passes through an isolated TANGENT
502            // NODE inside both trims (see the classification below). The march
503            // is attempted anyway — that is the whole point — but a marcher
504            // that cannot get through a node must refuse in the tangent-node
505            // class naming the node, not leak its own step-budget message.
506            let mut tangent_node: Option<Vec3> = None;
507            let mut lap_start = None;
508            profile.lap(&mut lap_start);
509            let pair_classification = classify_surface_pair_cached(
510                &first.face.surface,
511                &first_classify[first_index],
512                &second.face.surface,
513                &second_classify[second_index],
514                options.tolerance,
515                PAIR_ANGULAR_TOLERANCE,
516            ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
517            profile.classify += profile.lap(&mut lap_start);
518            if pair_classification.relation == SurfacePairRelation::Disjoint {
519                if debug_pairs {
520                    eprintln!(
521                        "pair {}x{}: DISJOINT-cull sep={:.4e}",
522                        first.face.id, second.face.id, pair_classification.minimum_separation
523                    );
524                }
525                continue;
526            }
527            // Coincident carriers are never marched: their true intersection
528            // is a 2D region, not a curve, so anything the marcher traces on
529            // them is noise ("similar faces we do not intersect" — Golovanov
530            // §6.2).  The sampled classification catches coincident pairs
531            // the strict reconstruction test misses (partial overlaps,
532            // differing parameterizations); both route to the boundary-curve
533            // exchange below.
534            let is_cosurface = pair_classification.relation == SurfacePairRelation::Cosurface
535                || cosurface_pair(&first.face.surface, &second.face.surface, options.tolerance)?;
536            profile.cosurface += profile.lap(&mut lap_start);
537            if is_cosurface {
538                if debug_pairs {
539                    eprintln!("pair {}x{}: cosurface", first.face.id, second.face.id);
540                }
541                cosurface_pairs.push((first.key(), second.key()));
542                for edge in &face_edge_lists[&second.key()] {
543                    if !edge.degenerate {
544                        builder.process_curve(
545                            cached_subcurve(second.operand, edge)?,
546                            first,
547                            second,
548                            &[first],
549                            &[first],
550                            false,
551                        )?;
552                    }
553                }
554                for edge in &face_edge_lists[&first.key()] {
555                    if !edge.degenerate {
556                        builder.process_curve(
557                            cached_subcurve(first.operand, edge)?,
558                            first,
559                            second,
560                            &[second],
561                            &[second],
562                            false,
563                        )?;
564                    }
565                }
566                profile.process_curve += profile.lap(&mut lap_start);
567                continue;
568            }
569
570            // The exact section lanes below (planar-iso, analytic) mint this
571            // pair's whole intersection, the part over either trim included, so
572            // a SPAN of an edge lying on the other face would be a second copy
573            // of a curve they already mint — which reorders the face's pieces
574            // and with them its fragments' names (the 2026-09-10 chamfer groove
575            // rim's `Box_PX` / `Box_PX_1`). Where they answer, only a whole
576            // edge is exchanged, as before.
577            let planar_iso = planar_iso_intersection(
578                &first.face.surface,
579                &second.face.surface,
580                options.tolerance,
581            )?;
582            profile.planar_iso += profile.lap(&mut lap_start);
583            let analytic = crate::intersect_analytic_pair(
584                &first.face.surface,
585                &second.face.surface,
586                options.tolerance,
587            );
588            profile.analytic += profile.lap(&mut lap_start);
589            let exact_section = planar_iso.is_some() || analytic.is_some();
590
591            for edge in &face_edge_lists[&first.key()] {
592                if !edge.degenerate {
593                    let curve = cached_subcurve(first.operand, edge)?;
594                    let spans = curve_spans_on_face(curve, second.face, &face_edge_lists[&second.key()], options.tolerance, exact_section)?;
595                    for span in spans {
596                        builder.process_curve(span, first, second, &[second], &[second], false)?;
597                    }
598                }
599            }
600            for edge in &face_edge_lists[&second.key()] {
601                if !edge.degenerate {
602                    let curve = cached_subcurve(second.operand, edge)?;
603                    let spans = curve_spans_on_face(curve, first.face, &face_edge_lists[&first.key()], options.tolerance, exact_section)?;
604                    for span in spans {
605                        builder.process_curve(span, first, second, &[first], &[first], false)?;
606                    }
607                }
608            }
609            profile.lies_on += profile.lap(&mut lap_start);
610            // CENSUS ONLY (`BREP_FIT_CENSUS=1`): the lane this pair takes under
611            // the count-keyed and the geometry-keyed gates, and — when it is
612            // marched here and answered exactly there — the point-set comparison
613            // of the two, printed when `pair_census` is dropped. `imprint/gate_census.rs`.
614            let mut pair_census = if fit_census_enabled() {
615                census_pair(first, second, options.tolerance)?
616            } else {
617                None
618            };
619            if let Some(curve) = planar_iso {
620                if debug_pairs {
621                    eprintln!("pair {}x{}: planar_iso", first.face.id, second.face.id);
622                }
623                builder.process_curve(
624                    curve,
625                    first,
626                    second,
627                    &[first, second],
628                    &[first, second],
629                    true,
630                )?;
631                profile.process_curve += profile.lap(&mut lap_start);
632                continue;
633            }
634
635            // Recognized analytic pairs produce their exact intersection
636            // curves (lines, circles, ellipses) directly — no marching, no
637            // polyline fitting, no chord-sag drift. An empty result is a
638            // proof of non-intersection and also skips the marcher. (Read
639            // above, with the planar-iso curve.)
640            if let Some(curves) = analytic {
641                if debug_pairs {
642                    eprintln!(
643                        "pair {}x{}: analytic x{}",
644                        first.face.id,
645                        second.face.id,
646                        curves.len()
647                    );
648                }
649                for curve in curves {
650                    builder.process_curve(
651                        curve,
652                        first,
653                        second,
654                        &[first, second],
655                        &[first, second],
656                        true,
657                    )?;
658                }
659                profile.process_curve += profile.lap(&mut lap_start);
660                continue;
661            }
662
663            // A pair whose every touching sample is TANGENTIAL cannot
664            // contain a transverse intersection curve: the marcher would
665            // walk the tangency band's noise, which neither closes nor
666            // reaches a boundary (the distance-0 glue-extrude runaway).
667            // Shared topology at a tangential contact comes from the
668            // boundary-curve exchange above ("similar faces we do not
669            // intersect" extended to tangential contacts — Golovanov §6.2;
670            // where surfaces CROSS through a tangency, off-line samples
671            // have non-parallel normals and the pair still marches).
672            if pair_classification.relation == SurfacePairRelation::NearTangent
673                && pair_classification.tangential_only
674            {
675                // The coarse 5×5 classifier can flag `tangential_only` off an
676                // incidental tangential KISS between two curved carriers and
677                // miss the transverse loop where they actually cross (two
678                // overlapping tori: their tubes cross while their inner walls
679                // just touch). Before honouring the skip, a GATED supplemental
680                // detector (denser two-sided seeding, tangency-band seeds
681                // rejected, trace-exhaustion swallowed, transverse-only
682                // branches) checks whether a genuine transverse curve of
683                // meaningful length lies inside BOTH trims.
684                //
685                // Such a pair's intersection is SINGULAR where a tangency
686                // sits on it, and what the imprint can do about that depends
687                // entirely on the SHAPE of the tangency — see
688                // `imprint/tangent_contact.rs`. An isolated NODE (two branches
689                // crossing, as every equal-radius pair produces) is already
690                // assembled correctly by the ordinary march plus the 2D
691                // arrangement's pinch carving; an EXTENDED contact along a
692                // whole curve has no section to imprint at all and tears the
693                // shell. Both are classified below and only the second is
694                // refused. Skipping either silently would DROP the
695                // intersection (torus∪torus double-counts; torus−torus removes
696                // nothing — the two structural bugs the semantic oracle
697                // found), so nothing here ever falls through quietly.
698                let first_march = first_restricted[first_index]
699                    .as_ref()
700                    .map(|window| &window.surface)
701                    .unwrap_or(&first.face.surface);
702                let second_march = second_restricted[second_index]
703                    .as_ref()
704                    .map(|window| &window.surface)
705                    .unwrap_or(&second.face.surface);
706                let supplemental = intersect_surfaces_supplemental(
707                    first_march,
708                    second_march,
709                    &SurfaceIntersectionOptions {
710                        tolerance: options.tolerance,
711                        maximum_step: options.maximum_ssi_step,
712                        ..Default::default()
713                    },
714                ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
715                let mut dropped_length = 0.0f64;
716                let mut clipped: Vec<Vec<Vec3>> = Vec::new();
717                for branch in &supplemental {
718                    for run in clip_branch_to_trims(&branch.points, first, second)? {
719                        if run.len() < 2 {
720                            continue;
721                        }
722                        let length: f64 = run
723                            .windows(2)
724                            .map(|pair| pair[1].sub(pair[0]).length())
725                            .sum();
726                        dropped_length = dropped_length.max(length);
727                        clipped.push(run);
728                    }
729                }
730                if dropped_length <= options.tolerance * 100.0 {
731                    if debug_pairs {
732                        eprintln!(
733                            "pair {}x{}: tangential-only contact, march skipped",
734                            first.face.id, second.face.id
735                        );
736                    }
737                    continue;
738                }
739                // A transverse curve EXISTS. What would make it unimprintable is
740                // a tangent NODE on it — and the node need not lie inside the
741                // trims. Two equal-radius pipe arms are tangent to each other
742                // where their axes' common perpendicular leaves the junction,
743                // and a joint ball wider than the arms trims that crotch off
744                // both faces; what is left inside the trims is an ordinary
745                // crossing. So the question is not "does a transverse curve
746                // exist" but "does the curve INSIDE BOTH TRIMS reach a
747                // tangency". Only the latter is the checkerboard case.
748                let mut tangency_inside_trims = false;
749                let mut tangency_witness = Vec3::default();
750                'clipped: for run in &clipped {
751                    for (index, &point) in run.iter().enumerate() {
752                        if pair_normals_parallel_at(first, second, point)? {
753                            tangency_inside_trims = true;
754                            tangency_witness = point;
755                            if debug_pairs {
756                                eprintln!(
757                                    "pair {}x{}: TANGENCY at run index {}/{} ({:.6},{:.6},{:.6}) dropped_len={:.4e}",
758                                    first.face.id, second.face.id, index, run.len(),
759                                    point.x, point.y, point.z, dropped_length
760                                );
761                            }
762                            break 'clipped;
763                        }
764                    }
765                }
766                if tangency_inside_trims {
767                    // WHAT SHAPE is the tangency? The witness is only a marched
768                    // sample within the transverse-seed angular gate, so it is
769                    // REFINED onto the contact before being classified — at the
770                    // raw witness a G1 cylinder/torus join and a genuine
771                    // equal-radius node are eighteen-fold apart, which would be
772                    // a band; at the refined contact they are thirty orders
773                    // apart, which is a rank question. See
774                    // `imprint/tangent_contact.rs`.
775                    let contact = classify_tangent_contact(
776                        &first.face.surface,
777                        &second.face.surface,
778                        tangency_witness,
779                        options.tolerance,
780                    )?;
781                    if debug_pairs {
782                        eprintln!(
783                            "pair {}x{}: contact classification {:?}",
784                            first.face.id, second.face.id, contact
785                        );
786                    }
787                    match contact {
788                        // An isolated node: the section is a curve everywhere
789                        // but that one point, so the ordinary march below runs
790                        // and the 2D arrangement carves the pinch on both
791                        // faces. `tangent_node` records it so a march that
792                        // cannot get through still refuses in THIS class rather
793                        // than leaking the marcher's own message.
794                        Some(contact) if contact.is_isolated_node() => {
795                            tangent_node = Some(contact.point);
796                            tangent_nodes.push(contact.point);
797                        }
798                        // An extended contact (rank-deficient) or a witness we
799                        // could not refine onto any contact at all (None — so
800                        // nothing is proven and the pre-classification refusal
801                        // stands). Neither has a section curve the imprint can
802                        // represent.
803                        other => {
804                            let shape = match other {
805                                Some(contact) => contact.describe(),
806                                None => format!(
807                                    "the tangency near ({:.6},{:.6},{:.6}) could not be refined \
808                                     onto a contact, so its shape is unproven",
809                                    tangency_witness.x, tangency_witness.y, tangency_witness.z
810                                ),
811                            };
812                            return Err(KernelRefusal::new(
813                                RefusalClass::TangentNodeSingularity,
814                                KernelStage::Intersect,
815                                format!(
816                                    "boolean: unsupported singular/tangent-node surface intersection \
817                                     between faces {} and {}: {shape}",
818                                    first.face.id, second.face.id
819                                ),
820                            ));
821                        }
822                    }
823                }
824                if debug_pairs && tangent_node.is_none() {
825                    // Transverse the whole way inside both trims: the coarse
826                    // 5x5 classifier only saw the tangency the trims cut away.
827                    eprintln!(
828                        "pair {}x{}: classified tangential-only, but the curve inside both \
829                         trims is transverse ({dropped_length:.4}) — marching",
830                        first.face.id, second.face.id
831                    );
832                }
833            }
834
835            // The trim-window carriers: same surface and parameterization
836            // over the window, so hit (u, v) values remain valid on the
837            // originals; out-of-window intersections could never survive
838            // clip_branch_to_trims and are not walked at all.
839            let first_march = first_restricted[first_index]
840                .as_ref()
841                .map(|window| &window.surface)
842                .unwrap_or(&first.face.surface);
843            let second_march = second_restricted[second_index]
844                .as_ref()
845                .map(|window| &window.surface)
846                .unwrap_or(&second.face.surface);
847            let mut seed_points = Vec::new();
848            // Smallest |edge_tangent · surface_normal| over accepted seeds — the
849            // local grazing measure at the trim crossings (near 0 = the edge
850            // pierces the other surface tangentially). Gates the near-tangent
851            // clip-order rescue below to genuinely grazing pairs.
852            let mut min_seed_tangency = f64::INFINITY;
853            // Two grazing crossings of one trim edge bound a real, narrow
854            // interval. It cannot tolerate a section fit that misses either
855            // crossing, even when a coarse whole-model distance looks small.
856            let mut paired_grazing_crossings = false;
857            for (face, other_march, other) in
858                [(first, second_march, second), (second, first_march, first)]
859            {
860                for edge in &face_edge_lists[&face.key()] {
861                    if edge.degenerate {
862                        continue;
863                    }
864                    let mut grazing_crossings: Vec<Vec3> = Vec::new();
865                    for hit in intersect_curve_surface(&edge.curve, other_march, options.tolerance).or_refuse(KernelStage::Intersect, "intersect_curve_surface")?
866                    {
867                        if hit.t < edge.t0 - 1e-9 || hit.t > edge.t1 + 1e-9 {
868                            continue;
869                        }
870                        let derivative = edge.curve.derivatives(hit.t, 1).or_refuse(KernelStage::Intersect, "derivatives")?[1];
871                        // A STATIONARY POINT on the edge curve is not a reason
872                        // to refuse the boolean. An involute flank starts at
873                        // the base circle with an exactly zero tangent, so a
874                        // gear tooth's profile carries one cusp per flank, and
875                        // a hit landing on that parameter used to propagate
876                        // "Vec3.normalized: zero-length vector" out of the
877                        // whole union (the 2026-09-14 herringbone report: the
878                        // second tooth band's z=0 cap edge meets the first
879                        // band's flank exactly at the cusp). The direction is
880                        // still defined as the one-sided limit, so read it from
881                        // just inside the edge's own range; an edge with no
882                        // direction at all simply contributes no seed, exactly
883                        // as a hit whose face normal is unavailable already
884                        // does two lines below. Seeds are marcher hints, so a
885                        // dropped one costs a hint, never an answer.
886                        // Escape hatch for tamper-verification:
887                        // BREP_CUSP_TANGENT_RESCUE=0 restores the refusal.
888                        let tangent = match derivative.normalized() {
889                            Ok(tangent) => tangent,
890                            Err(error) => {
891                                if std::env::var("BREP_CUSP_TANGENT_RESCUE").as_deref() == Ok("0") {
892                                    return Err(error)
893                                        .or_refuse(KernelStage::Intersect, "normalized");
894                                }
895                                match edge.curve.stationary_tangent_rescue(hit.t, edge.t0, edge.t1)
896                                {
897                                    Some(tangent) => tangent,
898                                    None => continue,
899                                }
900                            }
901                        };
902                        // `hit` was solved on the other face's MARCH WINDOW, so
903                        // its uv is in that face's chart — a period off the
904                        // carrier's where the window is lifted, and `normal`
905                        // clamps a parameter past its domain.
906                        let normal = match other.chart().normal(hit.u, hit.v) {
907                            Ok(normal) => normal,
908                            Err(_) => continue,
909                        };
910                        if debug_pairs {
911                            eprintln!(
912                                "pair {}x{}: seed edge {} t={:.6} p=({:.5},{:.5},{:.5}) |tan.n|={:.4} {}",
913                                first.face.id,
914                                second.face.id,
915                                edge.id,
916                                hit.t,
917                                hit.point.x,
918                                hit.point.y,
919                                hit.point.z,
920                                tangent.dot(normal).abs(),
921                                if tangent.dot(normal).abs() >= 0.1 { "ACCEPT" } else { "reject" }
922                            );
923                        }
924                        if tangent.dot(normal).abs() >= 0.1 {
925                            seed_points.push(hit.point);
926                            min_seed_tangency = min_seed_tangency.min(tangent.dot(normal).abs());
927                            section_evidence = true;
928                        } else {
929                            if grazing_crossings.iter().any(|point| {
930                                point.sub(hit.point).length() > assembler_weld(options.tolerance)
931                            }) {
932                                paired_grazing_crossings = true;
933                            }
934                            grazing_crossings.push(hit.point);
935                        }
936                    }
937                }
938            }
939            profile.seeds += profile.lap(&mut lap_start);
940            profile.marched_pairs += 1;
941            if debug_pairs {
942                eprintln!(
943                    "pair {}x{}: marching (relation {:?})...",
944                    first.face.id, second.face.id, pair_classification.relation
945                );
946            }
947            let marched = intersect_surfaces(
948                first_march,
949                second_march,
950                &SurfaceIntersectionOptions {
951                    tolerance: options.tolerance,
952                    maximum_step: march_maximum_step(
953                        &pair_classification,
954                        options.maximum_ssi_step,
955                        builder.scale,
956                    ),
957                    seed_points: seed_points.clone(),
958                    ..Default::default()
959                },
960            );
961            // A pair carrying a tangent node keeps its OWN refusal class when
962            // the march fails: the node is why the trace cannot close, and the
963            // equal-radius torus pair depends on being told so rather than on
964            // reading a step-budget message it cannot act on.
965            let marched = match (marched, tangent_node) {
966                (Err(error), Some(node)) => {
967                    return Err(KernelRefusal::new(
968                        RefusalClass::TangentNodeSingularity,
969                        KernelStage::Intersect,
970                        format!(
971                            "boolean: unsupported singular/tangent-node surface intersection \
972                             between faces {} and {}: the section through the tangent node at \
973                             ({:.6},{:.6},{:.6}) could not be marched ({error})",
974                            first.face.id, second.face.id, node.x, node.y, node.z
975                        ),
976                    ));
977                }
978                (marched, _) => marched,
979            };
980            let mut branches = marched
981            .map_err(|error| {
982                format!(
983                    "{error} (marching faces {} and {})",
984                    first.face.id, second.face.id
985                )
986            }).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
987            profile.march += profile.lap(&mut lap_start);
988            let rescue_started = profile.enabled.then(Instant::now);
989            // MARCH-ORDER SWAP RESCUE (hatch BREP_MARCH_SWAP_RESCUE=0).
990            // `intersect_surfaces` is not order-symmetric: the coupled Newton
991            // trace can fail to start/continue from a valid seed when the two
992            // surfaces are presented in one operand order yet succeed in the
993            // other. This is the sole reason t217's `subtract(sphere, step)`
994            // fails while every other op passes — pair 118x842 (sphere-first)
995            // marches ZERO branches, while 842x118 (step-first, the working
996            // a\b order) marches the section from the IDENTICAL accepted pierce
997            // seeds. When the forward order returns no branch AND an accepted
998            // transverse pierce seed exists (so a real section provably crosses
999            // both trims), retry the march with the surfaces swapped — i.e.
1000            // reproduce the exact call the working operand order makes for this
1001            // pair (measured: seed_only alone does NOT recover it — the section
1002            // is found from an auto-grid start, not from the near-tangent pierce
1003            // seeds, which the seed normal-cross gate rejects). The returned
1004            // branch points are 3D and therefore order-independent, so no
1005            // parameter remap is needed; they flow through the identical clip /
1006            // process_curve gates below, which drop any out-of-trim or
1007            // sub-length run exactly as today. The rescue only ever runs when
1008            // the forward order found NOTHING, so it cannot alter a pair that
1009            // already marched.
1010            if branches.is_empty()
1011                && !seed_points.is_empty()
1012                && pair_classification.relation == SurfacePairRelation::Candidate
1013                && std::env::var("BREP_MARCH_SWAP_RESCUE").as_deref() != Ok("0")
1014            {
1015                // FAIL-SOFT: the forward order already returned Ok(empty); a
1016                // swapped-march error (trace-exhaustion is a real error class —
1017                // `intersect_surfaces_supplemental` swallows it for exactly this
1018                // reason) must NOT convert that graceful empty into a hard error.
1019                // On Err, keep the (empty) forward result and carry on.
1020                let swapped = intersect_surfaces(
1021                    second_march,
1022                    first_march,
1023                    &SurfaceIntersectionOptions {
1024                        tolerance: options.tolerance,
1025                        maximum_step: march_maximum_step(
1026                            &pair_classification,
1027                            options.maximum_ssi_step,
1028                            builder.scale,
1029                        ),
1030                        seed_points: seed_points.clone(),
1031                        ..Default::default()
1032                    },
1033                )
1034                .unwrap_or_default();
1035                if debug_pairs {
1036                    eprintln!(
1037                        "pair {}x{}: MARCH-SWAP RESCUE attempt -> {} branches, pts {:?}",
1038                        first.face.id,
1039                        second.face.id,
1040                        swapped.len(),
1041                        swapped.iter().map(|b| b.points.len()).collect::<Vec<_>>()
1042                    );
1043                }
1044                if !swapped.is_empty() {
1045                    branches = swapped;
1046                }
1047            }
1048            // NEAR-TANGENT CLIP-ORDER RESCUE (hatch BREP_MARCH_SWAP_CLIP_RESCUE=0).
1049            // Companion to the empty-branch MARCH-SWAP RESCUE above:
1050            // `intersect_surfaces` is order-asymmetric not only in WHETHER it
1051            // marches, but in the exact sample positions of the section
1052            // polyline. On a NEAR-TANGENT graze, `clip_branch_to_trims`
1053            // classifies those samples against the mutual trims, and a sample
1054            // landing just past the near-tangent boundary is dropped as a
1055            // false-Outside that TRUNCATES the clipped section. The two operand
1056            // orders drop DIFFERENT near-tangent tail samples, so one order
1057            // yields a section ~0.1-0.2mm shorter at ONE endpoint (t660: b\a's
1058            // step-first order clips pairs 223x105/399x105 ~0.22/0.14mm short of
1059            // the section a\b's cyl-first order keeps, stranding edge 173
1060            // one-use). Near-tangent clip errors are almost exclusively
1061            // false-Outside (a point truly outside a trim rarely projects to an
1062            // in-trim uv), so the order with the LONGER clipped section suffered
1063            // fewer drops and is the more complete one. When the swapped order
1064            // marches the SAME branch structure with a meaningfully longer
1065            // clipped total, adopt its branches (3D points, so they flow through
1066            // the identical clip/process_curve gates below). Gated to Candidate
1067            // pairs (the near-tangent/ambiguous class; clean Transverse
1068            // crossings clip identically in both orders and never differ) with
1069            // an accepted GRAZING pierce seed (min |tan·n| < 0.5 — a real
1070            // section provably crosses, and does so near-tangentially, the only
1071            // regime where the clip wobbles; this also bounds the extra march to
1072            // grazing pairs), and requires a >0.1%-of-length margin so
1073            // raw-sampling jitter (t660: 0.02%) cannot flip a clean pair.
1074            // Fail-soft: any swapped-march or comparison-clip error keeps the
1075            // forward result.
1076            if !branches.is_empty()
1077                && !seed_points.is_empty()
1078                && min_seed_tangency < 0.5
1079                && pair_classification.relation == SurfacePairRelation::Candidate
1080                && std::env::var("BREP_MARCH_SWAP_CLIP_RESCUE").as_deref() != Ok("0")
1081            {
1082                let swapped = intersect_surfaces(
1083                    second_march,
1084                    first_march,
1085                    &SurfaceIntersectionOptions {
1086                        tolerance: options.tolerance,
1087                        maximum_step: march_maximum_step(
1088                            &pair_classification,
1089                            options.maximum_ssi_step,
1090                            builder.scale,
1091                        ),
1092                        seed_points: seed_points.clone(),
1093                        ..Default::default()
1094                    },
1095                )
1096                .unwrap_or_default();
1097                // Only a swap that reproduces the SAME branch count — a
1098                // refined-endpoint variant of the same section, not a different
1099                // branch decomposition (guards against adopting a spurious
1100                // extra branch as "longer").
1101                if !swapped.is_empty() && swapped.len() == branches.len() {
1102                    let totals = (|| -> Result<(f64, f64), KernelRefusal> {
1103                        let mut fwd = 0.0;
1104                        for b in &branches {
1105                            let refined = insert_seed_points_into_branch(
1106                                &b.points,
1107                                &seed_points,
1108                                options.tolerance,
1109                            );
1110                            for run in clip_branch_to_trims(&refined, first, second)? {
1111                                fwd += run
1112                                    .windows(2)
1113                                    .map(|p| p[1].sub(p[0]).length())
1114                                    .sum::<f64>();
1115                            }
1116                        }
1117                        let mut swp = 0.0;
1118                        for b in &swapped {
1119                            let refined = insert_seed_points_into_branch(
1120                                &b.points,
1121                                &seed_points,
1122                                options.tolerance,
1123                            );
1124                            for run in clip_branch_to_trims(&refined, first, second)? {
1125                                swp += run
1126                                    .windows(2)
1127                                    .map(|p| p[1].sub(p[0]).length())
1128                                    .sum::<f64>();
1129                            }
1130                        }
1131                        Ok((fwd, swp))
1132                    })();
1133                    if let Ok((fwd_clip, swp_clip)) = totals {
1134                        let margin = fwd_clip.max(swp_clip) * 1.0e-3;
1135                        let adopt = swp_clip > fwd_clip + margin;
1136                        if debug_pairs {
1137                            eprintln!(
1138                                "pair {}x{}: CLIP-SWAP RESCUE fwd_clip={:.6} swp_clip={:.6} margin={:.6}{}",
1139                                first.face.id,
1140                                second.face.id,
1141                                fwd_clip,
1142                                swp_clip,
1143                                margin,
1144                                if adopt { " ADOPT" } else { "" }
1145                            );
1146                        }
1147                        if adopt {
1148                            branches = swapped;
1149                        }
1150                    }
1151                }
1152            }
1153            if let Some(started) = rescue_started {
1154                profile.rescue_march += started.elapsed();
1155            }
1156            if branches.iter().any(|branch| branch.points.len() >= 2) {
1157                section_evidence = true;
1158            }
1159            if debug_pairs {
1160                eprintln!(
1161                    "pair {}x{}: MARCH {} branches, pts {:?}",
1162                    first.face.id,
1163                    second.face.id,
1164                    branches.len(),
1165                    branches.iter().map(|b| b.points.len()).collect::<Vec<_>>()
1166                );
1167            }
1168            for branch in branches {
1169                // The pierce seeds are the section's exact trim-crossing
1170                // points — insert them so no trim interval shorter than the
1171                // march step is invisible to the point-classification clip.
1172                let refined_points =
1173                    insert_seed_points_into_branch(&branch.points, &seed_points, options.tolerance);
1174                let clip_started = profile.enabled.then(Instant::now);
1175                let runs = clip_branch_to_trims(&refined_points, first, second)?;
1176                if let Some(started) = clip_started {
1177                    profile.clip += started.elapsed();
1178                }
1179                for run in runs {
1180                    if debug_pairs {
1181                        eprintln!(
1182                            "pair {}x{}: clip run len_pts={} length={:.4e}",
1183                            first.face.id,
1184                            second.face.id,
1185                            run.len(),
1186                            run.windows(2)
1187                                .map(|pair| pair[1].sub(pair[0]).length())
1188                                .sum::<f64>()
1189                        );
1190                    }
1191                    if run.len() < 2 {
1192                        continue;
1193                    }
1194                    let length: f64 = run
1195                        .windows(2)
1196                        .map(|pair| pair[1].sub(pair[0]).length())
1197                        .sum();
1198                    if length <= options.tolerance * 100.0 {
1199                        continue;
1200                    }
1201                    let shared_started = profile.enabled.then(Instant::now);
1202                    let follows_shared = branch_follows_shared_boundary(
1203                        &run,
1204                        first,
1205                        second,
1206                        &builder.edges,
1207                        options.tolerance,
1208                        builder.scale,
1209                    )?;
1210                    if let Some(started) = shared_started {
1211                        profile.shared_boundary += started.elapsed();
1212                    }
1213                    if follows_shared {
1214                        if debug_pairs {
1215                            eprintln!(
1216                                "pair {}x{}: run dropped (follows shared boundary)",
1217                                first.face.id, second.face.id
1218                            );
1219                        }
1220                        continue;
1221                    }
1222                    if let Some(census) = pair_census.as_mut() {
1223                        census.record_stations(&run);
1224                    }
1225                    let pieces_before = builder.pieces.len();
1226                    let chunk_points = options.fit_chunk_points.unwrap_or(run.len()).max(2);
1227                    let mut start = 0;
1228                    while start + 1 < run.len() {
1229                        let end = (start + chunk_points - 1).min(run.len() - 1);
1230                        let fit_started = profile.enabled.then(Instant::now);
1231                        let fit_tolerance = options.tolerance.max(1e-7);
1232                        let mut section: Vec<Vec3> = run[start..=end].to_vec();
1233                        let slivers = even_sliver_ends(&mut section, &first.face.surface, &second.face.surface, options.tolerance)
1234                            .or_refuse(KernelStage::Intersect, "csg.imprint.driver.sliver_end")?;
1235                        if slivers > 0 {
1236                            if let Ok(path) = std::env::var("BREP_SECTION_SLIVER_CENSUS") {
1237                                use std::io::Write;
1238                                if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1239                                    let _ = writeln!(file, "sliver-end census: pair {}x{} slivers {slivers} stations {}", first.face.id, second.face.id, section.len());
1240                                }
1241                            }
1242                        }
1243                        let (mut fit, recovered) = crate::fit::fit_polyline_of_degree_with_recovery(
1244                            &section,
1245                            fit_tolerance,
1246                            options.maximum_fit_points,
1247                            options.local_fit,
1248                            section_fit_degree(),
1249                        ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
1250                        // A recovered fit may reproduce every march point yet
1251                        // still wander off the carriers between sparse stations.
1252                        // Measure it against both supports before accepting that
1253                        // recovery, and use the existing bounded SSI refiner.
1254                        let refine_gate = section_refine_gate()
1255                            .or_else(|| recovered.then_some(1.0)).or_else(|| {
1256                            (paired_grazing_crossings
1257                                && std::env::var("BREP_GRAZE_SECTION_REFINE").as_deref() != Ok("0"))
1258                                .then_some(1.0)
1259                        });
1260                        if let Some(gate) = refine_gate {
1261                            let stations_before = fit.kept.len();
1262                            let (refined, worst, unrefined, rounds, exit) = refine_section_against_carriers(
1263                                &mut section,
1264                                fit,
1265                                &first.face.surface,
1266                                &second.face.surface,
1267                                fit_tolerance,
1268                                options.tolerance,
1269                                options.local_fit,
1270                                gate * fit_tolerance,
1271                            ).or_refuse(KernelStage::Intersect, "csg.imprint.driver.midspan")?;
1272                            // `BREP_SECTION_REFINE_CENSUS=<file>`: one line per section,
1273                            // appended, so a corpus replay (whose children's stderr is
1274                            // captured) can be read for trip counts and growth.
1275                            if let Ok(path) = std::env::var("BREP_SECTION_REFINE_CENSUS") {
1276                                use std::io::Write;
1277                                if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
1278                                    let _ = writeln!(
1279                                        file,
1280                                        "section-refine census: pair {}x{} exit {:?} rounds {} stations {} -> {} unrefined {:.3e} worst {:.3e}",
1281                                        first.face.id, second.face.id, exit, rounds, stations_before, refined.kept.len(), unrefined, worst
1282                                    );
1283                                }
1284                            }
1285                            if debug_pairs && (rounds > 0 || !matches!(exit, MidspanExit::Converged | MidspanExit::BelowGate)) {
1286                                eprintln!(
1287                                    "pair {}x{}: mid-span refinement {:?} after {} round(s): {} stations, worst {:.3e}",
1288                                    first.face.id, second.face.id, exit, rounds, section.len(), worst
1289                                );
1290                            }
1291                            fit = refined;
1292                        }
1293                        if let Some(started) = fit_started {
1294                            profile.fit += started.elapsed();
1295                        }
1296                        if let Some(census) = pair_census.as_mut() {
1297                            census.record_fit(&fit);
1298                        }
1299                        let process_started = profile.enabled.then(Instant::now);
1300                        builder.process_curve(
1301                            fit.curve,
1302                            first,
1303                            second,
1304                            &[first, second],
1305                            &[first, second],
1306                            true,
1307                        )?;
1308                        if let Some(started) = process_started {
1309                            profile.process += started.elapsed();
1310                        }
1311                        start = end;
1312                    }
1313                    for piece in &builder.pieces[pieces_before..] {
1314                        builder.marched_pieces.insert(piece.id);
1315                    }
1316                    if debug_pairs {
1317                        eprintln!(
1318                            "pair {}x{}: run -> {} pieces",
1319                            first.face.id,
1320                            second.face.id,
1321                            builder.pieces.len() - pieces_before
1322                        );
1323                    }
1324                }
1325            }
1326            profile.clip_and_fit += profile.lap(&mut lap_start);
1327        }
1328    }
1329    profile.report();
1330    // SELF-TOUCH SPLIT: a face whose loops touch at an edge interior (a hole
1331    // tangent to a fillet setback) gets a vertex minted at the touch on BOTH
1332    // edges, so the fragment arrangement's pinch resolution assembles — see
1333    // `imprint/self_touch.rs`. Escape hatch: BREP_SELF_TOUCH_SPLIT=0.
1334    let mut tail_lap: Option<Instant> = None;
1335    let mut tail_ms = Vec::<(&str, f64)>::new();
1336    let mut lap = |name: &'static str, started: &mut Option<Instant>, tail_ms: &mut Vec<(&str, f64)>| {
1337        if profile.enabled {
1338            let now = Instant::now();
1339            if let Some(previous) = *started {
1340                tail_ms.push((name, (now - previous).as_secs_f64() * 1_000.0));
1341            }
1342            *started = Some(now);
1343        }
1344    };
1345    lap("pairs", &mut tail_lap, &mut tail_ms);
1346    if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() != Ok("0") {
1347        builder.split_self_touching_loops(&first_faces, &face_edge_lists, &cached_subcurve)?;
1348        builder.split_self_touching_loops(&second_faces, &face_edge_lists, &cached_subcurve)?;
1349    }
1350    // PIECE-ENDPOINT EXCHANGE post-pass: with every pair's process_curve
1351    // done, the piece set is final for the ridden-edge class — imprint each
1352    // open riding piece's junction endpoints onto the ridden edges (see the
1353    // method doc; hatch BREP_OVERLAP_PIECE_ENDPOINT_SPLIT=0).
1354    builder.exchange_piece_endpoint_junctions()?;
1355    lap("self_touch", &mut tail_lap, &mut tail_ms);
1356    let mut edge_splits = builder
1357        .edge_splits
1358        .into_iter()
1359        .map(|((operand, edge_id), mut parameters)| {
1360            parameters.sort_by(f64::total_cmp);
1361            EdgeSplitRecord {
1362                operand,
1363                edge_id,
1364                parameters,
1365            }
1366        })
1367        .collect::<Vec<_>>();
1368    edge_splits.sort_by_key(|record| (record.operand, record.edge_id));
1369    let mut by_face = builder
1370        .by_face
1371        .into_iter()
1372        .map(|(face, piece_ids)| FaceImprints {
1373            operand: face.operand,
1374            face_id: face.face_id,
1375            piece_ids,
1376        })
1377        .collect::<Vec<_>>();
1378    by_face.sort_by_key(|record| (record.operand, record.face_id));
1379    let section_evidence = section_evidence || !builder.pieces.is_empty();
1380    let marched_pieces = std::mem::take(&mut builder.marched_pieces);
1381    let mut result = ImprintResultRecord {
1382        tangent_nodes,
1383        vertices: builder.vertices,
1384        pieces: builder.pieces,
1385        by_face,
1386        edge_splits,
1387        barrier_edges: builder.barrier_edges.into_iter().collect(),
1388        section_evidence,
1389        cosurface_pairs,
1390    };
1391    // COINCIDENT-PIECE MERGE (problemInbox equator-tangent, and the generic
1392    // one-circle-from-many-pairs class): the SAME section curve can be minted
1393    // by several pairs — a cosurface boundary-edge copy (the cylinder cap
1394    // ring lying ON the inscribed sphere) AND the cap-plane's analytic
1395    // section ring are one circle minted twice, each carrying only its own
1396    // pair's supports/pcurves. Assembly then builds duplicate edges that
1397    // cannot both be two-use → one-use strands. Merge pieces whose curves
1398    // coincide along their whole span (bidirectional max deviation within
1399    // the weld band): keep the first, union the supports/pcurves/by_face
1400    // registrations of the rest into it. Escape hatch:
1401    // BREP_COINCIDENT_PIECE_MERGE=0.
1402    lap("collect", &mut tail_lap, &mut tail_ms);
1403    if std::env::var("BREP_COINCIDENT_PIECE_MERGE").as_deref() != Ok("0") {
1404        let weld = assembler_weld(options.tolerance).max(options.tolerance * 10.0);
1405        // Two pieces that coincide along their WHOLE spans (deviation within
1406        // the weld both ways) also have matching end points, up to direction
1407        // and a weld or two of overhang, so a piece whose ends are nowhere
1408        // near the other's needs no 33-station projection sweep. The sweep is
1409        // what made this pass quadratic in wall time: 630 pieces cost 3.2 s
1410        // of a 3.8 s imprint on the 2026-09-12 mesh-import report, all of it
1411        // rejecting pairs an end-point look already rules out.
1412        let mut ends: Vec<[Vec3; 2]> = Vec::with_capacity(result.pieces.len());
1413        for piece in &result.pieces {
1414            let [d0, d1] = piece.curve.domain().or_refuse(KernelStage::Intersect, "domain")?;
1415            ends.push([
1416                piece.curve.evaluate(d0).or_refuse(KernelStage::Intersect, "evaluate")?,
1417                piece.curve.evaluate(d1).or_refuse(KernelStage::Intersect, "evaluate")?,
1418            ]);
1419        }
1420        let end_band = 4.0 * weld;
1421        // A CLOSED piece (a full ring) has no end points to speak of: two
1422        // rings of the same circle seamed at different azimuths coincide
1423        // along their whole spans while their domain ends sit anywhere on
1424        // the ring, so a closed piece always takes the full sweep.
1425        let ends_match = |a: &[Vec3; 2], b: &[Vec3; 2]| -> bool {
1426            let near = |p: Vec3, q: Vec3| p.sub(q).length() <= end_band;
1427            near(a[0], a[1])
1428                || near(b[0], b[1])
1429                || (near(a[0], b[0]) && near(a[1], b[1]))
1430                || (near(a[0], b[1]) && near(a[1], b[0]))
1431        };
1432        let mut removed: Vec<u64> = Vec::new();
1433        let mut index = 0;
1434        while index < result.pieces.len() {
1435            let mut other = index + 1;
1436            while other < result.pieces.len() {
1437                let coincide = ends_match(&ends[index], &ends[other]) && {
1438                    let a = &result.pieces[index];
1439                    let b = &result.pieces[other];
1440                    max_curve_deviation(&a.curve, &b.curve)? <= weld
1441                        && max_curve_deviation(&b.curve, &a.curve)? <= weld
1442                };
1443                if coincide {
1444                    let absorbed = result.pieces.remove(other);
1445                    ends.remove(other);
1446                    removed.push(absorbed.id);
1447                    let keeper = &mut result.pieces[index];
1448                    for pcurve in absorbed.pcurves {
1449                        if !keeper
1450                            .pcurves
1451                            .iter()
1452                            .any(|existing| {
1453                                existing.operand == pcurve.operand
1454                                    && existing.face_id == pcurve.face_id
1455                            })
1456                        {
1457                            keeper.pcurves.push(pcurve);
1458                        }
1459                    }
1460                    let keeper_id = keeper.id;
1461                    for record in &mut result.by_face {
1462                        if let Some(position) =
1463                            record.piece_ids.iter().position(|&id| id == absorbed.id)
1464                        {
1465                            if record.piece_ids.contains(&keeper_id) {
1466                                record.piece_ids.remove(position);
1467                            } else {
1468                                record.piece_ids[position] = keeper_id;
1469                            }
1470                        }
1471                    }
1472                } else {
1473                    other += 1;
1474                }
1475            }
1476            index += 1;
1477        }
1478        if !removed.is_empty() && std::env::var("BREP_DEBUG_BOOL").is_ok() {
1479            eprintln!("coincident-piece merge: absorbed {:?}", removed);
1480        }
1481    }
1482    // Rescue near-tangent SSI truncations BEFORE canonicalization so the added
1483    // bridge pieces' endpoints (existing crossing/stub vertices) fold into the
1484    // same junction merges as every other section.
1485    lap("coincident_merge", &mut tail_lap, &mut tail_ms);
1486    extend_truncated_sections(
1487        &mut result,
1488        &face_edge_lists,
1489        solid_a,
1490        solid_b,
1491        &charts,
1492        options.tolerance,
1493    )?;
1494    lap("extend_truncated", &mut tail_lap, &mut tail_ms);
1495    canonicalize_imprint_junctions(&mut result, solid_a, solid_b, &charts, options.tolerance)?;
1496    lap("canonicalize", &mut tail_lap, &mut tail_ms);
1497    // ONE RIM, ONE EDGE: with vertex identity final, dissolve the section
1498    // vertices that no operand edge passes through — the marcher's own
1499    // parameterization origin, which `process_curve` cannot tell from a
1500    // junction because it sees one pair at a time. See the method doc.
1501    let dissolved = dissolve_section_origin_vertices(
1502        &mut result,
1503        &marched_pieces,
1504        solid_a,
1505        solid_b,
1506        &charts,
1507        options.tolerance,
1508    )?;
1509    if dissolved > 0 && std::env::var("BREP_DEBUG_BOOL").is_ok() {
1510        eprintln!("origin-dissolve: {dissolved} parameterization vertex/vertices removed");
1511    }
1512    lap("origin_dissolve", &mut tail_lap, &mut tail_ms);
1513    // B2: reuse an existing boundary edge as the shared section edge wherever a
1514    // section coincides with one along its whole span (vertices are final after
1515    // canonicalization; `face_edge_lists` holds each face's boundary edges on
1516    // the healed operands). Runs here so both operands reference ONE edge.
1517    reuse_boundary_section_edges(
1518        &mut result,
1519        &face_edge_lists,
1520        solid_a,
1521        solid_b,
1522        options.tolerance,
1523    )?;
1524    // Capstone step 1 — instrumentation only, zero behavior change: report
1525    // every (section piece × boundary edge) contact where the piece runs
1526    // within the scale-derived band of the edge over a real span. Measuring
1527    // the bands here first validates the graze-contact model on the
1528    // acceptance suite before step 2's common-block machinery replaces a
1529    // grazed overlap with a shared edge.
1530    lap("reuse_boundary", &mut tail_lap, &mut tail_ms);
1531    report_graze_contacts(&result, &face_edge_lists, solid_a, solid_b, options.tolerance)?;
1532    if profile.enabled {
1533        let line = tail_ms
1534            .iter()
1535            .map(|(name, ms)| format!("{name}={ms:.2}"))
1536            .collect::<Vec<_>>()
1537            .join(" ");
1538        eprintln!("imprint.stages {line}");
1539    }
1540    Ok(result)
1541}
1542
1543/// Debug-only graze-contact survey (`BREP_DEBUG_GRAZE=1`): for each section
1544/// piece and each boundary edge of its support faces, sample the piece and
1545/// measure distance to the edge; report contacts whose in-band span exceeds
1546/// both the weld scale and 4× the minimum deviation (span-wise proximity, not
1547/// a point touch). `band_cap` reuses the residual-merge `sep_cap` ceiling —
1548/// measured, never grown. The output is the raw material for capstone step 2
1549/// (partial-span common-block): which contacts exist, their spans, and their
1550/// measured bands.
1551fn report_graze_contacts(
1552    result: &ImprintResultRecord,
1553    face_edge_lists: &HashMap<FaceKey, Vec<&EdgeRecord>>,
1554    solid_a: &BrepSolid,
1555    solid_b: &BrepSolid,
1556    tolerance: f64,
1557) -> Result<(), KernelRefusal> {
1558    if std::env::var("BREP_DEBUG_GRAZE").as_deref() != Ok("1") {
1559        return Ok(());
1560    }
1561    let raw_extent = raw_solid_extent(solid_a).max(raw_solid_extent(solid_b));
1562    let (_, band_cap) = residual_merge_bands(raw_extent, tolerance);
1563    const SAMPLES: usize = 17;
1564    for piece in &result.pieces {
1565        let [t0, t1] = [piece.t0, piece.t1];
1566        if !(t1 > t0) {
1567            continue;
1568        }
1569        for key in piece.support_faces {
1570            let Some(edges) = face_edge_lists.get(&key) else {
1571                continue;
1572            };
1573            for edge in edges {
1574                if edge.degenerate {
1575                    continue;
1576                }
1577                let mut in_band = 0usize;
1578                let mut min_dev = f64::INFINITY;
1579                let mut max_dev_in_band = 0.0f64;
1580                let mut span = 0.0f64;
1581                let mut prev: Option<(bool, Vec3)> = None;
1582                for k in 0..SAMPLES {
1583                    let t = t0 + (t1 - t0) * k as f64 / (SAMPLES - 1) as f64;
1584                    let point = piece.curve.evaluate(t).or_refuse(KernelStage::Intersect, "evaluate")?;
1585                    let deviation = project_point_to_curve(&edge.curve, point).or_refuse(KernelStage::Intersect, "project_point_to_curve")?.distance;
1586                    min_dev = min_dev.min(deviation);
1587                    let inside = deviation <= band_cap;
1588                    if inside {
1589                        in_band += 1;
1590                        max_dev_in_band = max_dev_in_band.max(deviation);
1591                        if let Some((true, prev_point)) = prev {
1592                            span += point.sub(prev_point).length();
1593                        }
1594                    }
1595                    prev = Some((inside, point));
1596                }
1597                // Span-wise contact: several consecutive samples in band and a
1598                // span that dwarfs the closest-approach (not a transversal
1599                // crossing, which dips in and out at one sample).
1600                if in_band >= 3 && span > (4.0 * min_dev).max(assembler_weld(tolerance)) {
1601                    eprintln!(
1602                        "graze: piece {} sup=[{}:{},{}:{}] ~ edge {}:{} span={:.3e} band=[{:.3e},{:.3e}] samples_in_band={}/{}",
1603                        piece.id,
1604                        piece.support_faces[0].operand,
1605                        piece.support_faces[0].face_id,
1606                        piece.support_faces[1].operand,
1607                        piece.support_faces[1].face_id,
1608                        key.operand,
1609                        edge.id,
1610                        span,
1611                        min_dev,
1612                        max_dev_in_band,
1613                        in_band,
1614                        SAMPLES
1615                    );
1616                }
1617            }
1618        }
1619    }
1620    Ok(())
1621}
1622
1623/// Are the two carriers TANGENT (normals parallel) where they both pass through
1624/// `point`?
1625///
1626/// The test the tangential-only refusal above needs: a point on the marched
1627/// intersection is a TANGENT NODE when the two surface normals there are
1628/// parallel. The threshold is the transversality bound the supplemental
1629/// detector already accepts seeds by (`TRANSVERSE_SEED_CROSS`), not the
1630/// far tighter pair-classifier bound — a node the march merely passes CLOSE to
1631/// still poisons the assembly, so this errs toward calling a pair singular.
1632fn pair_normals_parallel_at(
1633    first: TaggedFace<'_>,
1634    second: TaggedFace<'_>,
1635    point: Vec3,
1636) -> Result<bool, KernelRefusal> {
1637    let mut normals = [Vec3::default(); 2];
1638    for (slot, face) in normals.iter_mut().zip([first, second]) {
1639        let projection = project_point_to_surface(&face.face.surface, point)
1640            .or_refuse(KernelStage::Intersect, "project_point_to_surface")?;
1641        let Ok(normal) = face.face.surface.normal(projection.u, projection.v) else {
1642            // A pole/singular parameter point cannot witness transversality;
1643            // treat it as tangential so the pair stays refused.
1644            return Ok(true);
1645        };
1646        *slot = normal;
1647    }
1648    Ok(normals[0].cross(normals[1]).length() <= crate::TRANSVERSE_SEED_CROSS)
1649}