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