Skip to main content

brep_kernel/csg/imprint/
driver.rs

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