Skip to main content

brep_kernel/csg/imprint/
driver.rs

1use crate::{KernelRefusal, KernelStage, OrRefuse, RefusalClass};
2use super::*;
3use super::gate_census::{census_pair, fit_census_enabled};
4
5pub fn build_imprints(
6    solid_a: &BrepSolid,
7    solid_b: &BrepSolid,
8    options: &ImprintOptions,
9) -> Result<ImprintResultRecord, KernelRefusal> {
10    let mut section_evidence = false;
11    let edges = edge_map(solid_a, solid_b);
12    let mut builder = ImprintBuilder {
13        edges,
14        tolerance: options.tolerance,
15        barrier_edges: HashSet::default(),
16        overlap_ridden_edges: HashSet::default(),
17        scale: solid_scale(solid_a).max(solid_scale(solid_b)),
18        vertices: Vec::new(),
19        vertex_radii: HashMap::default(),
20        pieces: Vec::new(),
21        by_face: HashMap::default(),
22        edge_splits: HashMap::default(),
23        marched_pieces: HashSet::default(),
24        next_id: 1,
25    };
26    let mut first_faces = faces(solid_a, 0);
27    let mut second_faces = faces(solid_b, 1);
28    // Per-face trim-window carriers, computed once: tighter BVH bounds, and
29    // the seed/march stages walk the window instead of the full carrier
30    // (identical geometry and parameterization inside the window).
31    //
32    // A window LIFTED across a closed direction's seam (`restricted_carrier`)
33    // is the exception to "same parameterization": it is the trim's frame, a
34    // period off the carrier's on one side. Its face carries it as its
35    // `chart`, and every uv the imprint reads for that face — the clip's
36    // containment, the sections' pcurves, a seed's normal — is read there.
37    let first_restricted: Vec<Option<MarchWindow>> = first_faces
38        .iter()
39        .map(|tagged| restricted_carrier(tagged.face))
40        .collect();
41    let second_restricted: Vec<Option<MarchWindow>> = second_faces
42        .iter()
43        .map(|tagged| restricted_carrier(tagged.face))
44        .collect();
45    for (tagged, window) in first_faces.iter_mut().zip(&first_restricted) {
46        tagged.lifted = window.as_ref().filter(|window| window.lifted).map(|window| &window.surface);
47    }
48    for (tagged, window) in second_faces.iter_mut().zip(&second_restricted) {
49        tagged.lifted = window.as_ref().filter(|window| window.lifted).map(|window| &window.surface);
50    }
51    let first_faces = first_faces;
52    let second_faces = second_faces;
53    // The lifted charts, by key, for the post-passes that rebuild a pcurve
54    // after the mint (junction canonicalization, the origin dissolve, the
55    // truncation bridge). A face absent here is read on its own carrier.
56    let charts: FaceCharts<'_> = first_faces
57        .iter()
58        .chain(second_faces.iter())
59        .filter_map(|tagged| tagged.lifted.map(|surface| (tagged.key(), surface)))
60        .collect();
61    let first_bounds = first_faces
62        .iter()
63        .zip(&first_restricted)
64        .map(|(tagged, restricted)| {
65            face_bounds(
66                tagged.face,
67                restricted.as_ref().map(|window| &window.surface),
68                &builder.edges,
69                tagged.operand,
70                options.tolerance,
71            )
72        })
73        .collect::<Result<Vec<_>, _>>()?;
74    let second_bounds = second_faces
75        .iter()
76        .zip(&second_restricted)
77        .map(|(tagged, restricted)| {
78            face_bounds(
79                tagged.face,
80                restricted.as_ref().map(|window| &window.surface),
81                &builder.edges,
82                tagged.operand,
83                options.tolerance,
84            )
85        })
86        .collect::<Result<Vec<_>, _>>()?;
87    let second_bvh = Bvh::build(&second_bounds);
88    // Per-face classifier data and edge lists/subcurves, computed once and
89    // reused across every pair the face participates in.
90    let first_classify = first_faces
91        .iter()
92        .map(|tagged| SurfaceClassifyData::build(&tagged.face.surface, options.tolerance))
93        .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
94    let second_classify = second_faces
95        .iter()
96        .map(|tagged| SurfaceClassifyData::build(&tagged.face.surface, options.tolerance))
97        .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
98    let mut face_edge_lists: HashMap<FaceKey, Vec<&EdgeRecord>> = HashMap::default();
99    for tagged in first_faces.iter().chain(second_faces.iter()) {
100        face_edge_lists.insert(tagged.key(), face_edges(*tagged, &builder.edges)?);
101    }
102    let mut subcurves: HashMap<(u8, u64), NurbsCurve> = HashMap::default();
103    for (&key, edge) in &builder.edges {
104        if !edge.degenerate {
105            // Failures fall through: the use sites recompute and surface the
106            // original error exactly where the uncached code did.
107            if let Ok(curve) = edge_subcurve(edge) {
108                subcurves.insert(key, curve);
109            }
110        }
111    }
112    let cached_subcurve = |operand: u8, edge: &EdgeRecord| -> Result<NurbsCurve, KernelRefusal> {
113        match subcurves.get(&(operand, edge.id)) {
114            Some(curve) => Ok(curve.clone()),
115            None => edge_subcurve(edge),
116        }
117    };
118    let mut profile = ImprintProfile::new();
119    let mut tangent_nodes: Vec<Vec3> = Vec::new();
120    let mut cosurface_pairs: Vec<(FaceKey, FaceKey)> = Vec::new();
121    let mut paired = Vec::new();
122    for (first_index, first) in first_faces.iter().enumerate() {
123        let first = *first;
124        paired.clear();
125        second_bvh.overlapping(
126            first_bounds[first_index],
127            options.tolerance * 100.0,
128            &mut paired,
129        );
130        paired.sort_unstable();
131        let debug_pairs = std::env::var("BREP_DEBUG_PAIRS").is_ok();
132        if debug_pairs {
133            eprintln!(
134                "bvh first_face={} -> {} candidate pairs {:?}",
135                first.face.id,
136                paired.len(),
137                paired
138                    .iter()
139                    .map(|&index| second_faces[index].face.id)
140                    .collect::<Vec<_>>()
141            );
142        }
143        for &second_index in &paired {
144            let second = second_faces[second_index];
145            profile.pairs += 1;
146            // Set when this pair's section passes through an isolated TANGENT
147            // NODE inside both trims (see the classification below). The march
148            // is attempted anyway — that is the whole point — but a marcher
149            // that cannot get through a node must refuse in the tangent-node
150            // class naming the node, not leak its own step-budget message.
151            let mut tangent_node: Option<Vec3> = None;
152            let mut lap_start = None;
153            profile.lap(&mut lap_start);
154            let pair_classification = classify_surface_pair_cached(
155                &first.face.surface,
156                &first_classify[first_index],
157                &second.face.surface,
158                &second_classify[second_index],
159                options.tolerance,
160                PAIR_ANGULAR_TOLERANCE,
161            ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
162            profile.classify += profile.lap(&mut lap_start);
163            if pair_classification.relation == SurfacePairRelation::Disjoint {
164                if debug_pairs {
165                    eprintln!(
166                        "pair {}x{}: DISJOINT-cull sep={:.4e}",
167                        first.face.id, second.face.id, pair_classification.minimum_separation
168                    );
169                }
170                continue;
171            }
172            // Coincident carriers are never marched: their true intersection
173            // is a 2D region, not a curve, so anything the marcher traces on
174            // them is noise ("similar faces we do not intersect" — Golovanov
175            // §6.2).  The sampled classification catches coincident pairs
176            // the strict reconstruction test misses (partial overlaps,
177            // differing parameterizations); both route to the boundary-curve
178            // exchange below.
179            let is_cosurface = pair_classification.relation == SurfacePairRelation::Cosurface
180                || cosurface_pair(&first.face.surface, &second.face.surface, options.tolerance)?;
181            profile.cosurface += profile.lap(&mut lap_start);
182            if is_cosurface {
183                if debug_pairs {
184                    eprintln!("pair {}x{}: cosurface", first.face.id, second.face.id);
185                }
186                cosurface_pairs.push((first.key(), second.key()));
187                for edge in &face_edge_lists[&second.key()] {
188                    if !edge.degenerate {
189                        builder.process_curve(
190                            cached_subcurve(second.operand, edge)?,
191                            first,
192                            second,
193                            &[first],
194                            &[first],
195                            false,
196                        )?;
197                    }
198                }
199                for edge in &face_edge_lists[&first.key()] {
200                    if !edge.degenerate {
201                        builder.process_curve(
202                            cached_subcurve(first.operand, edge)?,
203                            first,
204                            second,
205                            &[second],
206                            &[second],
207                            false,
208                        )?;
209                    }
210                }
211                profile.process_curve += profile.lap(&mut lap_start);
212                continue;
213            }
214
215            // The exact section lanes below (planar-iso, analytic) mint this
216            // pair's whole intersection, the part over either trim included, so
217            // a SPAN of an edge lying on the other face would be a second copy
218            // of a curve they already mint — which reorders the face's pieces
219            // and with them its fragments' names (the 2026-09-10 chamfer groove
220            // rim's `Box_PX` / `Box_PX_1`). Where they answer, only a whole
221            // edge is exchanged, as before.
222            let planar_iso = planar_iso_intersection(
223                &first.face.surface,
224                &second.face.surface,
225                options.tolerance,
226            )?;
227            profile.planar_iso += profile.lap(&mut lap_start);
228            let analytic = crate::intersect_analytic_pair(
229                &first.face.surface,
230                &second.face.surface,
231                options.tolerance,
232            );
233            profile.analytic += profile.lap(&mut lap_start);
234            let exact_section = planar_iso.is_some() || analytic.is_some();
235
236            for edge in &face_edge_lists[&first.key()] {
237                if !edge.degenerate {
238                    let curve = cached_subcurve(first.operand, edge)?;
239                    let spans = curve_spans_on_face(curve, second.face, &face_edge_lists[&second.key()], options.tolerance, exact_section)?;
240                    for span in spans {
241                        builder.process_curve(span, first, second, &[second], &[second], false)?;
242                    }
243                }
244            }
245            for edge in &face_edge_lists[&second.key()] {
246                if !edge.degenerate {
247                    let curve = cached_subcurve(second.operand, edge)?;
248                    let spans = curve_spans_on_face(curve, first.face, &face_edge_lists[&first.key()], options.tolerance, exact_section)?;
249                    for span in spans {
250                        builder.process_curve(span, first, second, &[first], &[first], false)?;
251                    }
252                }
253            }
254            profile.lies_on += profile.lap(&mut lap_start);
255            // CENSUS ONLY (`BREP_FIT_CENSUS=1`): the lane this pair takes under
256            // the count-keyed and the geometry-keyed gates, and — when it is
257            // marched here and answered exactly there — the point-set comparison
258            // of the two, printed when `pair_census` is dropped. `imprint/gate_census.rs`.
259            let mut pair_census = if fit_census_enabled() {
260                census_pair(first, second, options.tolerance)?
261            } else {
262                None
263            };
264            if let Some(curve) = planar_iso {
265                if debug_pairs {
266                    eprintln!("pair {}x{}: planar_iso", first.face.id, second.face.id);
267                }
268                builder.process_curve(
269                    curve,
270                    first,
271                    second,
272                    &[first, second],
273                    &[first, second],
274                    true,
275                )?;
276                profile.process_curve += profile.lap(&mut lap_start);
277                continue;
278            }
279
280            // Recognized analytic pairs produce their exact intersection
281            // curves (lines, circles, ellipses) directly — no marching, no
282            // polyline fitting, no chord-sag drift. An empty result is a
283            // proof of non-intersection and also skips the marcher. (Read
284            // above, with the planar-iso curve.)
285            if let Some(curves) = analytic {
286                if debug_pairs {
287                    eprintln!(
288                        "pair {}x{}: analytic x{}",
289                        first.face.id,
290                        second.face.id,
291                        curves.len()
292                    );
293                }
294                for curve in curves {
295                    builder.process_curve(
296                        curve,
297                        first,
298                        second,
299                        &[first, second],
300                        &[first, second],
301                        true,
302                    )?;
303                }
304                profile.process_curve += profile.lap(&mut lap_start);
305                continue;
306            }
307
308            // A pair whose every touching sample is TANGENTIAL cannot
309            // contain a transverse intersection curve: the marcher would
310            // walk the tangency band's noise, which neither closes nor
311            // reaches a boundary (the distance-0 glue-extrude runaway).
312            // Shared topology at a tangential contact comes from the
313            // boundary-curve exchange above ("similar faces we do not
314            // intersect" extended to tangential contacts — Golovanov §6.2;
315            // where surfaces CROSS through a tangency, off-line samples
316            // have non-parallel normals and the pair still marches).
317            if pair_classification.relation == SurfacePairRelation::NearTangent
318                && pair_classification.tangential_only
319            {
320                // The coarse 5×5 classifier can flag `tangential_only` off an
321                // incidental tangential KISS between two curved carriers and
322                // miss the transverse loop where they actually cross (two
323                // overlapping tori: their tubes cross while their inner walls
324                // just touch). Before honouring the skip, a GATED supplemental
325                // detector (denser two-sided seeding, tangency-band seeds
326                // rejected, trace-exhaustion swallowed, transverse-only
327                // branches) checks whether a genuine transverse curve of
328                // meaningful length lies inside BOTH trims.
329                //
330                // Such a pair's intersection is SINGULAR where a tangency
331                // sits on it, and what the imprint can do about that depends
332                // entirely on the SHAPE of the tangency — see
333                // `imprint/tangent_contact.rs`. An isolated NODE (two branches
334                // crossing, as every equal-radius pair produces) is already
335                // assembled correctly by the ordinary march plus the 2D
336                // arrangement's pinch carving; an EXTENDED contact along a
337                // whole curve has no section to imprint at all and tears the
338                // shell. Both are classified below and only the second is
339                // refused. Skipping either silently would DROP the
340                // intersection (torus∪torus double-counts; torus−torus removes
341                // nothing — the two structural bugs the semantic oracle
342                // found), so nothing here ever falls through quietly.
343                let first_march = first_restricted[first_index]
344                    .as_ref()
345                    .map(|window| &window.surface)
346                    .unwrap_or(&first.face.surface);
347                let second_march = second_restricted[second_index]
348                    .as_ref()
349                    .map(|window| &window.surface)
350                    .unwrap_or(&second.face.surface);
351                let supplemental = intersect_surfaces_supplemental(
352                    first_march,
353                    second_march,
354                    &SurfaceIntersectionOptions {
355                        tolerance: options.tolerance,
356                        maximum_step: options.maximum_ssi_step,
357                        ..Default::default()
358                    },
359                ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
360                let mut dropped_length = 0.0f64;
361                let mut clipped: Vec<Vec<Vec3>> = Vec::new();
362                for branch in &supplemental {
363                    for run in clip_branch_to_trims(&branch.points, first, second)? {
364                        if run.len() < 2 {
365                            continue;
366                        }
367                        let length: f64 = run
368                            .windows(2)
369                            .map(|pair| pair[1].sub(pair[0]).length())
370                            .sum();
371                        dropped_length = dropped_length.max(length);
372                        clipped.push(run);
373                    }
374                }
375                if dropped_length <= options.tolerance * 100.0 {
376                    if debug_pairs {
377                        eprintln!(
378                            "pair {}x{}: tangential-only contact, march skipped",
379                            first.face.id, second.face.id
380                        );
381                    }
382                    continue;
383                }
384                // A transverse curve EXISTS. What would make it unimprintable is
385                // a tangent NODE on it — and the node need not lie inside the
386                // trims. Two equal-radius pipe arms are tangent to each other
387                // where their axes' common perpendicular leaves the junction,
388                // and a joint ball wider than the arms trims that crotch off
389                // both faces; what is left inside the trims is an ordinary
390                // crossing. So the question is not "does a transverse curve
391                // exist" but "does the curve INSIDE BOTH TRIMS reach a
392                // tangency". Only the latter is the checkerboard case.
393                let mut tangency_inside_trims = false;
394                let mut tangency_witness = Vec3::default();
395                'clipped: for run in &clipped {
396                    for (index, &point) in run.iter().enumerate() {
397                        if pair_normals_parallel_at(first, second, point)? {
398                            tangency_inside_trims = true;
399                            tangency_witness = point;
400                            if debug_pairs {
401                                eprintln!(
402                                    "pair {}x{}: TANGENCY at run index {}/{} ({:.6},{:.6},{:.6}) dropped_len={:.4e}",
403                                    first.face.id, second.face.id, index, run.len(),
404                                    point.x, point.y, point.z, dropped_length
405                                );
406                            }
407                            break 'clipped;
408                        }
409                    }
410                }
411                if tangency_inside_trims {
412                    // WHAT SHAPE is the tangency? The witness is only a marched
413                    // sample within the transverse-seed angular gate, so it is
414                    // REFINED onto the contact before being classified — at the
415                    // raw witness a G1 cylinder/torus join and a genuine
416                    // equal-radius node are eighteen-fold apart, which would be
417                    // a band; at the refined contact they are thirty orders
418                    // apart, which is a rank question. See
419                    // `imprint/tangent_contact.rs`.
420                    let contact = classify_tangent_contact(
421                        &first.face.surface,
422                        &second.face.surface,
423                        tangency_witness,
424                        options.tolerance,
425                    )?;
426                    if debug_pairs {
427                        eprintln!(
428                            "pair {}x{}: contact classification {:?}",
429                            first.face.id, second.face.id, contact
430                        );
431                    }
432                    match contact {
433                        // An isolated node: the section is a curve everywhere
434                        // but that one point, so the ordinary march below runs
435                        // and the 2D arrangement carves the pinch on both
436                        // faces. `tangent_node` records it so a march that
437                        // cannot get through still refuses in THIS class rather
438                        // than leaking the marcher's own message.
439                        Some(contact) if contact.is_isolated_node() => {
440                            tangent_node = Some(contact.point);
441                            tangent_nodes.push(contact.point);
442                        }
443                        // An extended contact (rank-deficient) or a witness we
444                        // could not refine onto any contact at all (None — so
445                        // nothing is proven and the pre-classification refusal
446                        // stands). Neither has a section curve the imprint can
447                        // represent.
448                        other => {
449                            let shape = match other {
450                                Some(contact) => contact.describe(),
451                                None => format!(
452                                    "the tangency near ({:.6},{:.6},{:.6}) could not be refined \
453                                     onto a contact, so its shape is unproven",
454                                    tangency_witness.x, tangency_witness.y, tangency_witness.z
455                                ),
456                            };
457                            return Err(KernelRefusal::new(
458                                RefusalClass::TangentNodeSingularity,
459                                KernelStage::Intersect,
460                                format!(
461                                    "boolean: unsupported singular/tangent-node surface intersection \
462                                     between faces {} and {}: {shape}",
463                                    first.face.id, second.face.id
464                                ),
465                            ));
466                        }
467                    }
468                }
469                if debug_pairs && tangent_node.is_none() {
470                    // Transverse the whole way inside both trims: the coarse
471                    // 5x5 classifier only saw the tangency the trims cut away.
472                    eprintln!(
473                        "pair {}x{}: classified tangential-only, but the curve inside both \
474                         trims is transverse ({dropped_length:.4}) — marching",
475                        first.face.id, second.face.id
476                    );
477                }
478            }
479
480            // The trim-window carriers: same surface and parameterization
481            // over the window, so hit (u, v) values remain valid on the
482            // originals; out-of-window intersections could never survive
483            // clip_branch_to_trims and are not walked at all.
484            let first_march = first_restricted[first_index]
485                .as_ref()
486                .map(|window| &window.surface)
487                .unwrap_or(&first.face.surface);
488            let second_march = second_restricted[second_index]
489                .as_ref()
490                .map(|window| &window.surface)
491                .unwrap_or(&second.face.surface);
492            let mut seed_points = Vec::new();
493            // Smallest |edge_tangent · surface_normal| over accepted seeds — the
494            // local grazing measure at the trim crossings (near 0 = the edge
495            // pierces the other surface tangentially). Gates the near-tangent
496            // clip-order rescue below to genuinely grazing pairs.
497            let mut min_seed_tangency = f64::INFINITY;
498            for (face, other_march, other) in
499                [(first, second_march, second), (second, first_march, first)]
500            {
501                for edge in &face_edge_lists[&face.key()] {
502                    if edge.degenerate {
503                        continue;
504                    }
505                    for hit in intersect_curve_surface(&edge.curve, other_march, options.tolerance).or_refuse(KernelStage::Intersect, "intersect_curve_surface")?
506                    {
507                        if hit.t < edge.t0 - 1e-9 || hit.t > edge.t1 + 1e-9 {
508                            continue;
509                        }
510                        let derivative = edge.curve.derivatives(hit.t, 1).or_refuse(KernelStage::Intersect, "derivatives")?[1];
511                        // A STATIONARY POINT on the edge curve is not a reason
512                        // to refuse the boolean. An involute flank starts at
513                        // the base circle with an exactly zero tangent, so a
514                        // gear tooth's profile carries one cusp per flank, and
515                        // a hit landing on that parameter used to propagate
516                        // "Vec3.normalized: zero-length vector" out of the
517                        // whole union (the 2026-09-14 herringbone report: the
518                        // second tooth band's z=0 cap edge meets the first
519                        // band's flank exactly at the cusp). The direction is
520                        // still defined as the one-sided limit, so read it from
521                        // just inside the edge's own range; an edge with no
522                        // direction at all simply contributes no seed, exactly
523                        // as a hit whose face normal is unavailable already
524                        // does two lines below. Seeds are marcher hints, so a
525                        // dropped one costs a hint, never an answer.
526                        // Escape hatch for tamper-verification:
527                        // BREP_CUSP_TANGENT_RESCUE=0 restores the refusal.
528                        let tangent = match derivative.normalized() {
529                            Ok(tangent) => tangent,
530                            Err(error) => {
531                                if std::env::var("BREP_CUSP_TANGENT_RESCUE").as_deref() == Ok("0") {
532                                    return Err(error)
533                                        .or_refuse(KernelStage::Intersect, "normalized");
534                                }
535                                match edge.curve.stationary_tangent_rescue(hit.t, edge.t0, edge.t1)
536                                {
537                                    Some(tangent) => tangent,
538                                    None => continue,
539                                }
540                            }
541                        };
542                        // `hit` was solved on the other face's MARCH WINDOW, so
543                        // its uv is in that face's chart — a period off the
544                        // carrier's where the window is lifted, and `normal`
545                        // clamps a parameter past its domain.
546                        let normal = match other.chart().normal(hit.u, hit.v) {
547                            Ok(normal) => normal,
548                            Err(_) => continue,
549                        };
550                        if debug_pairs {
551                            eprintln!(
552                                "pair {}x{}: seed edge {} t={:.6} p=({:.5},{:.5},{:.5}) |tan.n|={:.4} {}",
553                                first.face.id,
554                                second.face.id,
555                                edge.id,
556                                hit.t,
557                                hit.point.x,
558                                hit.point.y,
559                                hit.point.z,
560                                tangent.dot(normal).abs(),
561                                if tangent.dot(normal).abs() >= 0.1 { "ACCEPT" } else { "reject" }
562                            );
563                        }
564                        if tangent.dot(normal).abs() >= 0.1 {
565                            seed_points.push(hit.point);
566                            min_seed_tangency = min_seed_tangency.min(tangent.dot(normal).abs());
567                            section_evidence = true;
568                        }
569                    }
570                }
571            }
572            profile.seeds += profile.lap(&mut lap_start);
573            profile.marched_pairs += 1;
574            if debug_pairs {
575                eprintln!(
576                    "pair {}x{}: marching (relation {:?})...",
577                    first.face.id, second.face.id, pair_classification.relation
578                );
579            }
580            let marched = intersect_surfaces(
581                first_march,
582                second_march,
583                &SurfaceIntersectionOptions {
584                    tolerance: options.tolerance,
585                    maximum_step: march_maximum_step(
586                        &pair_classification,
587                        options.maximum_ssi_step,
588                        builder.scale,
589                    ),
590                    seed_points: seed_points.clone(),
591                    ..Default::default()
592                },
593            );
594            // A pair carrying a tangent node keeps its OWN refusal class when
595            // the march fails: the node is why the trace cannot close, and the
596            // equal-radius torus pair depends on being told so rather than on
597            // reading a step-budget message it cannot act on.
598            let marched = match (marched, tangent_node) {
599                (Err(error), Some(node)) => {
600                    return Err(KernelRefusal::new(
601                        RefusalClass::TangentNodeSingularity,
602                        KernelStage::Intersect,
603                        format!(
604                            "boolean: unsupported singular/tangent-node surface intersection \
605                             between faces {} and {}: the section through the tangent node at \
606                             ({:.6},{:.6},{:.6}) could not be marched ({error})",
607                            first.face.id, second.face.id, node.x, node.y, node.z
608                        ),
609                    ));
610                }
611                (marched, _) => marched,
612            };
613            let mut branches = marched
614            .map_err(|error| {
615                format!(
616                    "{error} (marching faces {} and {})",
617                    first.face.id, second.face.id
618                )
619            }).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
620            profile.march += profile.lap(&mut lap_start);
621            let rescue_started = profile.enabled.then(Instant::now);
622            // MARCH-ORDER SWAP RESCUE (hatch BREP_MARCH_SWAP_RESCUE=0).
623            // `intersect_surfaces` is not order-symmetric: the coupled Newton
624            // trace can fail to start/continue from a valid seed when the two
625            // surfaces are presented in one operand order yet succeed in the
626            // other. This is the sole reason t217's `subtract(sphere, step)`
627            // fails while every other op passes — pair 118x842 (sphere-first)
628            // marches ZERO branches, while 842x118 (step-first, the working
629            // a\b order) marches the section from the IDENTICAL accepted pierce
630            // seeds. When the forward order returns no branch AND an accepted
631            // transverse pierce seed exists (so a real section provably crosses
632            // both trims), retry the march with the surfaces swapped — i.e.
633            // reproduce the exact call the working operand order makes for this
634            // pair (measured: seed_only alone does NOT recover it — the section
635            // is found from an auto-grid start, not from the near-tangent pierce
636            // seeds, which the seed normal-cross gate rejects). The returned
637            // branch points are 3D and therefore order-independent, so no
638            // parameter remap is needed; they flow through the identical clip /
639            // process_curve gates below, which drop any out-of-trim or
640            // sub-length run exactly as today. The rescue only ever runs when
641            // the forward order found NOTHING, so it cannot alter a pair that
642            // already marched.
643            if branches.is_empty()
644                && !seed_points.is_empty()
645                && pair_classification.relation == SurfacePairRelation::Candidate
646                && std::env::var("BREP_MARCH_SWAP_RESCUE").as_deref() != Ok("0")
647            {
648                // FAIL-SOFT: the forward order already returned Ok(empty); a
649                // swapped-march error (trace-exhaustion is a real error class —
650                // `intersect_surfaces_supplemental` swallows it for exactly this
651                // reason) must NOT convert that graceful empty into a hard error.
652                // On Err, keep the (empty) forward result and carry on.
653                let swapped = intersect_surfaces(
654                    second_march,
655                    first_march,
656                    &SurfaceIntersectionOptions {
657                        tolerance: options.tolerance,
658                        maximum_step: march_maximum_step(
659                            &pair_classification,
660                            options.maximum_ssi_step,
661                            builder.scale,
662                        ),
663                        seed_points: seed_points.clone(),
664                        ..Default::default()
665                    },
666                )
667                .unwrap_or_default();
668                if debug_pairs {
669                    eprintln!(
670                        "pair {}x{}: MARCH-SWAP RESCUE attempt -> {} branches, pts {:?}",
671                        first.face.id,
672                        second.face.id,
673                        swapped.len(),
674                        swapped.iter().map(|b| b.points.len()).collect::<Vec<_>>()
675                    );
676                }
677                if !swapped.is_empty() {
678                    branches = swapped;
679                }
680            }
681            // NEAR-TANGENT CLIP-ORDER RESCUE (hatch BREP_MARCH_SWAP_CLIP_RESCUE=0).
682            // Companion to the empty-branch MARCH-SWAP RESCUE above:
683            // `intersect_surfaces` is order-asymmetric not only in WHETHER it
684            // marches, but in the exact sample positions of the section
685            // polyline. On a NEAR-TANGENT graze, `clip_branch_to_trims`
686            // classifies those samples against the mutual trims, and a sample
687            // landing just past the near-tangent boundary is dropped as a
688            // false-Outside that TRUNCATES the clipped section. The two operand
689            // orders drop DIFFERENT near-tangent tail samples, so one order
690            // yields a section ~0.1-0.2mm shorter at ONE endpoint (t660: b\a's
691            // step-first order clips pairs 223x105/399x105 ~0.22/0.14mm short of
692            // the section a\b's cyl-first order keeps, stranding edge 173
693            // one-use). Near-tangent clip errors are almost exclusively
694            // false-Outside (a point truly outside a trim rarely projects to an
695            // in-trim uv), so the order with the LONGER clipped section suffered
696            // fewer drops and is the more complete one. When the swapped order
697            // marches the SAME branch structure with a meaningfully longer
698            // clipped total, adopt its branches (3D points, so they flow through
699            // the identical clip/process_curve gates below). Gated to Candidate
700            // pairs (the near-tangent/ambiguous class; clean Transverse
701            // crossings clip identically in both orders and never differ) with
702            // an accepted GRAZING pierce seed (min |tan·n| < 0.5 — a real
703            // section provably crosses, and does so near-tangentially, the only
704            // regime where the clip wobbles; this also bounds the extra march to
705            // grazing pairs), and requires a >0.1%-of-length margin so
706            // raw-sampling jitter (t660: 0.02%) cannot flip a clean pair.
707            // Fail-soft: any swapped-march or comparison-clip error keeps the
708            // forward result.
709            if !branches.is_empty()
710                && !seed_points.is_empty()
711                && min_seed_tangency < 0.5
712                && pair_classification.relation == SurfacePairRelation::Candidate
713                && std::env::var("BREP_MARCH_SWAP_CLIP_RESCUE").as_deref() != Ok("0")
714            {
715                let swapped = intersect_surfaces(
716                    second_march,
717                    first_march,
718                    &SurfaceIntersectionOptions {
719                        tolerance: options.tolerance,
720                        maximum_step: march_maximum_step(
721                            &pair_classification,
722                            options.maximum_ssi_step,
723                            builder.scale,
724                        ),
725                        seed_points: seed_points.clone(),
726                        ..Default::default()
727                    },
728                )
729                .unwrap_or_default();
730                // Only a swap that reproduces the SAME branch count — a
731                // refined-endpoint variant of the same section, not a different
732                // branch decomposition (guards against adopting a spurious
733                // extra branch as "longer").
734                if !swapped.is_empty() && swapped.len() == branches.len() {
735                    let totals = (|| -> Result<(f64, f64), KernelRefusal> {
736                        let mut fwd = 0.0;
737                        for b in &branches {
738                            let refined = insert_seed_points_into_branch(
739                                &b.points,
740                                &seed_points,
741                                options.tolerance,
742                            );
743                            for run in clip_branch_to_trims(&refined, first, second)? {
744                                fwd += run
745                                    .windows(2)
746                                    .map(|p| p[1].sub(p[0]).length())
747                                    .sum::<f64>();
748                            }
749                        }
750                        let mut swp = 0.0;
751                        for b in &swapped {
752                            let refined = insert_seed_points_into_branch(
753                                &b.points,
754                                &seed_points,
755                                options.tolerance,
756                            );
757                            for run in clip_branch_to_trims(&refined, first, second)? {
758                                swp += run
759                                    .windows(2)
760                                    .map(|p| p[1].sub(p[0]).length())
761                                    .sum::<f64>();
762                            }
763                        }
764                        Ok((fwd, swp))
765                    })();
766                    if let Ok((fwd_clip, swp_clip)) = totals {
767                        let margin = fwd_clip.max(swp_clip) * 1.0e-3;
768                        let adopt = swp_clip > fwd_clip + margin;
769                        if debug_pairs {
770                            eprintln!(
771                                "pair {}x{}: CLIP-SWAP RESCUE fwd_clip={:.6} swp_clip={:.6} margin={:.6}{}",
772                                first.face.id,
773                                second.face.id,
774                                fwd_clip,
775                                swp_clip,
776                                margin,
777                                if adopt { " ADOPT" } else { "" }
778                            );
779                        }
780                        if adopt {
781                            branches = swapped;
782                        }
783                    }
784                }
785            }
786            if let Some(started) = rescue_started {
787                profile.rescue_march += started.elapsed();
788            }
789            if branches.iter().any(|branch| branch.points.len() >= 2) {
790                section_evidence = true;
791            }
792            if debug_pairs {
793                eprintln!(
794                    "pair {}x{}: MARCH {} branches, pts {:?}",
795                    first.face.id,
796                    second.face.id,
797                    branches.len(),
798                    branches.iter().map(|b| b.points.len()).collect::<Vec<_>>()
799                );
800            }
801            for branch in branches {
802                // The pierce seeds are the section's exact trim-crossing
803                // points — insert them so no trim interval shorter than the
804                // march step is invisible to the point-classification clip.
805                let refined_points =
806                    insert_seed_points_into_branch(&branch.points, &seed_points, options.tolerance);
807                let clip_started = profile.enabled.then(Instant::now);
808                let runs = clip_branch_to_trims(&refined_points, first, second)?;
809                if let Some(started) = clip_started {
810                    profile.clip += started.elapsed();
811                }
812                for run in runs {
813                    if debug_pairs {
814                        eprintln!(
815                            "pair {}x{}: clip run len_pts={} length={:.4e}",
816                            first.face.id,
817                            second.face.id,
818                            run.len(),
819                            run.windows(2)
820                                .map(|pair| pair[1].sub(pair[0]).length())
821                                .sum::<f64>()
822                        );
823                    }
824                    if run.len() < 2 {
825                        continue;
826                    }
827                    let length: f64 = run
828                        .windows(2)
829                        .map(|pair| pair[1].sub(pair[0]).length())
830                        .sum();
831                    if length <= options.tolerance * 100.0 {
832                        continue;
833                    }
834                    let shared_started = profile.enabled.then(Instant::now);
835                    let follows_shared = branch_follows_shared_boundary(
836                        &run,
837                        first,
838                        second,
839                        &builder.edges,
840                        options.tolerance,
841                        builder.scale,
842                    )?;
843                    if let Some(started) = shared_started {
844                        profile.shared_boundary += started.elapsed();
845                    }
846                    if follows_shared {
847                        if debug_pairs {
848                            eprintln!(
849                                "pair {}x{}: run dropped (follows shared boundary)",
850                                first.face.id, second.face.id
851                            );
852                        }
853                        continue;
854                    }
855                    if let Some(census) = pair_census.as_mut() {
856                        census.record_stations(&run);
857                    }
858                    let pieces_before = builder.pieces.len();
859                    let chunk_points = options.fit_chunk_points.unwrap_or(run.len()).max(2);
860                    let mut start = 0;
861                    while start + 1 < run.len() {
862                        let end = (start + chunk_points - 1).min(run.len() - 1);
863                        let fit_started = profile.enabled.then(Instant::now);
864                        let fit = fit_polyline(
865                            &run[start..=end],
866                            options.tolerance.max(1e-7),
867                            options.maximum_fit_points,
868                            options.local_fit,
869                        ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
870                        if let Some(started) = fit_started {
871                            profile.fit += started.elapsed();
872                        }
873                        if let Some(census) = pair_census.as_mut() {
874                            census.record_fit(&fit);
875                        }
876                        let process_started = profile.enabled.then(Instant::now);
877                        builder.process_curve(
878                            fit.curve,
879                            first,
880                            second,
881                            &[first, second],
882                            &[first, second],
883                            true,
884                        )?;
885                        if let Some(started) = process_started {
886                            profile.process += started.elapsed();
887                        }
888                        start = end;
889                    }
890                    for piece in &builder.pieces[pieces_before..] {
891                        builder.marched_pieces.insert(piece.id);
892                    }
893                    if debug_pairs {
894                        eprintln!(
895                            "pair {}x{}: run -> {} pieces",
896                            first.face.id,
897                            second.face.id,
898                            builder.pieces.len() - pieces_before
899                        );
900                    }
901                }
902            }
903            profile.clip_and_fit += profile.lap(&mut lap_start);
904        }
905    }
906    profile.report();
907    // SELF-TOUCH SPLIT: a face whose loops touch at an edge interior (a hole
908    // tangent to a fillet setback) gets a vertex minted at the touch on BOTH
909    // edges, so the fragment arrangement's pinch resolution assembles — see
910    // `imprint/self_touch.rs`. Escape hatch: BREP_SELF_TOUCH_SPLIT=0.
911    let mut tail_lap: Option<Instant> = None;
912    let mut tail_ms = Vec::<(&str, f64)>::new();
913    let mut lap = |name: &'static str, started: &mut Option<Instant>, tail_ms: &mut Vec<(&str, f64)>| {
914        if profile.enabled {
915            let now = Instant::now();
916            if let Some(previous) = *started {
917                tail_ms.push((name, (now - previous).as_secs_f64() * 1_000.0));
918            }
919            *started = Some(now);
920        }
921    };
922    lap("pairs", &mut tail_lap, &mut tail_ms);
923    if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() != Ok("0") {
924        builder.split_self_touching_loops(&first_faces, &face_edge_lists, &cached_subcurve)?;
925        builder.split_self_touching_loops(&second_faces, &face_edge_lists, &cached_subcurve)?;
926    }
927    // PIECE-ENDPOINT EXCHANGE post-pass: with every pair's process_curve
928    // done, the piece set is final for the ridden-edge class — imprint each
929    // open riding piece's junction endpoints onto the ridden edges (see the
930    // method doc; hatch BREP_OVERLAP_PIECE_ENDPOINT_SPLIT=0).
931    builder.exchange_piece_endpoint_junctions()?;
932    lap("self_touch", &mut tail_lap, &mut tail_ms);
933    let mut edge_splits = builder
934        .edge_splits
935        .into_iter()
936        .map(|((operand, edge_id), mut parameters)| {
937            parameters.sort_by(f64::total_cmp);
938            EdgeSplitRecord {
939                operand,
940                edge_id,
941                parameters,
942            }
943        })
944        .collect::<Vec<_>>();
945    edge_splits.sort_by_key(|record| (record.operand, record.edge_id));
946    let mut by_face = builder
947        .by_face
948        .into_iter()
949        .map(|(face, piece_ids)| FaceImprints {
950            operand: face.operand,
951            face_id: face.face_id,
952            piece_ids,
953        })
954        .collect::<Vec<_>>();
955    by_face.sort_by_key(|record| (record.operand, record.face_id));
956    let section_evidence = section_evidence || !builder.pieces.is_empty();
957    let marched_pieces = std::mem::take(&mut builder.marched_pieces);
958    let mut result = ImprintResultRecord {
959        tangent_nodes,
960        vertices: builder.vertices,
961        pieces: builder.pieces,
962        by_face,
963        edge_splits,
964        barrier_edges: builder.barrier_edges.into_iter().collect(),
965        section_evidence,
966        cosurface_pairs,
967    };
968    // COINCIDENT-PIECE MERGE (problemInbox equator-tangent, and the generic
969    // one-circle-from-many-pairs class): the SAME section curve can be minted
970    // by several pairs — a cosurface boundary-edge copy (the cylinder cap
971    // ring lying ON the inscribed sphere) AND the cap-plane's analytic
972    // section ring are one circle minted twice, each carrying only its own
973    // pair's supports/pcurves. Assembly then builds duplicate edges that
974    // cannot both be two-use → one-use strands. Merge pieces whose curves
975    // coincide along their whole span (bidirectional max deviation within
976    // the weld band): keep the first, union the supports/pcurves/by_face
977    // registrations of the rest into it. Escape hatch:
978    // BREP_COINCIDENT_PIECE_MERGE=0.
979    lap("collect", &mut tail_lap, &mut tail_ms);
980    if std::env::var("BREP_COINCIDENT_PIECE_MERGE").as_deref() != Ok("0") {
981        let weld = assembler_weld(options.tolerance).max(options.tolerance * 10.0);
982        // Two pieces that coincide along their WHOLE spans (deviation within
983        // the weld both ways) also have matching end points, up to direction
984        // and a weld or two of overhang, so a piece whose ends are nowhere
985        // near the other's needs no 33-station projection sweep. The sweep is
986        // what made this pass quadratic in wall time: 630 pieces cost 3.2 s
987        // of a 3.8 s imprint on the 2026-09-12 mesh-import report, all of it
988        // rejecting pairs an end-point look already rules out.
989        let mut ends: Vec<[Vec3; 2]> = Vec::with_capacity(result.pieces.len());
990        for piece in &result.pieces {
991            let [d0, d1] = piece.curve.domain().or_refuse(KernelStage::Intersect, "domain")?;
992            ends.push([
993                piece.curve.evaluate(d0).or_refuse(KernelStage::Intersect, "evaluate")?,
994                piece.curve.evaluate(d1).or_refuse(KernelStage::Intersect, "evaluate")?,
995            ]);
996        }
997        let end_band = 4.0 * weld;
998        // A CLOSED piece (a full ring) has no end points to speak of: two
999        // rings of the same circle seamed at different azimuths coincide
1000        // along their whole spans while their domain ends sit anywhere on
1001        // the ring, so a closed piece always takes the full sweep.
1002        let ends_match = |a: &[Vec3; 2], b: &[Vec3; 2]| -> bool {
1003            let near = |p: Vec3, q: Vec3| p.sub(q).length() <= end_band;
1004            near(a[0], a[1])
1005                || near(b[0], b[1])
1006                || (near(a[0], b[0]) && near(a[1], b[1]))
1007                || (near(a[0], b[1]) && near(a[1], b[0]))
1008        };
1009        let mut removed: Vec<u64> = Vec::new();
1010        let mut index = 0;
1011        while index < result.pieces.len() {
1012            let mut other = index + 1;
1013            while other < result.pieces.len() {
1014                let coincide = ends_match(&ends[index], &ends[other]) && {
1015                    let a = &result.pieces[index];
1016                    let b = &result.pieces[other];
1017                    max_curve_deviation(&a.curve, &b.curve)? <= weld
1018                        && max_curve_deviation(&b.curve, &a.curve)? <= weld
1019                };
1020                if coincide {
1021                    let absorbed = result.pieces.remove(other);
1022                    ends.remove(other);
1023                    removed.push(absorbed.id);
1024                    let keeper = &mut result.pieces[index];
1025                    for pcurve in absorbed.pcurves {
1026                        if !keeper
1027                            .pcurves
1028                            .iter()
1029                            .any(|existing| {
1030                                existing.operand == pcurve.operand
1031                                    && existing.face_id == pcurve.face_id
1032                            })
1033                        {
1034                            keeper.pcurves.push(pcurve);
1035                        }
1036                    }
1037                    let keeper_id = keeper.id;
1038                    for record in &mut result.by_face {
1039                        if let Some(position) =
1040                            record.piece_ids.iter().position(|&id| id == absorbed.id)
1041                        {
1042                            if record.piece_ids.contains(&keeper_id) {
1043                                record.piece_ids.remove(position);
1044                            } else {
1045                                record.piece_ids[position] = keeper_id;
1046                            }
1047                        }
1048                    }
1049                } else {
1050                    other += 1;
1051                }
1052            }
1053            index += 1;
1054        }
1055        if !removed.is_empty() && std::env::var("BREP_DEBUG_BOOL").is_ok() {
1056            eprintln!("coincident-piece merge: absorbed {:?}", removed);
1057        }
1058    }
1059    // Rescue near-tangent SSI truncations BEFORE canonicalization so the added
1060    // bridge pieces' endpoints (existing crossing/stub vertices) fold into the
1061    // same junction merges as every other section.
1062    lap("coincident_merge", &mut tail_lap, &mut tail_ms);
1063    extend_truncated_sections(
1064        &mut result,
1065        &face_edge_lists,
1066        solid_a,
1067        solid_b,
1068        &charts,
1069        options.tolerance,
1070    )?;
1071    lap("extend_truncated", &mut tail_lap, &mut tail_ms);
1072    canonicalize_imprint_junctions(&mut result, solid_a, solid_b, &charts, options.tolerance)?;
1073    lap("canonicalize", &mut tail_lap, &mut tail_ms);
1074    // ONE RIM, ONE EDGE: with vertex identity final, dissolve the section
1075    // vertices that no operand edge passes through — the marcher's own
1076    // parameterization origin, which `process_curve` cannot tell from a
1077    // junction because it sees one pair at a time. See the method doc.
1078    let dissolved = dissolve_section_origin_vertices(
1079        &mut result,
1080        &marched_pieces,
1081        solid_a,
1082        solid_b,
1083        &charts,
1084        options.tolerance,
1085    )?;
1086    if dissolved > 0 && std::env::var("BREP_DEBUG_BOOL").is_ok() {
1087        eprintln!("origin-dissolve: {dissolved} parameterization vertex/vertices removed");
1088    }
1089    lap("origin_dissolve", &mut tail_lap, &mut tail_ms);
1090    // B2: reuse an existing boundary edge as the shared section edge wherever a
1091    // section coincides with one along its whole span (vertices are final after
1092    // canonicalization; `face_edge_lists` holds each face's boundary edges on
1093    // the healed operands). Runs here so both operands reference ONE edge.
1094    reuse_boundary_section_edges(
1095        &mut result,
1096        &face_edge_lists,
1097        solid_a,
1098        solid_b,
1099        options.tolerance,
1100    )?;
1101    // Capstone step 1 — instrumentation only, zero behavior change: report
1102    // every (section piece × boundary edge) contact where the piece runs
1103    // within the scale-derived band of the edge over a real span. Measuring
1104    // the bands here first validates the graze-contact model on the
1105    // acceptance suite before step 2's common-block machinery replaces a
1106    // grazed overlap with a shared edge.
1107    lap("reuse_boundary", &mut tail_lap, &mut tail_ms);
1108    report_graze_contacts(&result, &face_edge_lists, solid_a, solid_b, options.tolerance)?;
1109    if profile.enabled {
1110        let line = tail_ms
1111            .iter()
1112            .map(|(name, ms)| format!("{name}={ms:.2}"))
1113            .collect::<Vec<_>>()
1114            .join(" ");
1115        eprintln!("imprint.stages {line}");
1116    }
1117    Ok(result)
1118}
1119
1120/// Debug-only graze-contact survey (`BREP_DEBUG_GRAZE=1`): for each section
1121/// piece and each boundary edge of its support faces, sample the piece and
1122/// measure distance to the edge; report contacts whose in-band span exceeds
1123/// both the weld scale and 4× the minimum deviation (span-wise proximity, not
1124/// a point touch). `band_cap` reuses the residual-merge `sep_cap` ceiling —
1125/// measured, never grown. The output is the raw material for capstone step 2
1126/// (partial-span common-block): which contacts exist, their spans, and their
1127/// measured bands.
1128fn report_graze_contacts(
1129    result: &ImprintResultRecord,
1130    face_edge_lists: &HashMap<FaceKey, Vec<&EdgeRecord>>,
1131    solid_a: &BrepSolid,
1132    solid_b: &BrepSolid,
1133    tolerance: f64,
1134) -> Result<(), KernelRefusal> {
1135    if std::env::var("BREP_DEBUG_GRAZE").as_deref() != Ok("1") {
1136        return Ok(());
1137    }
1138    let raw_extent = raw_solid_extent(solid_a).max(raw_solid_extent(solid_b));
1139    let (_, band_cap) = residual_merge_bands(raw_extent, tolerance);
1140    const SAMPLES: usize = 17;
1141    for piece in &result.pieces {
1142        let [t0, t1] = [piece.t0, piece.t1];
1143        if !(t1 > t0) {
1144            continue;
1145        }
1146        for key in piece.support_faces {
1147            let Some(edges) = face_edge_lists.get(&key) else {
1148                continue;
1149            };
1150            for edge in edges {
1151                if edge.degenerate {
1152                    continue;
1153                }
1154                let mut in_band = 0usize;
1155                let mut min_dev = f64::INFINITY;
1156                let mut max_dev_in_band = 0.0f64;
1157                let mut span = 0.0f64;
1158                let mut prev: Option<(bool, Vec3)> = None;
1159                for k in 0..SAMPLES {
1160                    let t = t0 + (t1 - t0) * k as f64 / (SAMPLES - 1) as f64;
1161                    let point = piece.curve.evaluate(t).or_refuse(KernelStage::Intersect, "evaluate")?;
1162                    let deviation = project_point_to_curve(&edge.curve, point).or_refuse(KernelStage::Intersect, "project_point_to_curve")?.distance;
1163                    min_dev = min_dev.min(deviation);
1164                    let inside = deviation <= band_cap;
1165                    if inside {
1166                        in_band += 1;
1167                        max_dev_in_band = max_dev_in_band.max(deviation);
1168                        if let Some((true, prev_point)) = prev {
1169                            span += point.sub(prev_point).length();
1170                        }
1171                    }
1172                    prev = Some((inside, point));
1173                }
1174                // Span-wise contact: several consecutive samples in band and a
1175                // span that dwarfs the closest-approach (not a transversal
1176                // crossing, which dips in and out at one sample).
1177                if in_band >= 3 && span > (4.0 * min_dev).max(assembler_weld(tolerance)) {
1178                    eprintln!(
1179                        "graze: piece {} sup=[{}:{},{}:{}] ~ edge {}:{} span={:.3e} band=[{:.3e},{:.3e}] samples_in_band={}/{}",
1180                        piece.id,
1181                        piece.support_faces[0].operand,
1182                        piece.support_faces[0].face_id,
1183                        piece.support_faces[1].operand,
1184                        piece.support_faces[1].face_id,
1185                        key.operand,
1186                        edge.id,
1187                        span,
1188                        min_dev,
1189                        max_dev_in_band,
1190                        in_band,
1191                        SAMPLES
1192                    );
1193                }
1194            }
1195        }
1196    }
1197    Ok(())
1198}
1199
1200/// Are the two carriers TANGENT (normals parallel) where they both pass through
1201/// `point`?
1202///
1203/// The test the tangential-only refusal above needs: a point on the marched
1204/// intersection is a TANGENT NODE when the two surface normals there are
1205/// parallel. The threshold is the transversality bound the supplemental
1206/// detector already accepts seeds by (`TRANSVERSE_SEED_CROSS`), not the
1207/// far tighter pair-classifier bound — a node the march merely passes CLOSE to
1208/// still poisons the assembly, so this errs toward calling a pair singular.
1209fn pair_normals_parallel_at(
1210    first: TaggedFace<'_>,
1211    second: TaggedFace<'_>,
1212    point: Vec3,
1213) -> Result<bool, KernelRefusal> {
1214    let mut normals = [Vec3::default(); 2];
1215    for (slot, face) in normals.iter_mut().zip([first, second]) {
1216        let projection = project_point_to_surface(&face.face.surface, point)
1217            .or_refuse(KernelStage::Intersect, "project_point_to_surface")?;
1218        let Ok(normal) = face.face.surface.normal(projection.u, projection.v) else {
1219            // A pole/singular parameter point cannot witness transversality;
1220            // treat it as tangential so the pair stays refused.
1221            return Ok(true);
1222        };
1223        *slot = normal;
1224    }
1225    Ok(normals[0].cross(normals[1]).length() <= crate::TRANSVERSE_SEED_CROSS)
1226}