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/// Why a section goes to [`refine_section_against_carriers`].
38#[derive(Clone, Copy, Debug, PartialEq)]
39pub(super) enum SectionRefineRoute {
40 /// `BREP_SECTION_REFINE_GATE`: every section, at the variable's multiple.
41 Global,
42 /// A recovered fit reproduces its march points; check it between them.
43 Recovered,
44 /// A pair of grazing crossings on one trim edge.
45 Grazing,
46 /// The standing fit missed the tolerance it was asked for. The polyline
47 /// deviation can overstate the carrier miss (a band-raised floor reads the
48 /// input's noise), so the refiner's own carrier measure decides: a section
49 /// whose mid-spans already hold the gate leaves at round 0 as `BelowGate`,
50 /// unchanged. Measured 2026-10-04: the StationCeiling fits it reaches ship
51 /// 4.7e-5 to 2.1e-4 off the true section; the refiner brings them to
52 /// 1e-8..3e-8.
53 UnmetRequest,
54 /// A standing fit that MET its request on no other route, whose kept
55 /// stations all lie on both carriers within the fit tolerance, but whose
56 /// curve stands further off the carriers BETWEEN them than the local gate
57 /// ([`carrier_miss_route`]). The polyline it met is the march's chords; an
58 /// interpolant through consistent stations can still ring between them.
59 /// Measured 2026-10-04 (08356963b, 94 cases): 23 of 285 fast-path sections,
60 /// 13 distinct pairs, read their mid-spans up to 6.03e-6 off while every
61 /// kept station read within 1e-7; fixture 28 t217 pair 118x474 read 1.59e-6
62 /// between stations 1.8e-9 off.
63 CarrierMiss,
64}
65
66/// The carrier-miss route for a fit that met its request on no other route:
67/// refine at the local gate when its mid-spans miss the carriers by more than
68/// the fit tolerance WHILE every kept station is within it. Stations off a
69/// carrier are the input's (moving them is the reserved relocation ruling),
70/// so such a section keeps the fast path and is not reported as refined. An
71/// unreadable reading (NaN) never routes.
72pub(super) fn carrier_miss_route(station_upper: f64, midspan_upper: f64, fit_tolerance: f64) -> Option<(f64, SectionRefineRoute)> {
73 (station_upper.is_finite() && midspan_upper.is_finite() && station_upper <= fit_tolerance && midspan_upper > fit_tolerance)
74 .then_some((1.0, SectionRefineRoute::CarrierMiss))
75}
76
77/// The carrier readings [`carrier_miss_route`] decides on: the worst of both
78/// carriers over EVERY point of the section's run (the march's original
79/// stations, kept or dropped by the fit, so a fit through consistent kept
80/// stations cannot hide an inconsistent dropped one), and at the fit's
81/// mid-spans. Projector distances, so upper readings; NaN as soon as any is
82/// unreadable. The mid-span read is the route's TRIGGER, not a bound on the
83/// curve's maximum; [`knot_interior_carrier_upper`] is the finer read.
84pub(super) fn fast_path_carrier_readings(fit: &PolylineFit, run: &[Vec3], first: &NurbsSurface, second: &NurbsSurface) -> (f64, f64) {
85 let read = |surface: &NurbsSurface, point: Vec3| project_point_to_surface(surface, point).map_or(f64::NAN, |projection| projection.distance);
86 let mut station = 0.0f64;
87 for point in run {
88 let (a, b) = (read(first, *point), read(second, *point));
89 station = if a.is_nan() || b.is_nan() || station.is_nan() { f64::NAN } else { station.max(a).max(b) };
90 }
91 (station, midspan_carrier_upper(fit, first, second))
92}
93
94/// The worst of BOTH carriers at the fit's mid-spans, each carrier read on its
95/// own and checked finite BEFORE any max (`f64::max` would drop a NaN from one
96/// carrier and pass the other's reading): NaN as soon as either is unreadable.
97pub(super) fn midspan_carrier_upper(fit: &PolylineFit, first: &NurbsSurface, second: &NurbsSurface) -> f64 {
98 let read = |surface: &NurbsSurface, point: Vec3| project_point_to_surface(surface, point).map_or(f64::NAN, |projection| projection.distance);
99 let mut worst = 0.0f64;
100 for span in 0..fit.parameters.len().saturating_sub(1) {
101 let mid = 0.5 * (fit.parameters[span] + fit.parameters[span + 1]);
102 let (a, b) = match fit.curve.evaluate(mid) {
103 Ok(point) => (read(first, point), read(second, point)),
104 Err(_) => (f64::NAN, f64::NAN),
105 };
106 if !(a.is_finite() && b.is_finite()) || worst.is_nan() {
107 return f64::NAN;
108 }
109 worst = worst.max(a).max(b);
110 }
111 worst
112}
113
114/// The curve's worst distance from both carriers at the quarter points of
115/// every one of its own knot spans (its native pieces, where an interpolant
116/// is extremal between knots), NaN as soon as any read is unreadable. Sampled
117/// and projector-based: an upper reading at its points, not a certified
118/// maximum of the curve.
119pub(super) fn knot_interior_carrier_upper(curve: &NurbsCurve, first: &NurbsSurface, second: &NurbsSurface) -> f64 {
120 let read = |surface: &NurbsSurface, point: Vec3| project_point_to_surface(surface, point).map_or(f64::NAN, |projection| projection.distance);
121 let Ok([t0, t1]) = curve.domain() else { return f64::NAN };
122 let mut knots: Vec<f64> = curve.knots.iter().copied().filter(|knot| *knot >= t0 && *knot <= t1).collect();
123 knots.dedup();
124 let mut worst = 0.0f64;
125 for pair in knots.windows(2) {
126 if pair[1] <= pair[0] {
127 continue;
128 }
129 for fraction in [0.25, 0.5, 0.75] {
130 let value = match curve.evaluate(pair[0] + (pair[1] - pair[0]) * fraction) {
131 Ok(point) => {
132 let (a, b) = (read(first, point), read(second, point));
133 if a.is_finite() && b.is_finite() { a.max(b) } else { f64::NAN }
134 }
135 Err(_) => f64::NAN,
136 };
137 worst = if worst.is_nan() || !value.is_finite() { f64::NAN } else { worst.max(value) };
138 }
139 }
140 worst
141}
142
143/// The refine route and its gate (a multiple of the fit tolerance) for one
144/// section, or `None` for the fast path.
145///
146/// A standing fit that MET its request takes exactly the routes that existed
147/// before: the global gate alone when set, else a recovered fit, else a grazing
148/// pair (read lazily, only when the first two do not apply), else nothing — it
149/// goes straight to `process_curve`, untouched.
150///
151/// A fit that MISSED its request is always refined, at the local gate 1, or at
152/// a tighter global gate. A looser global gate does not mask the miss: seam 60
153/// was a recovered fit missing its 1e-7 request, and `BREP_SECTION_REFINE_GATE`
154/// above 1 used to let it through. Its route stays `UnmetRequest` under any
155/// global gate, so its cost reads apart from the global gate's.
156pub(super) fn section_refine_route(
157 global: Option<f64>,
158 recovered: bool,
159 grazing: impl FnOnce() -> bool,
160 met_requested: bool,
161) -> Option<(f64, SectionRefineRoute)> {
162 if !met_requested {
163 return Some((global.map_or(1.0, |gate| gate.min(1.0)), SectionRefineRoute::UnmetRequest));
164 }
165 global
166 .map(|gate| (gate, SectionRefineRoute::Global))
167 .or_else(|| recovered.then_some((1.0, SectionRefineRoute::Recovered)))
168 .or_else(|| grazing().then_some((1.0, SectionRefineRoute::Grazing)))
169}
170
171/// The multiple `BREP_SECTION_REFINE_GATE=on` uses: the smallest measured
172/// that trips the skew union's undersampled sections and leaves fixture 22 as
173/// it was (k = 10 flips it).
174const SECTION_REFINE_GATE: f64 = 100.0;
175
176/// The axis-aligned box of a curve's control points (rational ones read at
177/// their Cartesian position), grown by `margin` on every side: a box the
178/// curve cannot leave, since a NURBS curve with positive weights lies in its
179/// control hull.
180pub(super) fn control_hull_box(curve: &NurbsCurve, margin: f64) -> [Vec3; 2] {
181 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
182 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
183 for control in &curve.control_points {
184 let w = if control.w.abs() > 1e-300 { control.w } else { 1.0 };
185 let point = Vec3::new(control.x / w, control.y / w, control.z / w);
186 low = Vec3::new(low.x.min(point.x), low.y.min(point.y), low.z.min(point.z));
187 high = Vec3::new(high.x.max(point.x), high.y.max(point.y), high.z.max(point.z));
188 }
189 [
190 Vec3::new(low.x - margin, low.y - margin, low.z - margin),
191 Vec3::new(high.x + margin, high.y + margin, high.z + margin),
192 ]
193}
194
195/// Whether two closed boxes share a point.
196pub(super) fn boxes_overlap(a: &[Vec3; 2], b: &[Vec3; 2]) -> bool {
197 a[0].x <= b[1].x && b[0].x <= a[1].x && a[0].y <= b[1].y && b[0].y <= a[1].y && a[0].z <= b[1].z && b[0].z <= a[1].z
198}
199
200/// A clipped run's END span can be a sliver: the clip endpoint lands beside a
201/// march station, a spacing ratio to the run's median of 0.011 on
202/// `anotherBooleanFail`, and a global interpolant rings next to such a jump —
203/// the mid-spans near the ends miss their carriers by 1e-3, and bisecting them
204/// only makes the spacing more uneven (2026-09-27). The reduced witness
205/// (`imprint/tests.rs` `sliver_end_evening`) says what makes the jump ring: a
206/// sliver end ON both carriers fits to 3e-8, while the same end 2.7e-4 off
207/// the torus — the vendor's edge curves sit that far off the tori they
208/// bound, and the run's end is solved on such an edge — rings at 2e-3 with
209/// the interior at 3e-13 (2026-09-30). Below
210/// `SLIVER_END_FRACTION` of the median spacing the station beside the
211/// endpoint is RE-SOLVED at the middle of the merged gap as a fresh
212/// intersection point (the default, `BREP_SECTION_SLIVER_END=resolve`) or
213/// dropped (`drop`); `off` restores the unevened fit for a before/after
214/// reading. Returns how many ends were evened.
215///
216/// ON BY DEFAULT since 2026-09-30 (lane K6), behind the measurement in
217/// [`even_sliver_ends_measured`]: only a section whose fit shows the ringing signature
218/// ([`ringing_signature`]) is evened, and the evened fit stands only where it reads closer to
219/// the carriers by [`SLIVER_END_GAIN`]. The ringing is what a marched section carries into the
220/// result: on `anotherBooleanFail` the sphere sections leave the analytic vendor torus by
221/// 3.1e-3 within 1% of their span (measured against the STEP file's own `TOROIDAL_SURFACE`,
222/// not the projector), the cut torus faces depart from their edges by 2.6e-4 and the result
223/// shell's vector-area closure reads 1.88e-4 against a 3.83e-5 bar. Evened, the sections sit
224/// 5e-8 off, the shell closes at 1.7e-6 and the boolean takes 2.3 s instead of 9.1 s. The
225/// 2026-09-27 shelving reason was three fixture-25 exact-value pins moving under a blanket
226/// `resolve`; a blanket default also refused fixture 28's reversed subtract and broke fixture
227/// 26's idempotence, which is why the signature gates it. Fixture 25's sections do not show
228/// the signature at the shipped contrast and its pins stand.
229fn even_sliver_ends(
230 section: &mut Vec<Vec3>,
231 first: &NurbsSurface,
232 second: &NurbsSurface,
233 tolerance: f64,
234) -> Result<usize, String> {
235 static MODE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
236 let mode = MODE.get_or_init(|| {
237 std::env::var("BREP_SECTION_SLIVER_END").unwrap_or_else(|_| "resolve".to_string())
238 });
239 if !(mode == "drop" || mode == "resolve") {
240 return Ok(0);
241 }
242 let mut evened = 0;
243 // One sliver at a time: each removal changes the spans it sits between.
244 for _ in 0..SLIVER_END_ROUNDS {
245 let count = section.len();
246 if count < 6 {
247 break;
248 }
249 let mut chords: Vec<f64> = section.windows(2).map(|pair| pair[1].sub(pair[0]).length()).collect();
250 let lengths = chords.clone();
251 chords.sort_by(f64::total_cmp);
252 let median = chords[chords.len() / 2];
253 let spans = lengths.len();
254 let near_ends = [0, 1, 2, spans - 3, spans - 2, spans - 1];
255 let Some(&sliver) = near_ends
256 .iter()
257 .filter(|&&span| lengths[span] < sliver_end_fraction() * median)
258 .min_by(|a, b| lengths[**a].total_cmp(&lengths[**b]))
259 else {
260 break;
261 };
262 // The sliver's bounding stations are `sliver` and `sliver + 1`; a run
263 // endpoint is never moved. Of the interior ones, remove the one whose
264 // removal leaves the shorter merged span.
265 let mut candidates = Vec::new();
266 for station in [sliver, sliver + 1] {
267 if station == 0 || station == count - 1 {
268 continue;
269 }
270 let merged = section[station + 1].sub(section[station - 1]).length();
271 candidates.push((station, merged));
272 }
273 let Some(&(station, _)) = candidates.iter().min_by(|a, b| a.1.total_cmp(&b.1)) else {
274 break;
275 };
276 if mode == "resolve" {
277 let (before, after) = (section[station - 1], section[station + 1]);
278 let seed = before.add(after).scale(0.5);
279 let reach = 0.5 * after.sub(before).length();
280 match crate::surface_surface_intersection::refine_to_intersection(first, second, seed, tolerance, reach)? {
281 Some(point) => section[station] = point,
282 None => {
283 section.remove(station);
284 }
285 }
286 } else {
287 section.remove(station);
288 }
289 evened += 1;
290 }
291 Ok(evened)
292}
293
294/// At 0.25 six of `anotherBooleanFail`'s eight sections stay NoProgress
295/// (worst miss 2.7e-4); at 0.5 all eight are evened and fall below the
296/// refinement gate (worst miss 2.8e-3 -> 5.4e-8).
297const SLIVER_END_FRACTION: f64 = 0.5;
298
299/// How many sliver spans one section may lose, at most, over both ends. Every
300/// `anotherBooleanFail` section reaches this cap (the march lands several
301/// closely spaced stations beside each clip end), and the evened fits read
302/// 2.2e-8..5.4e-8 against the carriers under it; whether a higher cap would
303/// read differently was not measured (lane K6, 2026-09-30).
304const SLIVER_END_ROUNDS: usize = 4;
305
306/// `BREP_SECTION_SLIVER_FRACTION` overrides [`SLIVER_END_FRACTION`] for the
307/// measurement that picks it.
308fn sliver_end_fraction() -> f64 {
309 static FRACTION: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
310 *FRACTION.get_or_init(|| {
311 std::env::var("BREP_SECTION_SLIVER_FRACTION")
312 .ok()
313 .and_then(|value| value.parse::<f64>().ok())
314 .filter(|fraction| *fraction > 0.0 && *fraction < 1.0)
315 .unwrap_or(SLIVER_END_FRACTION)
316 })
317}
318
319fn section_fit_degree() -> usize {
320 static DEGREE: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
321 *DEGREE.get_or_init(|| {
322 std::env::var("BREP_SECTION_FIT_DEGREE")
323 .ok()
324 .and_then(|value| value.parse::<usize>().ok())
325 .filter(|degree| (1..=7).contains(degree))
326 .unwrap_or(5)
327 })
328}
329
330/// `BREP_SPAN_BUDGET`, read once: unset leaves [`ImprintOptions::span_budget`]
331/// in charge; `off` forces none; `fit` is the pair's `intersection_fit` at the
332/// operands' scale; a number is an absolute budget.
333#[derive(Clone, Copy, Debug, PartialEq)]
334enum SpanBudgetSwitch {
335 Unset,
336 Off,
337 Fit,
338 Absolute(f64),
339}
340
341fn span_budget_switch() -> SpanBudgetSwitch {
342 static SWITCH: std::sync::OnceLock<SpanBudgetSwitch> = std::sync::OnceLock::new();
343 *SWITCH.get_or_init(|| match std::env::var("BREP_SPAN_BUDGET") {
344 Ok(value) if value == "fit" => SpanBudgetSwitch::Fit,
345 Ok(value) if value == "off" || value == "0" => SpanBudgetSwitch::Off,
346 Ok(value) => value
347 .parse::<f64>()
348 .ok()
349 .filter(|budget| *budget > 0.0)
350 .map_or(SpanBudgetSwitch::Unset, SpanBudgetSwitch::Absolute),
351 Err(_) => SpanBudgetSwitch::Unset,
352 })
353}
354
355/// The span budget every march of this imprint carries: the option under
356/// its environment override. `fit` is what the catalogue names as the
357/// budget a lane may carry, the imprint's `intersection_fit`
358/// (`KernelTolerances::for_scale`), never the corrector's `tolerance`.
359pub(super) fn span_budget(options: &ImprintOptions, scale: f64) -> Option<f64> {
360 match span_budget_switch() {
361 SpanBudgetSwitch::Unset => options.span_budget,
362 SpanBudgetSwitch::Off => None,
363 SpanBudgetSwitch::Fit => Some(crate::KernelTolerances::for_scale(scale, options.tolerance).intersection_fit),
364 SpanBudgetSwitch::Absolute(budget) => Some(budget),
365 }
366}
367
368/// [`ImprintOptions::hermite_sections`] under its environment override.
369fn hermite_sections(options: &ImprintOptions) -> bool {
370 match std::env::var("BREP_HERMITE_SECTIONS").as_deref() {
371 Ok("0") => false,
372 Ok(_) => true,
373 Err(_) => options.hermite_sections,
374 }
375}
376
377/// [`ImprintOptions::exact_iso_pcurves`] under its environment override
378/// (`BREP_EXACT_ISO_PCURVES`: `0`/`off`, `check`, anything else on).
379#[derive(Clone, Copy, Debug, PartialEq, Eq)]
380pub(super) enum ExactIsoPcurves {
381 Off,
382 On,
383 /// The fit lane runs; the exact pcurve is built beside it and compared
384 /// (`ImprintBuilder::exact_pcurve_check`).
385 Check,
386}
387
388fn exact_iso_pcurves(options: &ImprintOptions) -> ExactIsoPcurves {
389 match std::env::var("BREP_EXACT_ISO_PCURVES").as_deref() {
390 Ok("0") | Ok("off") => ExactIsoPcurves::Off,
391 Ok("check") => ExactIsoPcurves::Check,
392 Ok(_) => ExactIsoPcurves::On,
393 Err(_) if options.exact_iso_pcurves => ExactIsoPcurves::On,
394 Err(_) => ExactIsoPcurves::Off,
395 }
396}
397
398thread_local! {
399 /// Set by `build_imprints_impl` as it returns, read by the boolean
400 /// right after ([`take_rescue_firings`]) into
401 /// `intersect.march_swap_rescue` and `intersect.clip_swap_rescue`; a
402 /// thread-local rather than a field so the shared record's eight
403 /// literals stay as they are.
404 static RESCUE_FIRINGS: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
405}
406
407/// `(march-order swap rescues, clip-order swap rescues)` of the last imprint
408/// built on this thread, cleared by the read.
409pub(crate) fn take_rescue_firings() -> (u64, u64) {
410 RESCUE_FIRINGS.with(|cell| cell.replace((0, 0)))
411}
412
413/// [`ImprintOptions::march_starts`] under its environment override, as
414/// `(ordered_starts, consume_starts, tree_seeds)` for every march this
415/// imprint runs. `BREP_MARCH_STARTS`: `0`/`off`; `order` (A3's ordering
416/// alone); `snap` (the ordering with run-through starts consumed); `tree`
417/// (A4 alone); `order+tree`; anything else all three.
418fn march_starts(options: &ImprintOptions) -> (bool, bool, bool) {
419 match std::env::var("BREP_MARCH_STARTS").as_deref() {
420 Ok("0") | Ok("off") => (false, false, false),
421 Ok("order") => (true, false, false),
422 Ok("snap") => (true, true, false),
423 Ok("tree") => (false, false, true),
424 Ok("order+tree") => (true, false, true),
425 Ok(_) => (true, true, true),
426 Err(_) => (options.march_starts, options.march_starts, options.march_starts),
427 }
428}
429
430/// `BREP_SPAN_CENSUS=<file>`: one line per clipped section, appended, so a
431/// corpus replay can be read for station growth and refine-route firings
432/// under a budget. `BREP_CENSUS_LABEL` names the run on every line.
433fn span_census(line: impl FnOnce() -> String) {
434 if let Ok(path) = std::env::var("BREP_SPAN_CENSUS") {
435 use std::io::Write;
436 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
437 let label = std::env::var("BREP_CENSUS_LABEL").unwrap_or_default();
438 let _ = writeln!(file, "span-budget census: label {label} {}", line());
439 }
440 }
441}
442
443/// A marched section is held to the fit tolerance against the CARRIERS, not
444/// only against the polyline it was fitted through (2026-09-26). The march
445/// refines every station onto both surfaces to `tolerance * 0.01`, but the
446/// cubic interpolant between stations is measured by `fit_polyline` against the
447/// polyline's own chords, which are further from the true curve than the
448/// interpolant is — so a systematic bias of the interpolant (2.8e-8 mean,
449/// 4.7e-7 worst, inside the bore on `BadBoolean`'s sphere/bore rim at 135
450/// stations) passed every check the fit could make, and both trims inherited it
451/// (+1.14e-6 on the solid's volume, against two independent readings agreeing
452/// to 1e-8). The check below evaluates the fitted curve at every mid-span,
453/// measures it against both carriers, and where it misses `fit tolerance *
454/// SECTION_MIDSPAN_FRACTION` inserts a TRUE intersection point there and
455/// refits. The interpolant's bias falls as h^4, so one round usually suffices.
456/// A round that neither halves the worst miss nor lowers the count of spans
457/// missing is not in that regime — a carrier's C0 crease puts a corner in the
458/// section (fixture 25's pierce solid), where every round only doubles the
459/// stations — so that round is discarded and the loop stops. It is also capped
460/// in rounds and stations, and names how it left.
461/// `BREP_SECTION_REFINE_GATE=off` switches it off, for the before/after reading
462/// and nothing else.
463pub(super) const SECTION_MIDSPAN_FRACTION: f64 = 0.1;
464const SECTION_MIDSPAN_ROUNDS: usize = 4;
465/// A round earns another if it cuts the worst mid-span miss by this factor
466/// (h^4 promises 16 for a halved spacing) OR lowers the count of spans
467/// missing: near the floor a few isolated spans hold the worst while the count
468/// falls, and on a corner the count GROWS round on round.
469const SECTION_MIDSPAN_PROGRESS: f64 = 0.5;
470
471/// Squared distance from `point` to the segment `from`-`to`.
472fn distance_to_segment_sq(point: Vec3, from: Vec3, to: Vec3) -> f64 {
473 let d = to.sub(from);
474 let l2 = d.dot(d);
475 let t = if l2 > 0.0 { (point.sub(from).dot(d) / l2).clamp(0.0, 1.0) } else { 0.0 };
476 let q = from.add(d.scale(t));
477 point.sub(q).dot(point.sub(q))
478}
479
480/// How a mid-span refinement left (`SECTION_MIDSPAN_*`), for the pair trace.
481#[derive(Clone, Copy, Debug, PartialEq)]
482pub(super) enum MidspanExit {
483 /// Every mid-span within the floor of both carriers.
484 Converged,
485 /// The unrefined fit's worst mid-span is within the gate: the section is
486 /// sampled well enough that its degree carries it, and stations would
487 /// cost more downstream than they return.
488 BelowGate,
489 /// Some mid-span still misses and no inserted point could improve it
490 /// (the Newton failed, wandered off the span, or landed on a station
491 /// already present).
492 Stalled,
493 /// A round neither halved the worst miss nor lowered the count of spans
494 /// missing: not the h^4 regime, so the section has a corner the
495 /// interpolant cannot follow. That round's fit is discarded.
496 NoProgress,
497 RoundBudget,
498 StationCeiling,
499}
500
501/// Measure `fit` at every mid-span against both carriers; return the worst
502/// distance, the count of spans that miss `floor` and, for each of them the
503/// Newton could place, the refined intersection point to insert (with the
504/// span's index).
505fn midspan_misses(
506 fit: &PolylineFit,
507 first: &NurbsSurface,
508 second: &NurbsSurface,
509 floor: f64,
510 tolerance: f64,
511) -> Result<(f64, usize, Vec<(usize, Vec3)>), String> {
512 let mut worst = 0.0f64;
513 let mut missing = 0usize;
514 let mut inserts = Vec::new();
515 for span in 0..fit.parameters.len().saturating_sub(1) {
516 let mid = 0.5 * (fit.parameters[span] + fit.parameters[span + 1]);
517 let point = fit.curve.evaluate(mid)?;
518 let off = project_point_to_surface(first, point)?
519 .distance
520 .max(project_point_to_surface(second, point)?.distance);
521 worst = worst.max(off);
522 if off <= floor {
523 continue;
524 }
525 missing += 1;
526 // The refined point must stay on THIS span: half its chord is the reach.
527 let reach = 0.5 * fit.kept[span + 1].sub(fit.kept[span]).length();
528 if let Some(refined) = crate::surface_surface_intersection::refine_to_intersection(
529 first, second, point, tolerance, reach,
530 )? {
531 inserts.push((span, refined));
532 }
533 }
534 Ok((worst, missing, inserts))
535}
536
537/// Every mid-span miss of `fit` against both carriers, measured and nothing
538/// else: what [`even_sliver_ends_measured`] reads before and after evening.
539/// It refines nothing, so a section without a sliver end pays only the
540/// projections.
541fn midspan_misses_only(fit: &PolylineFit, first: &NurbsSurface, second: &NurbsSurface) -> Result<Vec<f64>, String> {
542 let mut misses = Vec::with_capacity(fit.parameters.len());
543 for span in 0..fit.parameters.len().saturating_sub(1) {
544 let mid = 0.5 * (fit.parameters[span] + fit.parameters[span + 1]);
545 let point = fit.curve.evaluate(mid)?;
546 let off = project_point_to_surface(first, point)?
547 .distance
548 .max(project_point_to_surface(second, point)?.distance);
549 misses.push(off);
550 }
551 Ok(misses)
552}
553
554/// The signature of a global interpolant ringing on a sliver end span: the
555/// worst mid-span miss sits in one of the [`SLIVER_END_SPANS`] spans at either
556/// end, is over the fit `floor`, AND stands [`SLIVER_RING_CONTRAST`] times
557/// above the median of the interior spans. A section sampled too coarsely for the carriers misses
558/// everywhere at once and does not show this; a section with fewer than two
559/// interior spans offers no evidence either way. Returns the end-span worst
560/// and the interior median when the signature is present.
561fn ringing_signature(misses: &[f64], floor: f64) -> Option<(f64, f64)> {
562 let spans = misses.len();
563 if spans < 2 * SLIVER_END_SPANS + 2 {
564 return None;
565 }
566 let ends = misses[..SLIVER_END_SPANS]
567 .iter()
568 .chain(&misses[spans - SLIVER_END_SPANS..])
569 .copied()
570 .fold(0.0f64, f64::max);
571 let mut interior: Vec<f64> = misses[SLIVER_END_SPANS..spans - SLIVER_END_SPANS].to_vec();
572 interior.sort_by(f64::total_cmp);
573 let median = interior[interior.len() / 2];
574 let worst = misses.iter().copied().fold(0.0f64, f64::max);
575 // Below the fit floor there is nothing to even: a 1e-8 miss on a planar
576 // section passed the contrast on fixture 26 and flipped its idempotence
577 // reading while changing nothing a bar could see.
578 (ends > floor.max(sliver_min_miss()) && ends >= worst && ends > sliver_ring_contrast() * median.max(f64::MIN_POSITIVE))
579 .then_some((ends, median))
580}
581
582/// How many spans at each end of a section [`even_sliver_ends`] may even —
583/// the same three it inspects — and where [`ringing_signature`] looks.
584const SLIVER_END_SPANS: usize = 3;
585
586/// The ringing signature's contrast: the end spans' worst mid-span miss over
587/// the interior spans' median. A converged march leaves its interior at
588/// 1e-9 and a sliver end rings at 1e-4..1e-3, so the ratio is 1e5 and up:
589/// `anotherBooleanFail`'s eight sections read 1.3e5..2.2e6 (interior
590/// 6e-10..2.6e-9, ends 1.6e-4..2.8e-3). A section whose interior itself
591/// misses is under-sampled rather than ringing, and evening its end moves a
592/// junction the assembler then cannot repair: fixture 28's pair 822×118
593/// (interior 1.5e-6, ends 1.4e-3, contrast 930) refused the reversed
594/// subtract whenever it was evened, while its pairs 474×118 (1.7e4) and
595/// 842×118 (1.4e5) are evened and build. Fixture 25's two sections read
596/// 2.2e3 and 5.9e3 (interior 2e-8) and keep their stations and their pins.
597/// 3e4 sits at least 4× from the nearest reading on either side
598/// (2026-09-30).
599const SLIVER_RING_CONTRAST: f64 = 3.0e4;
600
601/// `BREP_SECTION_SLIVER_CONTRAST` overrides [`SLIVER_RING_CONTRAST`] for the
602/// measurement that picks it.
603fn sliver_ring_contrast() -> f64 {
604 static CONTRAST: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
605 *CONTRAST.get_or_init(|| {
606 std::env::var("BREP_SECTION_SLIVER_CONTRAST")
607 .ok()
608 .and_then(|value| value.parse::<f64>().ok())
609 .filter(|contrast| *contrast >= 1.0)
610 .unwrap_or(SLIVER_RING_CONTRAST)
611 })
612}
613
614/// `BREP_SECTION_SLIVER_MIN_MISS` raises the absolute floor under the end
615/// spans' worst miss, for measurement only (the shipped floor is the fit
616/// tolerance).
617fn sliver_min_miss() -> f64 {
618 static MIN: std::sync::OnceLock<f64> = std::sync::OnceLock::new();
619 *MIN.get_or_init(|| {
620 std::env::var("BREP_SECTION_SLIVER_MIN_MISS")
621 .ok()
622 .and_then(|value| value.parse::<f64>().ok())
623 .filter(|miss| *miss >= 0.0)
624 .unwrap_or(0.0)
625 })
626}
627
628/// How a sliver-end evening left, for the census line.
629#[derive(Clone, Copy, Debug, PartialEq)]
630pub(super) enum SliverEvening {
631 /// No end span was shorter than the fraction, or the hatch is `off`.
632 NoSliver,
633 /// A sliver end exists but the unevened fit does not show the ringing
634 /// signature ([`ringing_signature`]): its stations stand.
635 NotRinging { slivers: usize, worst: f64 },
636 /// The evened section's fit reads at least [`SLIVER_END_GAIN`] times
637 /// closer to the carriers at its worst mid-span: the evened fit stands.
638 Accepted { slivers: usize, before: f64, after: f64, interior: f64 },
639 /// Evening did not earn its keep: the unevened fit stands, the stations
640 /// untouched.
641 Rejected { slivers: usize, before: f64, after: f64, interior: f64 },
642}
643
644/// A sliver-end evening must earn its keep: the evened fit stands only when
645/// its worst mid-span miss against both carriers falls by at least this
646/// factor. Measured on 2026-09-30 against the blanket default: `resolve` on
647/// every section that HAS a sliver end refused the reversed subtract of
648/// fixture 28 (`march_swap_rescue_recovers_reversed_subtract_t217`, an open
649/// loop) and made fixture 26's union read differently before and after
650/// operand splitting, while on `anotherBooleanFail` (2.8e-3 → 5e-8) and
651/// fixture 25 (2.0e-5 / 3.4e-5 → 9.2e-7) it cut the miss by 20× to 5e4×. A
652/// section whose evened fit is not clearly better keeps its stations.
653const SLIVER_END_GAIN: f64 = 2.0;
654
655/// [`even_sliver_ends`] with the measurement that decides whether it stands:
656/// fit the section as marched; if its mid-spans show the ringing signature
657/// ([`ringing_signature`]), even its sliver ends, fit again, and keep the
658/// evened fit only when its worst mid-span wins by [`SLIVER_END_GAIN`].
659/// Returns the section, its fit, the recovery flag of the fit that stands and
660/// the outcome.
661pub(super) fn even_sliver_ends_measured(
662 section: Vec<Vec3>,
663 first: &NurbsSurface,
664 second: &NurbsSurface,
665 tolerance: f64,
666 fit_tolerance: f64,
667 maximum_fit_points: usize,
668 local_fit: bool,
669) -> Result<(Vec<Vec3>, PolylineFit, bool, SliverEvening), String> {
670 let (fit, recovered) = crate::fit::fit_polyline_of_degree_with_recovery(
671 §ion,
672 fit_tolerance,
673 maximum_fit_points,
674 local_fit,
675 section_fit_degree(),
676 )?;
677 let mut evened = section.clone();
678 let slivers = even_sliver_ends(&mut evened, first, second, tolerance)?;
679 if slivers == 0 {
680 return Ok((section, fit, recovered, SliverEvening::NoSliver));
681 }
682 let misses = midspan_misses_only(&fit, first, second)?;
683 let Some((before, interior)) = ringing_signature(&misses, fit_tolerance) else {
684 let worst = misses.iter().copied().fold(0.0f64, f64::max);
685 return Ok((section, fit, recovered, SliverEvening::NotRinging { slivers, worst }));
686 };
687 let (evened_fit, evened_recovered) = crate::fit::fit_polyline_of_degree_with_recovery(
688 &evened,
689 fit_tolerance,
690 maximum_fit_points,
691 local_fit,
692 section_fit_degree(),
693 )?;
694 let after_misses = midspan_misses_only(&evened_fit, first, second)?;
695 let after = after_misses.iter().copied().fold(0.0f64, f64::max);
696 if let Ok(path) = std::env::var("BREP_SECTION_SLIVER_CENSUS") {
697 use std::io::Write;
698 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
699 let chords = |points: &[Vec3]| points.windows(2).map(|pair| format!("{:.4e}", pair[1].sub(pair[0]).length())).collect::<Vec<_>>().join(" ");
700 let fmt = |values: &[f64]| values.iter().map(|value| format!("{value:.2e}")).collect::<Vec<_>>().join(" ");
701 let _ = writeln!(file, "sliver-end detail: stations {} -> {} kept {} -> {}", section.len(), evened.len(), fit.kept.len(), evened_fit.kept.len());
702 let _ = writeln!(file, "sliver-end detail: chords before: {}", chords(§ion));
703 let _ = writeln!(file, "sliver-end detail: chords after: {}", chords(&evened));
704 let _ = writeln!(file, "sliver-end detail: misses before: {}", fmt(&misses));
705 let _ = writeln!(file, "sliver-end detail: misses after: {}", fmt(&after_misses));
706 let _ = writeln!(file, "sliver-end detail: ends before: {:?} {:?}", section[0], section[section.len() - 1]);
707 let _ = writeln!(file, "sliver-end detail: ends after: {:?} {:?}", evened[0], evened[evened.len() - 1]);
708 }
709 }
710 if after * SLIVER_END_GAIN <= before {
711 Ok((evened, evened_fit, evened_recovered, SliverEvening::Accepted { slivers, before, after, interior }))
712 } else {
713 Ok((section, fit, recovered, SliverEvening::Rejected { slivers, before, after, interior }))
714 }
715}
716
717/// Refit `section` until its mid-spans sit within `floor` of both carriers,
718/// inserting true intersection points where they do not. Returns the fit that
719/// stands, the worst mid-span miss it still carries, the unrefined fit's worst
720/// (what the gate read), the rounds spent and the exit.
721/// Keep the single best measured refiner state (root grant 2026-10-04,
722/// accepted-representative sections only): replaced when `worst` is finite and
723/// no larger than the kept one (ties go to the later state). An unreadable
724/// reading is never kept. At most one snapshot is held.
725pub(super) fn keep_best_state<S>(best: &mut Option<(S, f64)>, state: impl FnOnce() -> S, worst: f64) {
726 if worst.is_finite() && best.as_ref().is_none_or(|(_, kept)| worst <= *kept) {
727 *best = Some((state(), worst));
728 }
729}
730
731/// The rollback a budget-type exit (RoundBudget, StationCeiling, Stalled)
732/// takes: on a section where an ACCEPTED representative round ran, the kept
733/// best state when its COMMON reading is strictly lower than the current
734/// state's common reading on the same instrument (both carriers, finite, at the
735/// fit's mid-spans and its own knot-span quarter points). Otherwise `None`:
736/// the exit returns the current state exactly as before, with nothing cloned.
737/// An unreadable current reading never rolls back (it is not hidden).
738pub(super) fn take_rollback<S>(best: &mut Option<(S, f64)>, current_common: f64, representative_ran: bool) -> Option<(S, f64)> {
739 let lower = matches!(best, Some((_, kept)) if representative_ran && current_common.is_finite() && *kept < current_common);
740 if lower { best.take() } else { None }
741}
742
743/// Which fit a refinement RETURNED: a receipt for controls, deciding nothing.
744#[derive(Clone, Copy, Debug, PartialEq)]
745pub(super) enum RefinedFitSource {
746 /// The standing fit the caller passed in.
747 Standing,
748 /// The original full refit of the run, made directly (`full-unmatched`,
749 /// or the retry after an unreadable representative or a seeded stall).
750 FullRefit,
751 /// The seeded (forced-station) call and its own path: a `Fallback` path
752 /// IS the original full refit.
753 Seeded(crate::fit::SeededPath),
754 /// The representative candidate call and its own path: a `Fallback` path
755 /// IS the original full refit.
756 Representatives(crate::fit::SeededPath),
757 /// The best measured state the budget-exit rollback restored.
758 Rollback,
759}
760
761/// The refiner's receipt: whether the run took the representative route at
762/// all, and which fit it returned (None on an error return). Production
763/// callers discard it; only controls read it.
764#[allow(dead_code)]
765#[derive(Clone, Copy, Debug, Default, PartialEq)]
766pub(super) struct RefineReceipt {
767 pub original_consistent: bool,
768 pub returned: Option<RefinedFitSource>,
769}
770
771pub(super) fn refine_section_against_carriers(
772 section: &mut Vec<Vec3>,
773 fit: PolylineFit,
774 first: &NurbsSurface,
775 second: &NurbsSurface,
776 fit_tolerance: f64,
777 tolerance: f64,
778 local_fit: bool,
779 gate: f64,
780) -> Result<(PolylineFit, f64, f64, usize, MidspanExit), String> {
781 // `BREP_SECTION_REPRESENTATIVES=0` restores the seeded path everywhere for
782 // an attribution A/B only; read here, once per call, as before.
783 let representatives = std::env::var("BREP_SECTION_REPRESENTATIVES").as_deref() != Ok("0");
784 refine_section_against_carriers_with(section, fit, first, second, fit_tolerance, tolerance, local_fit, gate, representatives, &mut RefineReceipt::default())
785}
786
787/// [`refine_section_against_carriers`] with the representative switch passed
788/// in (the env hatch's value, so a control can take the seeded route without
789/// touching the process environment) and a receipt of the fit returned.
790#[allow(clippy::too_many_arguments)]
791pub(super) fn refine_section_against_carriers_with(
792 section: &mut Vec<Vec3>,
793 fit: PolylineFit,
794 first: &NurbsSurface,
795 second: &NurbsSurface,
796 fit_tolerance: f64,
797 tolerance: f64,
798 local_fit: bool,
799 gate: f64,
800 representatives: bool,
801 receipt: &mut RefineReceipt,
802) -> Result<(PolylineFit, f64, f64, usize, MidspanExit), String> {
803 // A reused receipt starts clean, so an error return leaves `returned`
804 // None as documented, never a previous call's value.
805 *receipt = RefineReceipt::default();
806 // Which fit `fit` currently is, and which fit `previous` holds.
807 let mut fit_source = RefinedFitSource::Standing;
808 let mut previous_source = RefinedFitSource::Standing;
809 let floor = fit_tolerance * SECTION_MIDSPAN_FRACTION;
810 let mut unrefined = f64::NAN;
811 let mut fit = fit;
812 let mut rounds = 0usize;
813 let mut previous: Option<(PolylineFit, f64, usize)> = None;
814 // Every true intersection point inserted so far: the residual-driven bend
815 // witnesses, which the refit must keep as stations.
816 let mut inserted_points: Vec<Vec3> = Vec::new();
817 // `BREP_SECTION_REFINE_CENSUS`: one `section-refine round:` line per round,
818 // bound to its section by the run's head and tail, so a stall can be read
819 // round by round (debug only; nothing below reads it).
820 let round_census = std::env::var("BREP_SECTION_REFINE_CENSUS").ok();
821 let (head, tail) = (section[0], section[section.len() - 1]);
822 // Whether EVERY original point of the run lies on both carriers within the
823 // fit tolerance (projector upper readings, NaN = no): only such a run takes
824 // the refiner-specific representative candidate below. A run with any
825 // point off a carrier keeps the existing seeded path exactly (moving or
826 // discounting input points is the reserved relocation ruling).
827 // `representatives` false (the `BREP_SECTION_REPRESENTATIVES=0` hatch)
828 // restores the seeded path everywhere for an attribution A/B only.
829 let original_consistent = representatives
830 && section.iter().all(|point| {
831 let a = project_point_to_surface(first, *point).map_or(f64::NAN, |projection| projection.distance);
832 let b = project_point_to_surface(second, *point).map_or(f64::NAN, |projection| projection.distance);
833 a.is_finite() && b.is_finite() && a <= fit_tolerance && b <= fit_tolerance
834 });
835 receipt.original_consistent = original_consistent;
836 let mut refit_path = String::from("standing");
837 let mut refit_seeds = 0usize;
838 let mut refit_seeds_kept = 0usize;
839 let mut refit_seeds_outside_pool = 0usize;
840 let mut refit_pool = 0usize;
841 let mut refit_pool_discarded = 0usize;
842 let mut refit_pool_floor = f64::NAN;
843 let mut refit_seed_min_chord = f64::NAN;
844 // Trace only: what the closest seed pair is made of (kept station or
845 // refiner insert at each end), to place the near pair upstream.
846 let mut refit_seed_min_pair = "n/a";
847 // Trace only: every seed the full refit's pool rejects, by ORIGIN (a march
848 // station of the run, or a refiner insert) and by REASON (dropped by
849 // `station_pool`'s simplification, or kept by it and then conditioned
850 // away as a near-duplicate): [march simplified, march conditioned,
851 // insert simplified, insert conditioned].
852 let mut refit_rejected = [0usize; 4];
853 // Fits made for the round being judged (2 when its seeded fit stalled and
854 // the full refit was tried as well), and that retry's own cost.
855 let mut refit_trials = 1usize;
856 let mut retry_ms = 0.0f64;
857 // Whether the fit judged this round is an ACCEPTED seeded fit (not merely
858 // a seeded call: a seeded call that fell back already IS the full refit).
859 let mut seeded_refit = false;
860 // Whether the fit judged this round is an ACCEPTED representative
861 // candidate, and what that candidate call spent (its own interpolations
862 // and whether it ended on the full refit), kept apart from the fit's
863 // report, which on a fallback must stay the full refit's to the bit.
864 let mut representative_fit = false;
865 let mut refit_cost = crate::fit::RepresentativeCost::default();
866 // Whether an ACCEPTED representative round ran on this section, and the one
867 // best measured (fit, section) state kept for its budget exits.
868 let mut representative_ran = false;
869 let mut best_state: Option<((PolylineFit, Vec<Vec3>), f64)> = None;
870 loop {
871 // On an accepted representative candidate a read or refinement ERROR is
872 // not a refusal: the section takes the original full refit of the same
873 // run instead, as an unreadable read does below.
874 let (mut worst, missing, inserts) = match midspan_misses(&fit, first, second, floor, tolerance) {
875 Ok(reading) => reading,
876 Err(_) if representative_fit => {
877 representative_fit = false;
878 seeded_refit = false;
879 let maximum_points = section.len().min(MAX_FIT_STATIONS);
880 fit = fit_polyline_of_degree(section, fit_tolerance, maximum_points, local_fit, section_fit_degree())?;
881 fit_source = RefinedFitSource::FullRefit;
882 refit_path = String::from("full-after-unreadable-representative");
883 refit_trials += 1;
884 refit_cost.full_refit = true;
885 continue;
886 }
887 Err(error) => return Err(error),
888 };
889 // An ACCEPTED representative candidate is judged on more than its
890 // mid-spans: both carriers must read finite at every mid-span and at
891 // the quarter points of the curve's own knot spans, and its worst is
892 // the worse of the two reads (sampled, projector upper readings; the
893 // carrier target is unchanged). An unreadable read never passes: the
894 // section takes the original full refit of the same run instead.
895 if representative_fit {
896 let interior = knot_interior_carrier_upper(&fit.curve, first, second);
897 let midspans = midspan_carrier_upper(&fit, first, second);
898 if interior.is_finite() && midspans.is_finite() {
899 worst = worst.max(interior).max(midspans);
900 } else {
901 representative_fit = false;
902 seeded_refit = false;
903 let maximum_points = section.len().min(MAX_FIT_STATIONS);
904 fit = fit_polyline_of_degree(section, fit_tolerance, maximum_points, local_fit, section_fit_degree())?;
905 fit_source = RefinedFitSource::FullRefit;
906 refit_path = String::from("full-after-unreadable-representative");
907 refit_trials += 1;
908 refit_cost.full_refit = true;
909 continue;
910 }
911 }
912 if let Some(path) = &round_census {
913 use std::io::Write;
914 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
915 let _ = writeln!(
916 file,
917 "section-refine round: head ({:.9}, {:.9}, {:.9}) tail ({:.9}, {:.9}, {:.9}) round {rounds} fit {refit_path} trials {refit_trials} retry_ms {retry_ms:.3} kept {} seeds {refit_seeds} seeds_kept {refit_seeds_kept} seeds_outside_pool {refit_seeds_outside_pool} seed_min_chord {refit_seed_min_chord:.3e} seed_min_pair {refit_seed_min_pair} rejected_march_simplified {} rejected_march_conditioned {} rejected_insert_simplified {} rejected_insert_conditioned {} pool {refit_pool} pool_discarded {refit_pool_discarded} pool_floor {refit_pool_floor:.3e} exit {:?} deviation {:.3e} floor {:.3e} run {} worst {:.3e} missing {} inserts_offered {} representatives {} witnesses {} candidate_fits {} candidate_full_refit {} candidate_end_offsets {:.3e}/{:.3e} seed_role {}",
918 head.x, head.y, head.z, tail.x, tail.y, tail.z,
919 fit.kept.len(),
920 refit_rejected[0], refit_rejected[1], refit_rejected[2], refit_rejected[3], fit.report.exit, fit.report.deviation, fit.report.floor, section.len(), worst, missing, inserts.len(),
921 refit_cost.representatives, refit_cost.witnesses, refit_cost.candidate_fits, refit_cost.full_refit,
922 refit_cost.start_offset, refit_cost.end_offset,
923 // On the representative path the seeds counted above are
924 // WITNESSES (not forced); on the seeded path they are forced.
925 if original_consistent { "witness" } else { "forced" }
926 );
927 // Trace only: where the full refit's band-raised floor comes
928 // from on THIS run, and whether the run's own points are on
929 // the carriers (input-consistent) or not. The band is
930 // `dropped_band`'s reading (largest distance of a point the
931 // pool drops from the segment between its surviving
932 // neighbours); its point is classified as simplified away or
933 // conditioned away, with its carrier distance and its gap to
934 // the nearest run neighbour. Projector distances are upper
935 // readings.
936 let pool = crate::fit::polyline_station_pool(section, fit_tolerance);
937 let simplified = crate::fit::simplify_polyline_indices(section, fit_tolerance);
938 let mut band = (0.0f64, usize::MAX);
939 for pair in pool.windows(2) {
940 for index in pair[0] + 1..pair[1] {
941 let off = distance_to_segment_sq(section[index], section[pair[0]], section[pair[1]]).sqrt();
942 if off > band.0 {
943 band = (off, index);
944 }
945 }
946 }
947 let carrier = |point: Vec3| -> f64 {
948 let a = project_point_to_surface(first, point).map_or(f64::NAN, |projection| projection.distance);
949 let b = project_point_to_surface(second, point).map_or(f64::NAN, |projection| projection.distance);
950 if a.is_nan() || b.is_nan() { f64::NAN } else { a.max(b) }
951 };
952 let mut run_carrier = 0.0f64;
953 for point in section.iter() {
954 let off = carrier(*point);
955 run_carrier = if off.is_nan() || run_carrier.is_nan() { f64::NAN } else { run_carrier.max(off) };
956 }
957 let (kind, band_carrier, gap) = if band.1 == usize::MAX {
958 ("none", f64::NAN, f64::NAN)
959 } else {
960 let index = band.1;
961 let kind = if simplified.binary_search(&index).is_ok() { "conditioned" } else { "simplified" };
962 let mut gap = f64::INFINITY;
963 if index > 0 {
964 gap = gap.min(section[index].sub(section[index - 1]).length());
965 }
966 if index + 1 < section.len() {
967 gap = gap.min(section[index + 1].sub(section[index]).length());
968 }
969 (kind, carrier(section[index]), gap)
970 };
971 let _ = writeln!(
972 file,
973 "section-refine band: head ({:.9}, {:.9}, {:.9}) tail ({:.9}, {:.9}, {:.9}) round {rounds} run {} pool {} tolerance {:.3e} band {:.3e} band_index {} of {} band_kind {kind} band_point_carrier_upper {band_carrier:.3e} band_point_gap {gap:.3e} run_carrier_upper {run_carrier:.3e}",
974 head.x, head.y, head.z, tail.x, tail.y, tail.z,
975 section.len(), pool.len(), fit_tolerance, band.0,
976 if band.1 == usize::MAX { -1 } else { band.1 as i64 }, section.len()
977 );
978 }
979 }
980 if rounds == 0 {
981 unrefined = worst;
982 }
983 // The rollback path's COMMON reading, only once an accepted
984 // representative round has run and only on a run the refit can hold
985 // (<= MAX_FIT_STATIONS): both carriers, finite, at the fit's mid-spans
986 // and its own knot-span quarter points, for EVERY state compared
987 // (representative, standing or full alike). One snapshot, the fit and
988 // the run it was measured on as one coherent pair: its payload is one
989 // PolylineFit and at most MAX_FIT_STATIONS points. Off this path
990 // nothing is read or cloned.
991 let mut current_common = f64::NAN;
992 if representative_ran && section.len() <= MAX_FIT_STATIONS {
993 let interior = knot_interior_carrier_upper(&fit.curve, first, second);
994 let midspans = midspan_carrier_upper(&fit, first, second);
995 current_common = if interior.is_finite() && midspans.is_finite() { interior.max(midspans) } else { f64::NAN };
996 keep_best_state(&mut best_state, || (fit.clone(), section.clone()), current_common);
997 }
998 if worst <= floor {
999 receipt.returned = Some(fit_source);
1000 return Ok((fit, worst, unrefined, rounds, MidspanExit::Converged));
1001 }
1002 if rounds == 0 && worst <= gate {
1003 receipt.returned = Some(fit_source);
1004 return Ok((fit, worst, unrefined, rounds, MidspanExit::BelowGate));
1005 }
1006 if let Some((earlier, earlier_worst, earlier_missing)) = previous.take() {
1007 // A round that did not earn its keep is discarded whole: the fit
1008 // of the last round that did stands (on a corner, the unrefined
1009 // fit), not whichever of the two happens to read a smaller max.
1010 if worst > earlier_worst * SECTION_MIDSPAN_PROGRESS && missing >= earlier_missing {
1011 // A SEEDED round that did not earn its keep is refitted once
1012 // with the original full refit of the same denser run, and
1013 // that is judged against the same earlier round before the
1014 // section stops. The seeded fit keeps fewer stations; on the
1015 // helmet's 2036x3009 section that gave back the whole of the
1016 // full refit's gain (6.335e-6 stood where 5.596e-7 was
1017 // reached). The full refit is the round the refiner made
1018 // before the seeded entry existed; only a round that fails
1019 // both ends the section, exactly as before.
1020 if seeded_refit {
1021 seeded_refit = false;
1022 let maximum_points = section.len().min(MAX_FIT_STATIONS);
1023 let retry_started = round_census.is_some().then(Instant::now);
1024 fit = fit_polyline_of_degree(section, fit_tolerance, maximum_points, local_fit, section_fit_degree())?;
1025 fit_source = RefinedFitSource::FullRefit;
1026 // Trace: one more fit for the SAME round, its cost, and no
1027 // seed counters carried over from the rejected seeded fit.
1028 refit_path = String::from("full-after-seeded-stall");
1029 // Every fit this round made: the candidate's (or the seeded
1030 // call's one) plus this retry.
1031 refit_trials += 1;
1032 retry_ms = retry_started.map_or(0.0, |started| started.elapsed().as_secs_f64() * 1e3);
1033 refit_seeds = 0;
1034 refit_seeds_kept = 0;
1035 refit_seeds_outside_pool = 0;
1036 refit_seed_min_chord = f64::NAN;
1037 refit_seed_min_pair = "n/a";
1038 refit_rejected = [0; 4];
1039 representative_fit = false;
1040 // Keep the candidate's own counts; the retry is the full refit.
1041 refit_cost.full_refit = true;
1042 previous = Some((earlier, earlier_worst, earlier_missing));
1043 continue;
1044 }
1045 receipt.returned = Some(previous_source);
1046 return Ok((earlier, earlier_worst, unrefined, rounds, MidspanExit::NoProgress));
1047 }
1048 }
1049 if inserts.is_empty() {
1050 if let Some(((kept_fit, kept_run), kept_common)) = take_rollback(&mut best_state, current_common, representative_ran) {
1051 *section = kept_run;
1052 receipt.returned = Some(RefinedFitSource::Rollback);
1053 return Ok((kept_fit, kept_common, unrefined, rounds, MidspanExit::Stalled));
1054 }
1055 receipt.returned = Some(fit_source);
1056 return Ok((fit, worst, unrefined, rounds, MidspanExit::Stalled));
1057 }
1058 if rounds >= SECTION_MIDSPAN_ROUNDS {
1059 if let Some(((kept_fit, kept_run), kept_common)) = take_rollback(&mut best_state, current_common, representative_ran) {
1060 *section = kept_run;
1061 receipt.returned = Some(RefinedFitSource::Rollback);
1062 return Ok((kept_fit, kept_common, unrefined, rounds, MidspanExit::RoundBudget));
1063 }
1064 receipt.returned = Some(fit_source);
1065 return Ok((fit, worst, unrefined, rounds, MidspanExit::RoundBudget));
1066 }
1067 if section.len() + inserts.len() > MAX_FIT_STATIONS {
1068 if let Some(((kept_fit, kept_run), kept_common)) = take_rollback(&mut best_state, current_common, representative_ran) {
1069 *section = kept_run;
1070 receipt.returned = Some(RefinedFitSource::Rollback);
1071 return Ok((kept_fit, kept_common, unrefined, rounds, MidspanExit::StationCeiling));
1072 }
1073 receipt.returned = Some(fit_source);
1074 return Ok((fit, worst, unrefined, rounds, MidspanExit::StationCeiling));
1075 }
1076 // Insert each refined point into the run between its span's stations,
1077 // on the nearest segment of the run there. Positions are located
1078 // against the run BEFORE any insertion of this round, then applied
1079 // from the back so earlier indices stay valid. The kept stations are
1080 // found by a FORWARD search from the previous one: a closed section
1081 // ends on its first station, and a search from the start would put
1082 // the last span's far end at index 0 and skip that span forever.
1083 let station_index = |point: Vec3, from: usize| -> Option<usize> {
1084 section[from..]
1085 .iter()
1086 .position(|p| p.sub(point).length() <= 1e-15 * (1.0 + point.length()))
1087 .map(|offset| from + offset)
1088 };
1089 let mut placed: Vec<(usize, Vec3)> = Vec::new();
1090 for (span, point) in inserts {
1091 let Some(lo) = station_index(fit.kept[span], 0) else {
1092 continue;
1093 };
1094 let Some(hi) = station_index(fit.kept[span + 1], lo + 1) else {
1095 continue;
1096 };
1097 if hi <= lo {
1098 continue;
1099 }
1100 if section[lo..=hi].iter().any(|p| p.sub(point).length() <= floor) {
1101 continue; // already a station: inserting it again refines nothing
1102 }
1103 let mut best = (lo, f64::INFINITY);
1104 for index in lo..hi {
1105 let d2 = distance_to_segment_sq(point, section[index], section[index + 1]);
1106 if d2 < best.1 {
1107 best = (index, d2);
1108 }
1109 }
1110 placed.push((best.0 + 1, point));
1111 }
1112 if placed.is_empty() {
1113 if let Some(((kept_fit, kept_run), kept_common)) = take_rollback(&mut best_state, current_common, representative_ran) {
1114 *section = kept_run;
1115 receipt.returned = Some(RefinedFitSource::Rollback);
1116 return Ok((kept_fit, kept_common, unrefined, rounds, MidspanExit::Stalled));
1117 }
1118 receipt.returned = Some(fit_source);
1119 return Ok((fit, worst, unrefined, rounds, MidspanExit::Stalled));
1120 }
1121 placed.sort_by(|a, b| b.0.cmp(&a.0));
1122 for (at, point) in placed {
1123 section.insert(at, point);
1124 inserted_points.push(point);
1125 }
1126 // The refit keeps the stations the current fit kept and every point
1127 // inserted so far as SEEDS, and grows only where it is measured to
1128 // miss (`fit_polyline_seeded_with_path`). It is no longer the whole denser run
1129 // from the run's own count: that interpolated every march station the
1130 // standing fit had dropped as well, 3x the controls on the skew-cylinder
1131 // loop for the same carrier floor. The ceiling is the one the full refit
1132 // derives from the same `maximum_points`, and a seeded fit that cannot
1133 // meet the floor, or cannot be read, IS the full refit. The carrier
1134 // measure above still decides every round.
1135 let same = |a: &Vec3, b: &Vec3| a.x.to_bits() == b.x.to_bits() && a.y.to_bits() == b.y.to_bits() && a.z.to_bits() == b.z.to_bits();
1136 let mut seeds = Vec::with_capacity(fit.kept.len() + inserted_points.len());
1137 // The previous fit's kept stations alone: the representative candidate's
1138 // starting set (inserts are its witnesses, not forced stations).
1139 let mut kept_seeds = Vec::with_capacity(fit.kept.len());
1140 // The refiner's own inserted true-carrier points: the representative
1141 // candidate's TIGHT witnesses, held to this refiner's carrier floor.
1142 let mut insert_indices = Vec::with_capacity(inserted_points.len());
1143 let mut next_kept = 0usize;
1144 for (index, point) in section.iter().enumerate() {
1145 // The kept stations are a subsequence of the run, in run order
1146 // (insertion only adds points), so a closed run's two equal ends
1147 // each match their own station.
1148 // An inserted point is a tight witness whether or not the previous
1149 // fit also kept it as a station (membership is independent).
1150 let inserted = inserted_points.iter().any(|candidate| same(point, candidate));
1151 if inserted {
1152 insert_indices.push(index);
1153 }
1154 if next_kept < fit.kept.len() && same(point, &fit.kept[next_kept]) {
1155 seeds.push(index);
1156 kept_seeds.push(index);
1157 next_kept += 1;
1158 } else if inserted {
1159 seeds.push(index);
1160 }
1161 }
1162 let maximum_points = section.len().min(MAX_FIT_STATIONS);
1163 // Every standing station must be found as a seed; if the pass missed
1164 // one, the seeded refit would silently lose it, so the section takes the
1165 // original full refit instead.
1166 refit_trials = 1;
1167 retry_ms = 0.0;
1168 let refit_source;
1169 let refit = if next_kept == fit.kept.len() {
1170 let (refit, path) = if original_consistent {
1171 let (refit, path, cost) = crate::fit::fit_polyline_representatives_with_witnesses(
1172 section,
1173 &kept_seeds,
1174 &insert_indices,
1175 floor,
1176 fit_tolerance,
1177 maximum_points,
1178 local_fit,
1179 section_fit_degree(),
1180 )?;
1181 // MIXED UNITS, named as such: the candidate's own
1182 // `fit_station_set` interpolations plus one per call of the
1183 // full refit `fit_polyline_of_degree`, which may itself run
1184 // several ladder and recovery interpolations that are NOT
1185 // counted here. Whole-job cost is the matched timing.
1186 refit_cost = cost;
1187 refit_trials = cost.candidate_fits + usize::from(cost.full_refit);
1188 (refit, path)
1189 } else {
1190 refit_cost = crate::fit::RepresentativeCost::default();
1191 crate::fit::fit_polyline_seeded_with_path(
1192 section,
1193 &seeds,
1194 fit_tolerance,
1195 maximum_points,
1196 local_fit,
1197 section_fit_degree(),
1198 )?
1199 };
1200 // Only an ACCEPTED seeded fit can be retried in full: a fallback
1201 // already is the full refit, and retrying it would repeat it.
1202 seeded_refit = matches!(path, crate::fit::SeededPath::Accepted { .. });
1203 representative_fit = original_consistent && seeded_refit;
1204 refit_source = if original_consistent { RefinedFitSource::Representatives(path) } else { RefinedFitSource::Seeded(path) };
1205 representative_ran |= representative_fit;
1206 if round_census.is_some() {
1207 refit_path = if original_consistent { format!("representatives-call:{path:?}") } else { format!("seeded-call:{path:?}") };
1208 }
1209 refit
1210 } else {
1211 seeded_refit = false;
1212 representative_fit = false;
1213 refit_cost = crate::fit::RepresentativeCost::default();
1214 refit_path = String::from("full-unmatched");
1215 refit_source = RefinedFitSource::FullRefit;
1216 fit_polyline_of_degree(section, fit_tolerance, maximum_points, local_fit, section_fit_degree())?
1217 };
1218 if round_census.is_some() {
1219 // Trace only: how many seeds the refit kept. This does NOT tell an
1220 // accepted seeded fit from the fallback (a full refit can keep
1221 // every seed too); the returned `SeededPath`, traced as `fit`, does.
1222 refit_seeds = seeds.len();
1223 refit_seeds_kept = seeds.iter().filter(|&&index| refit.kept.iter().any(|kept| same(kept, §ion[index]))).count();
1224 // Seeds the full refit's pool would NOT interpolate (simplified
1225 // or conditioned away): forced stations only the seeded fit keeps.
1226 let pool = crate::fit::polyline_station_pool(section, fit_tolerance);
1227 refit_pool = pool.len();
1228 refit_pool_discarded = section.len() - pool.len();
1229 refit_pool_floor = crate::fit::polyline_station_pool_floor(section, fit_tolerance);
1230 refit_seeds_outside_pool = seeds.iter().filter(|index| !pool.contains(index)).count();
1231 // Seeds are in run order; the closest consecutive pair is what a
1232 // near-duplicate conditioning floor would act on.
1233 refit_seed_min_chord = seeds
1234 .windows(2)
1235 .map(|pair| section[pair[1]].sub(section[pair[0]]).length())
1236 .fold(f64::INFINITY, f64::min);
1237 let is_insert = |index: usize| inserted_points.iter().any(|inserted| same(inserted, §ion[index]));
1238 refit_seed_min_pair = seeds
1239 .windows(2)
1240 .min_by(|a, b| {
1241 section[a[1]].sub(section[a[0]]).length().total_cmp(§ion[b[1]].sub(section[b[0]]).length())
1242 })
1243 .map_or("n/a", |pair| match (is_insert(pair[0]), is_insert(pair[1])) {
1244 (false, false) => "station-station",
1245 (false, true) => "station-insert",
1246 (true, false) => "insert-station",
1247 (true, true) => "insert-insert",
1248 });
1249 // Every rejected seed, classified. `station_pool` starts from this
1250 // same simplification, so a non-member that is not in it was
1251 // simplified away, and one that is in it was conditioned away.
1252 let simplified = crate::fit::simplify_polyline_indices(section, fit_tolerance);
1253 refit_rejected = [0; 4];
1254 for &index in seeds.iter().filter(|index| !pool.contains(index)) {
1255 let conditioned = simplified.binary_search(&index).is_ok();
1256 refit_rejected[2 * usize::from(is_insert(index)) + usize::from(conditioned)] += 1;
1257 }
1258 }
1259 previous = Some((std::mem::replace(&mut fit, refit), worst, missing));
1260 previous_source = std::mem::replace(&mut fit_source, refit_source);
1261 rounds += 1;
1262 }
1263}
1264use super::*;
1265use super::gate_census::{census_pair, fit_census_enabled};
1266
1267pub fn build_imprints(
1268 solid_a: &BrepSolid,
1269 solid_b: &BrepSolid,
1270 options: &ImprintOptions,
1271) -> Result<ImprintResultRecord, KernelRefusal> {
1272 build_imprints_impl(solid_a, solid_b, options, false, super::pcurve_cache::enabled())
1273}
1274
1275/// Imprint temporary open offset sheets. A section riding one sheet's trim
1276/// still cuts the other sheet when no boundary copy supplied that section.
1277pub(crate) fn build_carrier_imprints(
1278 solid_a: &BrepSolid,
1279 solid_b: &BrepSolid,
1280 options: &ImprintOptions,
1281) -> Result<ImprintResultRecord, KernelRefusal> {
1282 build_imprints_impl(solid_a, solid_b, options, true, super::pcurve_cache::enabled())
1283}
1284
1285/// `BREP_IMPRINT_PLANE_POINT_SKIP` (lane X row 5): skip the march for two
1286/// exact parallelogram faces whose contact is one point
1287/// ([`super::support::planar_point_contact`]) once their seeds are accepted.
1288/// On by default; `0`/`off` disables it.
1289pub(super) fn plane_point_skip_enabled() -> bool {
1290 let enabled = !matches!(std::env::var("BREP_IMPRINT_PLANE_POINT_SKIP").as_deref(), Ok("0") | Ok("off"));
1291 enabled
1292}
1293
1294thread_local! {
1295 static PLANE_POINT_SKIPS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1296}
1297
1298/// Pairs skipped by [`plane_point_skip_enabled`] on this thread since the last call.
1299pub(super) fn take_plane_point_skips() -> u64 {
1300 PLANE_POINT_SKIPS.with(|count| count.replace(0))
1301}
1302
1303
1304pub(super) fn build_imprints_impl(
1305 solid_a: &BrepSolid,
1306 solid_b: &BrepSolid,
1307 options: &ImprintOptions,
1308 open_carriers: bool,
1309 pcurve_cache: bool,
1310) -> Result<ImprintResultRecord, KernelRefusal> {
1311 let classify_before = crate::pair_classification::classify_reads();
1312 let setup_started = std::env::var_os("BREP_PROFILE").is_some().then(Instant::now);
1313 let _pcurve_cache = super::pcurve_cache::FitCacheScope::open(pcurve_cache);
1314 let reuse_whole_miss = open_carriers &&
1315 std::env::var("BREP_OFFSET_IMPRINT_READINGS").map_or(true, |v| v != "0" && v != "off");
1316 let mut section_evidence = false;
1317 let edges = edge_map(solid_a, solid_b);
1318 let mut builder = ImprintBuilder {
1319 open_carriers,
1320 edges,
1321 tolerance: options.tolerance,
1322 barrier_edges: HashSet::default(),
1323 overlap_ridden_edges: HashSet::default(),
1324 scale: solid_scale(solid_a).max(solid_scale(solid_b)),
1325 vertices: Vec::new(),
1326 vertex_radii: HashMap::default(),
1327 vertex_sources: HashMap::default(),
1328 pieces: Vec::new(),
1329 by_face: HashMap::default(),
1330 edge_splits: HashMap::default(),
1331 marched_pieces: HashSet::default(),
1332 emitted_pcurves: HashSet::default(),
1333 exact_pcurve_check: false,
1334 next_id: 1,
1335 };
1336 let hermite_lane = hermite_sections(options);
1337 let exact_iso = exact_iso_pcurves(options);
1338 let (ordered_starts, consume_starts, tree_seeds) = march_starts(options);
1339 builder.exact_pcurve_check = exact_iso == ExactIsoPcurves::Check;
1340 // The Hermite lane needs the march's span test; with no budget asked for
1341 // it carries the one the catalogue names, the pair's `intersection_fit`.
1342 let march_span_budget = span_budget(options, builder.scale)
1343 .or_else(|| hermite_lane.then(|| crate::KernelTolerances::for_scale(builder.scale, options.tolerance).intersection_fit));
1344 let mut first_faces = faces(solid_a, 0);
1345 let mut second_faces = faces(solid_b, 1);
1346 // Per-face trim-window carriers, computed once: tighter BVH bounds, and
1347 // the seed/march stages walk the window instead of the full carrier
1348 // (identical geometry and parameterization inside the window).
1349 //
1350 // A window LIFTED across a closed direction's seam (`restricted_carrier`)
1351 // is the exception to "same parameterization": it is the trim's frame, a
1352 // period off the carrier's on one side. Its face carries it as its
1353 // `chart`, and every uv the imprint reads for that face — the clip's
1354 // containment, the sections' pcurves, a seed's normal — is read there.
1355 let first_restricted: Vec<Option<MarchWindow>> = first_faces
1356 .iter()
1357 .map(|tagged| restricted_carrier(tagged.face))
1358 .collect();
1359 let second_restricted: Vec<Option<MarchWindow>> = second_faces
1360 .iter()
1361 .map(|tagged| restricted_carrier(tagged.face))
1362 .collect();
1363 for (tagged, window) in first_faces.iter_mut().zip(&first_restricted) {
1364 tagged.lifted = window.as_ref().filter(|window| window.lifted).map(|window| &window.surface);
1365 }
1366 for (tagged, window) in second_faces.iter_mut().zip(&second_restricted) {
1367 tagged.lifted = window.as_ref().filter(|window| window.lifted).map(|window| &window.surface);
1368 }
1369 let first_faces = first_faces;
1370 let second_faces = second_faces;
1371 // The lifted charts, by key, for the post-passes that rebuild a pcurve
1372 // after the mint (junction canonicalization, the origin dissolve, the
1373 // truncation bridge). A face absent here is read on its own carrier.
1374 let charts: FaceCharts<'_> = first_faces
1375 .iter()
1376 .chain(second_faces.iter())
1377 .filter_map(|tagged| tagged.lifted.map(|surface| (tagged.key(), surface)))
1378 .collect();
1379 let first_bounds = first_faces
1380 .iter()
1381 .zip(&first_restricted)
1382 .map(|(tagged, restricted)| {
1383 face_bounds(
1384 tagged.face,
1385 restricted.as_ref().map(|window| &window.surface),
1386 &builder.edges,
1387 tagged.operand,
1388 options.tolerance,
1389 )
1390 })
1391 .collect::<Result<Vec<_>, _>>()?;
1392 let second_bounds = second_faces
1393 .iter()
1394 .zip(&second_restricted)
1395 .map(|(tagged, restricted)| {
1396 face_bounds(
1397 tagged.face,
1398 restricted.as_ref().map(|window| &window.surface),
1399 &builder.edges,
1400 tagged.operand,
1401 options.tolerance,
1402 )
1403 })
1404 .collect::<Result<Vec<_>, _>>()?;
1405 let second_bvh = Bvh::build(&second_bounds);
1406 // Per-face classifier data and edge lists/subcurves, computed once and
1407 // reused across every pair the face participates in.
1408 let first_classify = first_faces
1409 .iter()
1410 .map(|tagged| SurfaceClassifyData::memoized(&tagged.face.surface, options.tolerance))
1411 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
1412 let second_classify = second_faces
1413 .iter()
1414 .map(|tagged| SurfaceClassifyData::memoized(&tagged.face.surface, options.tolerance))
1415 .collect::<Result<Vec<_>, _>>().or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
1416 let mut face_edge_lists: HashMap<FaceKey, Vec<&EdgeRecord>> = HashMap::default();
1417 for tagged in first_faces.iter().chain(second_faces.iter()) {
1418 face_edge_lists.insert(tagged.key(), face_edges(*tagged, &builder.edges)?);
1419 }
1420 let mut subcurves: HashMap<(u8, u64), NurbsCurve> = HashMap::default();
1421 for (&key, edge) in &builder.edges {
1422 if !edge.degenerate {
1423 // Failures fall through: the use sites recompute and surface the
1424 // original error exactly where the uncached code did.
1425 if let Ok(curve) = edge_subcurve(edge) {
1426 subcurves.insert(key, curve);
1427 }
1428 }
1429 }
1430 let cached_subcurve = |operand: u8, edge: &EdgeRecord| -> Result<NurbsCurve, KernelRefusal> {
1431 match subcurves.get(&(operand, edge.id)) {
1432 Some(curve) => Ok(curve.clone()),
1433 None => edge_subcurve(edge),
1434 }
1435 };
1436 if let Some(started) = setup_started { eprintln!("imprint.setup ms={:.3} faces={} classify_reads={}", started.elapsed().as_secs_f64()*1000.0, first_faces.len()+second_faces.len(), crate::pair_classification::classify_reads()-classify_before); }
1437 let mut profile = ImprintProfile::new();
1438 let mut march_swap_rescues = 0_u64;
1439 let mut clip_swap_rescues = 0_u64;
1440 // `imprint.fits`: the pcurve fits the pairs record, read through a nested
1441 // scope that folds into the operation's own ledger on close.
1442 let pairs_fits = profile.enabled.then(crate::PcurveFitScope::open);
1443 let mut tangent_nodes: Vec<Vec3> = Vec::new();
1444 let mut tangent_sections: Vec<TangentSectionRecord> = Vec::new();
1445 // Planar tangent runs to IMPRINT once every pair has minted its pieces
1446 // (`ImprintOptions::tangent_curve_imprint`): (section index, run, pair).
1447 let mut tangent_imprint_candidates: Vec<(usize, Vec<Vec3>, TaggedFace<'_>, TaggedFace<'_>)> = Vec::new();
1448 let mut cosurface_pairs: Vec<(FaceKey, FaceKey)> = Vec::new();
1449 let mut deferred_boundary_spans = Vec::new();
1450 let mut paired = Vec::new();
1451 for (first_index, first) in first_faces.iter().enumerate() {
1452 let first = *first;
1453 paired.clear();
1454 second_bvh.overlapping(
1455 first_bounds[first_index],
1456 options.tolerance * 100.0,
1457 &mut paired,
1458 );
1459 paired.sort_unstable();
1460 let debug_pairs = std::env::var("BREP_DEBUG_PAIRS").is_ok();
1461 if debug_pairs {
1462 eprintln!(
1463 "bvh first_face={} -> {} candidate pairs {:?}",
1464 first.face.id,
1465 paired.len(),
1466 paired
1467 .iter()
1468 .map(|&index| second_faces[index].face.id)
1469 .collect::<Vec<_>>()
1470 );
1471 }
1472 for &second_index in &paired {
1473 let second = second_faces[second_index];
1474 profile.pairs += 1;
1475 // Set when this pair's section passes through an isolated TANGENT
1476 // NODE inside both trims (see the classification below). The march
1477 // is attempted anyway — that is the whole point — but a marcher
1478 // that cannot get through a node must refuse in the tangent-node
1479 // class naming the node, not leak its own step-budget message.
1480 let mut tangent_node: Option<Vec3> = None;
1481 // The pair's branches came from the sub-gate re-march below: a
1482 // run end that stalls in the tangency band then lands on a trim
1483 // edge the carriers share exactly as a tangent-node pair's does.
1484 let mut grazing_march = false;
1485 let mut lap_start = None;
1486 profile.lap(&mut lap_start);
1487 let pair_classification = classify_surface_pair_cached(
1488 &first.face.surface,
1489 &first_classify[first_index],
1490 &second.face.surface,
1491 &second_classify[second_index],
1492 options.tolerance,
1493 PAIR_ANGULAR_TOLERANCE,
1494 ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
1495 profile.classify += profile.lap(&mut lap_start);
1496 if pair_classification.relation == SurfacePairRelation::Disjoint {
1497 if debug_pairs {
1498 eprintln!(
1499 "pair {}x{}: DISJOINT-cull sep={:.4e}",
1500 first.face.id, second.face.id, pair_classification.minimum_separation
1501 );
1502 }
1503 continue;
1504 }
1505 // Coincident carriers are never marched: their true intersection
1506 // is a 2D region, not a curve, so anything the marcher traces on
1507 // them is noise ("similar faces we do not intersect" — Golovanov
1508 // §6.2). The sampled classification catches coincident pairs
1509 // the strict reconstruction test misses (partial overlaps,
1510 // differing parameterizations); both route to the boundary-curve
1511 // exchange below.
1512 let is_cosurface = pair_classification.relation == SurfacePairRelation::Cosurface
1513 || cosurface_pair(&first.face.surface, &second.face.surface, options.tolerance)?;
1514 profile.cosurface += profile.lap(&mut lap_start);
1515 if is_cosurface {
1516 if debug_pairs {
1517 eprintln!("pair {}x{}: cosurface", first.face.id, second.face.id);
1518 }
1519 cosurface_pairs.push((first.key(), second.key()));
1520 for edge in &face_edge_lists[&second.key()] {
1521 if !edge.degenerate {
1522 builder.process_curve(
1523 cached_subcurve(second.operand, edge)?,
1524 first,
1525 second,
1526 &[first],
1527 &[first],
1528 false,
1529 None,
1530 )?;
1531 }
1532 }
1533 for edge in &face_edge_lists[&first.key()] {
1534 if !edge.degenerate {
1535 builder.process_curve(
1536 cached_subcurve(first.operand, edge)?,
1537 first,
1538 second,
1539 &[second],
1540 &[second],
1541 false,
1542 None,
1543 )?;
1544 }
1545 }
1546 profile.process_curve += profile.lap(&mut lap_start);
1547 continue;
1548 }
1549
1550 // Exact lanes discard sections riding an ordinary trim boundary:
1551 // that face already owns the edge, but the other face still needs
1552 // the overlapping span as a cut. Keep the existing whole-edge
1553 // exchange order, and append missing partial spans after all pairs
1554 // have run so duplicate sections retain their established IDs and
1555 // fragment names. Seams are retained by the exact lane itself.
1556 let planar_iso = planar_iso_section(
1557 &first.face.surface,
1558 &second.face.surface,
1559 options.tolerance,
1560 )?;
1561 profile.planar_iso += profile.lap(&mut lap_start);
1562 let analytic = crate::intersect_analytic_pair(
1563 &first.face.surface,
1564 &second.face.surface,
1565 options.tolerance,
1566 );
1567 profile.analytic += profile.lap(&mut lap_start);
1568 let exact_section = planar_iso.is_some() || analytic.is_some();
1569
1570 for (source, target) in [(first, second), (second, first)] {
1571 for edge in &face_edge_lists[&source.key()] {
1572 if edge.degenerate {
1573 continue;
1574 }
1575 let curve = cached_subcurve(source.operand, edge)?;
1576 let spans = curve_spans_on_face(
1577 curve.clone(), target.face, &face_edge_lists[&target.key()],
1578 options.tolerance, exact_section,
1579 )?;
1580 let seam = source.face.loops.iter().flat_map(|loop_record| &loop_record.coedges)
1581 .filter(|coedge| coedge.edge_id == edge.id).count() >= 2;
1582 if spans.is_empty() && exact_section && !seam {
1583 // The whole-only call above returned empty: its exact
1584 // curve/carrier reading was false. Nothing has mutated.
1585 let partial = if reuse_whole_miss {
1586 profile.whole_reading_reuses += 1;
1587 curve_spans_after_whole_miss(
1588 curve, target.face, &face_edge_lists[&target.key()], options.tolerance,
1589 )?
1590 } else {
1591 curve_spans_on_face(
1592 curve, target.face, &face_edge_lists[&target.key()], options.tolerance, false,
1593 )?
1594 };
1595 for span in partial {
1596 deferred_boundary_spans.push((span, first, second, target));
1597 }
1598 }
1599 for span in spans {
1600 builder.process_curve(span, first, second, &[target], &[target], false, None)?;
1601 }
1602 }
1603 }
1604 profile.lies_on += profile.lap(&mut lap_start);
1605 // CENSUS ONLY (`BREP_FIT_CENSUS=1`): the lane this pair takes under
1606 // the count-keyed and the geometry-keyed gates, and — when it is
1607 // marched here and answered exactly there — the point-set comparison
1608 // of the two, printed when `pair_census` is dropped. `imprint/gate_census.rs`.
1609 let mut pair_census = if fit_census_enabled() {
1610 census_pair(first, second, options.tolerance)?
1611 } else {
1612 None
1613 };
1614 if let Some(IsoSection { curve, pcurves }) = planar_iso {
1615 if debug_pairs {
1616 eprintln!("pair {}x{}: planar_iso", first.face.id, second.face.id);
1617 }
1618 // Slice 1: the exact pcurves ride along with the curve when
1619 // the lane is on (or checked). A face read in a lifted chart
1620 // declines: the iso line is in the carrier's own frame, and
1621 // the lifted chart's period is not the carrier's.
1622 let exact_pcurves = match (exact_iso, pcurves) {
1623 (ExactIsoPcurves::Off, _) => None,
1624 (_, Some(pcurves)) if first.lifted.is_none() && second.lifted.is_none() => {
1625 profile.exact_iso_sections += 1;
1626 Some(pcurves)
1627 }
1628 _ => {
1629 profile.exact_iso_declined += 1;
1630 None
1631 }
1632 };
1633 let lane_fits = profile.enabled.then(crate::PcurveFitScope::open);
1634 builder.process_curve(
1635 curve,
1636 first,
1637 second,
1638 &[first, second],
1639 &[first, second],
1640 true,
1641 exact_pcurves,
1642 )?;
1643 if let Some(scope) = lane_fits {
1644 profile.exact_lane_fits += scope.close().fits;
1645 }
1646 profile.process_curve += profile.lap(&mut lap_start);
1647 continue;
1648 }
1649
1650 // Recognized analytic pairs produce their exact intersection
1651 // curves (lines, circles, ellipses) directly — no marching, no
1652 // polyline fitting, no chord-sag drift. An empty result is a
1653 // proof of non-intersection and also skips the marcher. (Read
1654 // above, with the planar-iso curve.)
1655 if let Some(curves) = analytic {
1656 if debug_pairs {
1657 eprintln!(
1658 "pair {}x{}: analytic x{}",
1659 first.face.id,
1660 second.face.id,
1661 curves.len()
1662 );
1663 }
1664 // TANGENT-CURVE LANE. An exact lane's "provably empty" is a
1665 // proof about TRANSVERSE curves: a plane at exactly the
1666 // cylinder's radius from its axis has no generatrix pair, and
1667 // the pair would be dropped here without a word while the
1668 // carriers touch along a whole line. Where the classification
1669 // read the pair as near-tangent, the lane looks for the
1670 // extended contact and records it typed.
1671 if curves.is_empty()
1672 && options.tangent_curve_lane
1673 && pair_classification.relation == SurfacePairRelation::NearTangent
1674 {
1675 if let Some((mut section, run)) = tangent_curve_section(
1676 first, second, options, &first_faces, &second_faces, &face_edge_lists, debug_pairs,
1677 )? {
1678 if options.tangent_curve_imprint && section.planar && run.len() >= 2 {
1679 tangent_imprint_candidates.push((tangent_sections.len(), run, first, second));
1680 }
1681 tangent_sections.push(section);
1682 }
1683 }
1684 let lane_fits = profile.enabled.then(crate::PcurveFitScope::open);
1685 for curve in curves {
1686 builder.process_curve(
1687 curve,
1688 first,
1689 second,
1690 &[first, second],
1691 &[first, second],
1692 true,
1693 None,
1694 )?;
1695 }
1696 if let Some(scope) = lane_fits {
1697 profile.exact_lane_fits += scope.close().fits;
1698 }
1699 profile.process_curve += profile.lap(&mut lap_start);
1700 continue;
1701 }
1702
1703 // A pair whose every touching sample is TANGENTIAL cannot
1704 // contain a transverse intersection curve: the marcher would
1705 // walk the tangency band's noise, which neither closes nor
1706 // reaches a boundary (the distance-0 glue-extrude runaway).
1707 // Shared topology at a tangential contact comes from the
1708 // boundary-curve exchange above ("similar faces we do not
1709 // intersect" extended to tangential contacts — Golovanov §6.2;
1710 // where surfaces CROSS through a tangency, off-line samples
1711 // have non-parallel normals and the pair still marches).
1712 if pair_classification.relation == SurfacePairRelation::NearTangent
1713 && pair_classification.tangential_only
1714 {
1715 // The coarse 5×5 classifier can flag `tangential_only` off an
1716 // incidental tangential KISS between two curved carriers and
1717 // miss the transverse loop where they actually cross (two
1718 // overlapping tori: their tubes cross while their inner walls
1719 // just touch). Before honouring the skip, a GATED supplemental
1720 // detector (denser two-sided seeding, tangency-band seeds
1721 // rejected, trace-exhaustion swallowed, transverse-only
1722 // branches) checks whether a genuine transverse curve of
1723 // meaningful length lies inside BOTH trims.
1724 //
1725 // Such a pair's intersection is SINGULAR where a tangency
1726 // sits on it, and what the imprint can do about that depends
1727 // entirely on the SHAPE of the tangency — see
1728 // `imprint/tangent_contact.rs`. An isolated NODE (two branches
1729 // crossing, as every equal-radius pair produces) is already
1730 // assembled correctly by the ordinary march plus the 2D
1731 // arrangement's pinch carving; an EXTENDED contact along a
1732 // whole curve has no section to imprint at all and tears the
1733 // shell. Both are classified below and only the second is
1734 // refused. Skipping either silently would DROP the
1735 // intersection (torus∪torus double-counts; torus−torus removes
1736 // nothing — the two structural bugs the semantic oracle
1737 // found), so nothing here ever falls through quietly.
1738 let first_march = first_restricted[first_index]
1739 .as_ref()
1740 .map(|window| &window.surface)
1741 .unwrap_or(&first.face.surface);
1742 let second_march = second_restricted[second_index]
1743 .as_ref()
1744 .map(|window| &window.surface)
1745 .unwrap_or(&second.face.surface);
1746 let supplemental = intersect_surfaces_supplemental(
1747 first_march,
1748 second_march,
1749 &SurfaceIntersectionOptions {
1750 tolerance: options.tolerance,
1751 maximum_step: options.maximum_ssi_step,
1752 span_budget: march_span_budget,
1753 ..Default::default()
1754 },
1755 ).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
1756 let mut dropped_length = 0.0f64;
1757 let mut clipped: Vec<Vec<Vec3>> = Vec::new();
1758 for branch in &supplemental {
1759 for run in clip_branch_to_trims(&branch.points, first, second)? {
1760 if run.len() < 2 {
1761 continue;
1762 }
1763 let length: f64 = run
1764 .windows(2)
1765 .map(|pair| pair[1].sub(pair[0]).length())
1766 .sum();
1767 dropped_length = dropped_length.max(length);
1768 clipped.push(run);
1769 }
1770 }
1771 if dropped_length <= options.tolerance * 100.0 {
1772 if debug_pairs {
1773 eprintln!(
1774 "pair {}x{}: tangential-only contact, march skipped",
1775 first.face.id, second.face.id
1776 );
1777 }
1778 // TANGENT-CURVE LANE: the skip is right for a transverse
1779 // march, which has nothing to walk here, but the contact
1780 // may still be a CURVE inside both trims (the same
1781 // generatrix on a carrier no exact lane recognises).
1782 // Recognise and record it, typed, rather than lose it.
1783 if options.tangent_curve_lane {
1784 if let Some((mut section, run)) = tangent_curve_section(
1785 first, second, options, &first_faces, &second_faces, &face_edge_lists, debug_pairs,
1786 )? {
1787 if options.tangent_curve_imprint && section.planar && run.len() >= 2 {
1788 tangent_imprint_candidates.push((tangent_sections.len(), run, first, second));
1789 }
1790 tangent_sections.push(section);
1791 }
1792 }
1793 continue;
1794 }
1795 // A transverse curve EXISTS. What would make it unimprintable is
1796 // a tangent NODE on it — and the node need not lie inside the
1797 // trims. Two equal-radius pipe arms are tangent to each other
1798 // where their axes' common perpendicular leaves the junction,
1799 // and a joint ball wider than the arms trims that crotch off
1800 // both faces; what is left inside the trims is an ordinary
1801 // crossing. So the question is not "does a transverse curve
1802 // exist" but "does the curve INSIDE BOTH TRIMS reach a
1803 // tangency". Only the latter is the checkerboard case.
1804 let mut tangency_inside_trims = false;
1805 let mut tangency_witness = Vec3::default();
1806 'clipped: for run in &clipped {
1807 for (index, &point) in run.iter().enumerate() {
1808 if pair_normals_parallel_at(first, second, point)? {
1809 tangency_inside_trims = true;
1810 tangency_witness = point;
1811 if debug_pairs {
1812 eprintln!(
1813 "pair {}x{}: TANGENCY at run index {}/{} ({:.6},{:.6},{:.6}) dropped_len={:.4e}",
1814 first.face.id, second.face.id, index, run.len(),
1815 point.x, point.y, point.z, dropped_length
1816 );
1817 }
1818 break 'clipped;
1819 }
1820 }
1821 }
1822 if tangency_inside_trims {
1823 // WHAT SHAPE is the tangency? The witness is only a marched
1824 // sample within the transverse-seed angular gate, so it is
1825 // REFINED onto the contact before being classified — at the
1826 // raw witness a G1 cylinder/torus join and a genuine
1827 // equal-radius node are eighteen-fold apart, which would be
1828 // a band; at the refined contact they are thirty orders
1829 // apart, which is a rank question. See
1830 // `imprint/tangent_contact.rs`.
1831 let contact = classify_tangent_contact(
1832 &first.face.surface,
1833 &second.face.surface,
1834 tangency_witness,
1835 options.tolerance,
1836 )?;
1837 if debug_pairs {
1838 eprintln!(
1839 "pair {}x{}: contact classification {:?}",
1840 first.face.id, second.face.id, contact
1841 );
1842 }
1843 match contact {
1844 // An isolated node: the section is a curve everywhere
1845 // but that one point, so the ordinary march below runs
1846 // and the 2D arrangement carves the pinch on both
1847 // faces. `tangent_node` records it so a march that
1848 // cannot get through still refuses in THIS class rather
1849 // than leaking the marcher's own message.
1850 Some(contact) if contact.is_isolated_node() => {
1851 tangent_node = Some(contact.point);
1852 tangent_nodes.push(contact.point);
1853 }
1854 // An extended contact (rank-deficient) or a witness we
1855 // could not refine onto any contact at all (None — so
1856 // nothing is proven and the pre-classification refusal
1857 // stands). Neither has a section curve the imprint can
1858 // represent.
1859 other => {
1860 let shape = match other {
1861 Some(contact) => contact.describe(),
1862 None => format!(
1863 "the tangency near ({:.6},{:.6},{:.6}) could not be refined \
1864 onto a contact, so its shape is unproven",
1865 tangency_witness.x, tangency_witness.y, tangency_witness.z
1866 ),
1867 };
1868 return Err(KernelRefusal::new(
1869 RefusalClass::TangentNodeSingularity,
1870 KernelStage::Intersect,
1871 format!(
1872 "boolean: unsupported singular/tangent-node surface intersection \
1873 between faces {} and {}: {shape}",
1874 first.face.id, second.face.id
1875 ),
1876 ));
1877 }
1878 }
1879 }
1880 if debug_pairs && tangent_node.is_none() {
1881 // Transverse the whole way inside both trims: the coarse
1882 // 5x5 classifier only saw the tangency the trims cut away.
1883 eprintln!(
1884 "pair {}x{}: classified tangential-only, but the curve inside both \
1885 trims is transverse ({dropped_length:.4}) — marching",
1886 first.face.id, second.face.id
1887 );
1888 }
1889 }
1890
1891 // The trim-window carriers: same surface and parameterization
1892 // over the window, so hit (u, v) values remain valid on the
1893 // originals; out-of-window intersections could never survive
1894 // clip_branch_to_trims and are not walked at all.
1895 let first_march = first_restricted[first_index]
1896 .as_ref()
1897 .map(|window| &window.surface)
1898 .unwrap_or(&first.face.surface);
1899 let second_march = second_restricted[second_index]
1900 .as_ref()
1901 .map(|window| &window.surface)
1902 .unwrap_or(&second.face.surface);
1903 let mut seed_points = Vec::new();
1904 // Smallest |edge_tangent · surface_normal| over accepted seeds — the
1905 // local grazing measure at the trim crossings (near 0 = the edge
1906 // pierces the other surface tangentially). Gates the near-tangent
1907 // clip-order rescue below to genuinely grazing pairs.
1908 let mut min_seed_tangency = f64::INFINITY;
1909 // Two grazing crossings of one trim edge bound a real, narrow
1910 // interval. It cannot tolerate a section fit that misses either
1911 // crossing, even when a coarse whole-model distance looks small.
1912 let mut paired_grazing_crossings = false;
1913 // Every hit the seed gate rejected, for the whole pair: the
1914 // point, its `|tan·n|`, the edge (its operand, its id) and the
1915 // parameter on it, and whether that edge belongs to `first`.
1916 // Read only when the march comes back empty, below, where the
1917 // genuine crossings among them are told from lies-on samples.
1918 let mut grazing_hits: Vec<(Vec3, f64, u8, u64, f64, bool)> = Vec::new();
1919 for (face, other_march, other) in
1920 [(first, second_march, second), (second, first_march, first)]
1921 {
1922 for edge in &face_edge_lists[&face.key()] {
1923 if edge.degenerate {
1924 continue;
1925 }
1926 let mut grazing_crossings: Vec<Vec3> = Vec::new();
1927 // LIES-ON SKIP (2026-10-17, the shelled-box report's
1928 // offset bore against the offset blend walls): an edge
1929 // that LIES ON the other carrier — the test the exchange
1930 // stage above already trusted to hand the whole edge over
1931 // as a shared span — returns a hit per sample from
1932 // `intersect_curve_surface`, every one grazing (|tan.n| = 0
1933 // on a curve in the surface) and every one rejected: three
1934 // such edges answered 8 580 of that pair's 8 586 seed hits
1935 // at 0.36 s. Such an edge contributes no seed, no tangency
1936 // minimum and no section evidence; its one effect is the
1937 // paired-grazing flag, set when two of its grazing points
1938 // sit more than the weld apart, which is reproduced here
1939 // from the edge's own extent. `BREP_IMPRINT_LIES_ON_SEED_SKIP=0`
1940 // runs the solve as before.
1941 if lies_on_seed_skip_enabled() {
1942 let subcurve = cached_subcurve(face.operand, edge)?;
1943 if super::support::curve_lies_on_surface(&subcurve, other_march, options.tolerance)? {
1944 let [start, end] = subcurve.domain().or_refuse(KernelStage::Intersect, "domain")?;
1945 let mut probes = Vec::with_capacity(5);
1946 for fraction in [0.0, 0.25, 0.5, 0.75, 1.0] {
1947 probes.push(subcurve.evaluate(start + (end - start) * fraction).or_refuse(KernelStage::Intersect, "evaluate")?);
1948 }
1949 let weld = assembler_weld(options.tolerance);
1950 if probes.iter().any(|a| probes.iter().any(|b| a.sub(*b).length() > weld)) {
1951 paired_grazing_crossings = true;
1952 }
1953 if debug_pairs {
1954 eprintln!(
1955 "pair {}x{}: seed edge {} lies on the other carrier — seed solve skipped",
1956 first.face.id, second.face.id, edge.id
1957 );
1958 }
1959 continue;
1960 }
1961 }
1962 for hit in intersect_curve_surface(&edge.curve, other_march, options.tolerance).or_refuse(KernelStage::Intersect, "intersect_curve_surface")?
1963 {
1964 if hit.t < edge.t0 - 1e-9 || hit.t > edge.t1 + 1e-9 {
1965 continue;
1966 }
1967 let derivative = edge.curve.derivatives(hit.t, 1).or_refuse(KernelStage::Intersect, "derivatives")?[1];
1968 // A STATIONARY POINT on the edge curve is not a reason
1969 // to refuse the boolean. An involute flank starts at
1970 // the base circle with an exactly zero tangent, so a
1971 // gear tooth's profile carries one cusp per flank, and
1972 // a hit landing on that parameter used to propagate
1973 // "Vec3.normalized: zero-length vector" out of the
1974 // whole union (the 2026-09-14 herringbone report: the
1975 // second tooth band's z=0 cap edge meets the first
1976 // band's flank exactly at the cusp). The direction is
1977 // still defined as the one-sided limit, so read it from
1978 // just inside the edge's own range; an edge with no
1979 // direction at all simply contributes no seed, exactly
1980 // as a hit whose face normal is unavailable already
1981 // does two lines below. Seeds are marcher hints, so a
1982 // dropped one costs a hint, never an answer.
1983 // Escape hatch for tamper-verification:
1984 // BREP_CUSP_TANGENT_RESCUE=0 restores the refusal.
1985 let tangent = match derivative.normalized() {
1986 Ok(tangent) => tangent,
1987 Err(error) => {
1988 if std::env::var("BREP_CUSP_TANGENT_RESCUE").as_deref() == Ok("0") {
1989 return Err(error)
1990 .or_refuse(KernelStage::Intersect, "normalized");
1991 }
1992 match edge.curve.stationary_tangent_rescue(hit.t, edge.t0, edge.t1)
1993 {
1994 Some(tangent) => tangent,
1995 None => continue,
1996 }
1997 }
1998 };
1999 // `hit` was solved on the other face's MARCH WINDOW, so
2000 // its uv is in that face's chart — a period off the
2001 // carrier's where the window is lifted, and `normal`
2002 // clamps a parameter past its domain.
2003 let normal = match other.chart().normal(hit.u, hit.v) {
2004 Ok(normal) => normal,
2005 Err(_) => continue,
2006 };
2007 if debug_pairs {
2008 eprintln!(
2009 "pair {}x{}: seed edge {} t={:.6} p=({:.5},{:.5},{:.5}) |tan.n|={:.4} {}",
2010 first.face.id,
2011 second.face.id,
2012 edge.id,
2013 hit.t,
2014 hit.point.x,
2015 hit.point.y,
2016 hit.point.z,
2017 tangent.dot(normal).abs(),
2018 if tangent.dot(normal).abs() >= 0.1 { "ACCEPT" } else { "reject" }
2019 );
2020 }
2021 if tangent.dot(normal).abs() >= 0.1 {
2022 seed_points.push(hit.point);
2023 min_seed_tangency = min_seed_tangency.min(tangent.dot(normal).abs());
2024 section_evidence = true;
2025 } else {
2026 if grazing_crossings.iter().any(|point| {
2027 point.sub(hit.point).length() > assembler_weld(options.tolerance)
2028 }) {
2029 paired_grazing_crossings = true;
2030 }
2031 grazing_crossings.push(hit.point);
2032 grazing_hits.push((
2033 hit.point,
2034 tangent.dot(normal).abs(),
2035 face.operand,
2036 edge.id,
2037 hit.t,
2038 face.operand == first.operand && face.face.id == first.face.id,
2039 ));
2040 }
2041 }
2042 }
2043 }
2044 profile.seeds += profile.lap(&mut lap_start);
2045 // POINT CONTACT (`BREP_IMPRINT_PLANE_POINT_SKIP`, lane X row 5):
2046 // two exact parallelograms meeting at one point have no section
2047 // curve. Once a seed is accepted the pair's contact evidence is
2048 // already recorded and the march could only trace that point, so
2049 // it is skipped; an unseeded pair still marches.
2050 if !seed_points.is_empty()
2051 && plane_point_skip_enabled()
2052 && super::support::planar_point_contact(&first.face.surface, &second.face.surface, options.tolerance) == Some(true)
2053 {
2054 PLANE_POINT_SKIPS.with(|count| count.set(count.get() + 1));
2055 if debug_pairs {
2056 eprintln!("pair {}x{}: planar point contact, march skipped", first.face.id, second.face.id);
2057 }
2058 continue;
2059 }
2060 profile.marched_pairs += 1;
2061 if debug_pairs {
2062 eprintln!(
2063 "pair {}x{}: marching (relation {:?})...",
2064 first.face.id, second.face.id, pair_classification.relation
2065 );
2066 }
2067 let marched = intersect_surfaces(
2068 first_march,
2069 second_march,
2070 &SurfaceIntersectionOptions {
2071 tolerance: options.tolerance,
2072 maximum_step: march_maximum_step(
2073 &pair_classification,
2074 options.maximum_ssi_step,
2075 builder.scale,
2076 ),
2077 seed_points: seed_points.clone(),
2078 span_budget: march_span_budget,
2079 ordered_starts,
2080 consume_starts,
2081 tree_seeds,
2082 ..Default::default()
2083 },
2084 );
2085 // A pair carrying a tangent node keeps its OWN refusal class when
2086 // the march fails: the node is why the trace cannot close, and the
2087 // equal-radius torus pair depends on being told so rather than on
2088 // reading a step-budget message it cannot act on.
2089 let marched = match (marched, tangent_node) {
2090 (Err(error), Some(node)) => {
2091 return Err(KernelRefusal::new(
2092 RefusalClass::TangentNodeSingularity,
2093 KernelStage::Intersect,
2094 format!(
2095 "boolean: unsupported singular/tangent-node surface intersection \
2096 between faces {} and {}: the section through the tangent node at \
2097 ({:.6},{:.6},{:.6}) could not be marched ({error})",
2098 first.face.id, second.face.id, node.x, node.y, node.z
2099 ),
2100 ));
2101 }
2102 (marched, _) => marched,
2103 };
2104 let mut branches = marched
2105 .map_err(|error| {
2106 format!(
2107 "{error} (marching faces {} and {})",
2108 first.face.id, second.face.id
2109 )
2110 }).or_refuse(KernelStage::Intersect, "csg.imprint.driver")?;
2111 profile.march += profile.lap(&mut lap_start);
2112 let rescue_started = profile.enabled.then(Instant::now);
2113 // MARCH-ORDER SWAP RESCUE (hatch BREP_MARCH_SWAP_RESCUE=0).
2114 // `intersect_surfaces` is not order-symmetric: the coupled Newton
2115 // trace can fail to start/continue from a valid seed when the two
2116 // surfaces are presented in one operand order yet succeed in the
2117 // other. This is the sole reason t217's `subtract(sphere, step)`
2118 // fails while every other op passes — pair 118x842 (sphere-first)
2119 // marches ZERO branches, while 842x118 (step-first, the working
2120 // a\b order) marches the section from the IDENTICAL accepted pierce
2121 // seeds. When the forward order returns no branch AND an accepted
2122 // transverse pierce seed exists (so a real section provably crosses
2123 // both trims), retry the march with the surfaces swapped — i.e.
2124 // reproduce the exact call the working operand order makes for this
2125 // pair (measured: seed_only alone does NOT recover it — the section
2126 // is found from an auto-grid start, not from the near-tangent pierce
2127 // seeds, which the seed normal-cross gate rejects). The returned
2128 // branch points are 3D and therefore order-independent, so no
2129 // parameter remap is needed; they flow through the identical clip /
2130 // process_curve gates below, which drop any out-of-trim or
2131 // sub-length run exactly as today. The rescue only ever runs when
2132 // the forward order found NOTHING, so it cannot alter a pair that
2133 // already marched.
2134 if branches.is_empty()
2135 && !seed_points.is_empty()
2136 && pair_classification.relation == SurfacePairRelation::Candidate
2137 && std::env::var("BREP_MARCH_SWAP_RESCUE").as_deref() != Ok("0")
2138 {
2139 // FAIL-SOFT: the forward order already returned Ok(empty); a
2140 // swapped-march error (trace-exhaustion is a real error class —
2141 // `intersect_surfaces_supplemental` swallows it for exactly this
2142 // reason) must NOT convert that graceful empty into a hard error.
2143 // On Err, keep the (empty) forward result and carry on.
2144 let swapped = intersect_surfaces(
2145 second_march,
2146 first_march,
2147 &SurfaceIntersectionOptions {
2148 tolerance: options.tolerance,
2149 maximum_step: march_maximum_step(
2150 &pair_classification,
2151 options.maximum_ssi_step,
2152 builder.scale,
2153 ),
2154 seed_points: seed_points.clone(),
2155 span_budget: march_span_budget,
2156 ordered_starts,
2157 consume_starts,
2158 tree_seeds,
2159 ..Default::default()
2160 },
2161 )
2162 .unwrap_or_default();
2163 if debug_pairs {
2164 eprintln!(
2165 "pair {}x{}: MARCH-SWAP RESCUE attempt -> {} branches, pts {:?}",
2166 first.face.id,
2167 second.face.id,
2168 swapped.len(),
2169 swapped.iter().map(|b| b.points.len()).collect::<Vec<_>>()
2170 );
2171 }
2172 if !swapped.is_empty() {
2173 branches = swapped;
2174 march_swap_rescues += 1;
2175 }
2176 }
2177 // NEAR-TANGENT CLIP-ORDER RESCUE (hatch BREP_MARCH_SWAP_CLIP_RESCUE=0).
2178 // Companion to the empty-branch MARCH-SWAP RESCUE above:
2179 // `intersect_surfaces` is order-asymmetric not only in WHETHER it
2180 // marches, but in the exact sample positions of the section
2181 // polyline. On a NEAR-TANGENT graze, `clip_branch_to_trims`
2182 // classifies those samples against the mutual trims, and a sample
2183 // landing just past the near-tangent boundary is dropped as a
2184 // false-Outside that TRUNCATES the clipped section. The two operand
2185 // orders drop DIFFERENT near-tangent tail samples, so one order
2186 // yields a section ~0.1-0.2mm shorter at ONE endpoint (t660: b\a's
2187 // step-first order clips pairs 223x105/399x105 ~0.22/0.14mm short of
2188 // the section a\b's cyl-first order keeps, stranding edge 173
2189 // one-use). Near-tangent clip errors are almost exclusively
2190 // false-Outside (a point truly outside a trim rarely projects to an
2191 // in-trim uv), so the order with the LONGER clipped section suffered
2192 // fewer drops and is the more complete one. When the swapped order
2193 // marches the SAME branch structure with a meaningfully longer
2194 // clipped total, adopt its branches (3D points, so they flow through
2195 // the identical clip/process_curve gates below). Gated to Candidate
2196 // pairs (the near-tangent/ambiguous class; clean Transverse
2197 // crossings clip identically in both orders and never differ) with
2198 // an accepted GRAZING pierce seed (min |tan·n| < 0.5 — a real
2199 // section provably crosses, and does so near-tangentially, the only
2200 // regime where the clip wobbles; this also bounds the extra march to
2201 // grazing pairs), and requires a >0.1%-of-length margin so
2202 // raw-sampling jitter (t660: 0.02%) cannot flip a clean pair.
2203 // Fail-soft: any swapped-march or comparison-clip error keeps the
2204 // forward result.
2205 if !branches.is_empty()
2206 && !seed_points.is_empty()
2207 && min_seed_tangency < 0.5
2208 && pair_classification.relation == SurfacePairRelation::Candidate
2209 && std::env::var("BREP_MARCH_SWAP_CLIP_RESCUE").as_deref() != Ok("0")
2210 {
2211 let swapped = intersect_surfaces(
2212 second_march,
2213 first_march,
2214 &SurfaceIntersectionOptions {
2215 tolerance: options.tolerance,
2216 maximum_step: march_maximum_step(
2217 &pair_classification,
2218 options.maximum_ssi_step,
2219 builder.scale,
2220 ),
2221 seed_points: seed_points.clone(),
2222 span_budget: march_span_budget,
2223 ordered_starts,
2224 consume_starts,
2225 tree_seeds,
2226 ..Default::default()
2227 },
2228 )
2229 .unwrap_or_default();
2230 // Only a swap that reproduces the SAME branch count — a
2231 // refined-endpoint variant of the same section, not a different
2232 // branch decomposition (guards against adopting a spurious
2233 // extra branch as "longer").
2234 if !swapped.is_empty() && swapped.len() == branches.len() {
2235 let totals = (|| -> Result<(f64, f64), KernelRefusal> {
2236 let mut fwd = 0.0;
2237 for b in &branches {
2238 let refined = insert_seed_points_into_branch(
2239 &b.points,
2240 &seed_points,
2241 options.tolerance,
2242 );
2243 for run in clip_branch_to_trims(&refined, first, second)? {
2244 fwd += run
2245 .windows(2)
2246 .map(|p| p[1].sub(p[0]).length())
2247 .sum::<f64>();
2248 }
2249 }
2250 let mut swp = 0.0;
2251 for b in &swapped {
2252 let refined = insert_seed_points_into_branch(
2253 &b.points,
2254 &seed_points,
2255 options.tolerance,
2256 );
2257 for run in clip_branch_to_trims(&refined, first, second)? {
2258 swp += run
2259 .windows(2)
2260 .map(|p| p[1].sub(p[0]).length())
2261 .sum::<f64>();
2262 }
2263 }
2264 Ok((fwd, swp))
2265 })();
2266 if let Ok((fwd_clip, swp_clip)) = totals {
2267 let margin = fwd_clip.max(swp_clip) * 1.0e-3;
2268 let adopt = swp_clip > fwd_clip + margin;
2269 if debug_pairs {
2270 eprintln!(
2271 "pair {}x{}: CLIP-SWAP RESCUE fwd_clip={:.6} swp_clip={:.6} margin={:.6}{}",
2272 first.face.id,
2273 second.face.id,
2274 fwd_clip,
2275 swp_clip,
2276 margin,
2277 if adopt { " ADOPT" } else { "" }
2278 );
2279 }
2280 if adopt {
2281 branches = swapped;
2282 clip_swap_rescues += 1;
2283 }
2284 }
2285 }
2286 }
2287 if let Some(started) = rescue_started {
2288 profile.rescue_march += started.elapsed();
2289 }
2290 if branches.iter().any(|branch| branch.points.len() >= 2) {
2291 section_evidence = true;
2292 }
2293 if debug_pairs {
2294 eprintln!(
2295 "pair {}x{}: MARCH {} branches, pts {:?}",
2296 first.face.id,
2297 second.face.id,
2298 branches.len(),
2299 branches.iter().map(|b| b.points.len()).collect::<Vec<_>>()
2300 );
2301 }
2302 // GRAZING PIERCE, NO SECTION (2026-10-17, the revolve-into-bore
2303 // report). A trim edge of one face pierces the other carrier at
2304 // an angle below the seed gate, inside BOTH trims, and the pair
2305 // marched nothing: the pierce proves a section crosses the faces
2306 // there, the empty march says the crossing is too near tangency
2307 // for the marcher's start gate to admit (the -4° revolve of a
2308 // bored box's top face about its back edge, unioned back: the
2309 // torus hole wall continues the bore tangentially along their
2310 // shared rim and crosses it again at 0.002 rad — a third-order
2311 // contact, `boolean::attribute_to_tangent_node`). Nothing is
2312 // imprinted here; the contact is RECORDED, with the faces across
2313 // the pierced edges, so a tear downstream of it is attributed to
2314 // the contact in the tangent-node class — which declines the
2315 // perturbation ladder — instead of leaking as an anonymous
2316 // open-edge arrangement and burning the ladder's rungs (12.5 s
2317 // on the report, every rung refused by its mirror). A pair whose
2318 // pierce the trims do not both contain is left alone, as is one
2319 // with an accepted seed: those pairs the march could and should
2320 // have answered. `BREP_GRAZING_PIERCE_RECORD=0` is the hatch.
2321 if branches.is_empty()
2322 && seed_points.is_empty()
2323 && !grazing_hits.is_empty()
2324 && options.tangent_curve_lane
2325 && std::env::var("BREP_GRAZING_PIERCE_RECORD").as_deref() != Ok("0")
2326 {
2327 let mut inside: Vec<(Vec3, f64, u8, u64)> = Vec::new();
2328 for &(point, tangency, operand, edge_id, t, on_first) in &grazing_hits {
2329 // A pierce, not a lies-on sample: the edge must CROSS the
2330 // carrier at the hit (a rim lying on the other carrier
2331 // reads |tan·n| = 0 at every sample and never changes
2332 // side) — and inside both trims.
2333 let (owner, other_march) = if on_first { (first, second_march) } else { (second, first_march) };
2334 let Some(edge) = face_edge_lists[&owner.key()].iter().find(|edge| edge.id == edge_id) else {
2335 continue;
2336 };
2337 let crosses = edge_crosses_carrier(edge, t, other_march, options.tolerance)?;
2338 let contained = crosses && inside_both_trims(point, first, second)?;
2339 if debug_pairs && crosses {
2340 eprintln!(
2341 "pair {}x{}: grazing pierce op={operand} edge={edge_id} ({:.6},{:.6},{:.6}) |tan.n|={tangency:.4} inside both trims: {contained}",
2342 first.face.id, second.face.id, point.x, point.y, point.z
2343 );
2344 }
2345 if contained {
2346 inside.push((point, tangency, operand, edge_id));
2347 }
2348 }
2349 // SUB-GATE RE-MARCH (2026-10-17, the same report, second
2350 // half). The pierces are PROVEN section points, so the march
2351 // is retried from them alone, admitted below the marcher's
2352 // start gate: the crossing is resolvable while its lateral
2353 // uncertainty `tol / α` fits inside the tangency band's
2354 // half-width `√(2 R tol)`, i.e. `α ≥ √(tol / 2R)`, and no
2355 // carrier is curved tighter than the model, so the bound is
2356 // taken at `R = scale`. Grid starts stay gated (their
2357 // tangency-band runaway is the gate's reason). A branch that
2358 // comes back is imprinted exactly as a marched one; only when
2359 // none does is the contact recorded for the attribution. The
2360 // retry is fail-soft like the swap rescue: an Err keeps the
2361 // empty forward result. Hatch: BREP_GRAZING_SEED_MARCH=0.
2362 if !inside.is_empty() && std::env::var("BREP_GRAZING_SEED_MARCH").as_deref() != Ok("0") {
2363 let bound = (options.tolerance / (2.0 * builder.scale.max(options.tolerance))).sqrt();
2364 let pierces: Vec<Vec3> = inside.iter().map(|(point, _, _, _)| *point).collect();
2365 let rescued = intersect_surfaces(
2366 first_march,
2367 second_march,
2368 &SurfaceIntersectionOptions {
2369 tolerance: options.tolerance,
2370 maximum_step: march_maximum_step(
2371 &pair_classification,
2372 options.maximum_ssi_step,
2373 builder.scale,
2374 ),
2375 seed_points: pierces.clone(),
2376 seed_only: true,
2377 seed_start_cross: Some(bound),
2378 span_budget: march_span_budget,
2379 ordered_starts,
2380 consume_starts,
2381 tree_seeds,
2382 ..Default::default()
2383 },
2384 )
2385 .unwrap_or_default();
2386 if debug_pairs {
2387 eprintln!(
2388 "pair {}x{}: SUB-GATE MARCH from {} pierce(s) at bound {bound:.3e} -> {} branches, pts {:?}",
2389 first.face.id,
2390 second.face.id,
2391 pierces.len(),
2392 rescued.len(),
2393 rescued.iter().map(|b| b.points.len()).collect::<Vec<_>>()
2394 );
2395 }
2396 if !rescued.is_empty() {
2397 branches = rescued;
2398 grazing_march = true;
2399 // The pierces are the branches' exact trim crossings,
2400 // inserted below like any accepted seed.
2401 seed_points.extend(pierces);
2402 inside.clear();
2403 }
2404 }
2405 if !inside.is_empty() {
2406 if debug_pairs {
2407 eprintln!(
2408 "pair {}x{}: {} grazing pierce(s) inside both trims and no marched branch: {}",
2409 first.face.id,
2410 second.face.id,
2411 inside.len(),
2412 inside
2413 .iter()
2414 .map(|(point, tangency, operand, edge)| format!(
2415 "op={operand} edge={edge} ({:.6},{:.6},{:.6}) |tan.n|={tangency:.4}",
2416 point.x, point.y, point.z
2417 ))
2418 .collect::<Vec<_>>()
2419 .join(", ")
2420 );
2421 }
2422 // The extended contact the lane can name, when it finds
2423 // one (the shared rim here); else the record stands on
2424 // the pierces alone.
2425 let mut section = match tangent_curve_section(
2426 first, second, options, &first_faces, &second_faces, &face_edge_lists, debug_pairs,
2427 )? {
2428 Some((section, _run)) => section,
2429 None => TangentSectionRecord {
2430 first: first.key(),
2431 second: second.key(),
2432 start: inside[0].0,
2433 end: inside[inside.len() - 1].0,
2434 length: 0.0,
2435 closed: false,
2436 stations: 0,
2437 planar: false,
2438 faces: vec![first.key(), second.key()],
2439 piece_ids: Vec::new(),
2440 grazing: Vec::new(),
2441 },
2442 };
2443 // The faces across each pierced edge: the pierce is where
2444 // the missing section meets that edge, so those faces are
2445 // the ones a tear downstream of it will name.
2446 for (_, _, operand, edge_id) in &inside {
2447 let siblings = if *operand == first.operand { &first_faces } else { &second_faces };
2448 for sibling in siblings.iter() {
2449 let shares = sibling
2450 .face
2451 .loops
2452 .iter()
2453 .flat_map(|loop_record| &loop_record.coedges)
2454 .any(|coedge| coedge.edge_id == *edge_id);
2455 if shares && !section.faces.contains(&sibling.key()) {
2456 section.faces.push(sibling.key());
2457 }
2458 }
2459 }
2460 section.grazing = inside.iter().map(|(point, tangency, _, _)| (*point, *tangency)).collect();
2461 tangent_sections.push(section);
2462 }
2463 }
2464 if profile.enabled {
2465 // Net metadata only: observing carriers must not warm their
2466 // recognition/projection caches before the section fitter.
2467 eprintln!("imprint.march_pair first={:?} second={:?} ids={}x{} degrees={}x{}/{}x{} nets={}x{}/{}x{} stations={:?}",
2468 first.face.name, second.face.name, first.face.id, second.face.id,
2469 first.face.surface.degree_u, first.face.surface.degree_v,
2470 second.face.surface.degree_u, second.face.surface.degree_v,
2471 first.face.surface.control_points.len(), first.face.surface.control_points.first().map_or(0, Vec::len),
2472 second.face.surface.control_points.len(), second.face.surface.control_points.first().map_or(0, Vec::len),
2473 branches.iter().map(|branch| branch.points.len()).collect::<Vec<_>>());
2474 }
2475 for branch in branches {
2476 // The pierce seeds are the section's exact trim-crossing
2477 // points — insert them so no trim interval shorter than the
2478 // march step is invisible to the point-classification clip.
2479 let refined_points =
2480 insert_seed_points_into_branch(&branch.points, &seed_points, options.tolerance);
2481 let clip_started = profile.enabled.then(Instant::now);
2482 let mut runs = clip_branch_to_trims(&refined_points, first, second)?;
2483 // TANGENT-RIM LANDING: a run the march stalled short of a trim
2484 // edge the two carriers are tangent along ends ON that edge
2485 // (see `land_run_ends_on_tangent_rim`).
2486 // Reached for the pairs whose march can stall short: one
2487 // carrying an admitted node, one marched from sub-gate
2488 // pierces, or one the classifier read as near-tangent (the
2489 // −10° sibling: no node admitted, an ordinary march that
2490 // stops 4e-5 short of the rim it is tangent along).
2491 if tangent_node.is_some()
2492 || grazing_march
2493 || pair_classification.relation == SurfacePairRelation::NearTangent
2494 {
2495 for run in runs.iter_mut() {
2496 for (operand, edge_id, parameter, from, to) in land_run_ends_on_tangent_rim(
2497 run,
2498 tangent_node,
2499 first,
2500 second,
2501 &face_edge_lists,
2502 options.tolerance,
2503 )? {
2504 if debug_pairs {
2505 eprintln!(
2506 "pair {}x{}: tangent-rim landing: run end ({:.6},{:.6},{:.6}) -> \
2507 ({:.6},{:.6},{:.6}) on operand {operand} edge {edge_id} at t={parameter:.6}",
2508 first.face.id, second.face.id, from.x, from.y, from.z, to.x, to.y, to.z
2509 );
2510 }
2511 let edge = builder.edges[&(operand, edge_id)];
2512 builder.add_edge_split(operand, edge, parameter)?;
2513 }
2514 }
2515 }
2516 if debug_pairs {
2517 let dups = |points: &[Vec3]| points.windows(2).enumerate().filter(|(_, pair)| pair[1].sub(pair[0]).length() <= options.tolerance).map(|(i, pair)| format!("{i}:{:.2e}", pair[1].sub(pair[0]).length())).collect::<Vec<_>>();
2518 eprintln!(
2519 "pair {}x{}: DUPS branch {:?} seeded {:?} runs {:?} seeds {} branch_pts {} seeded_pts {}",
2520 first.face.id, second.face.id, dups(&branch.points), dups(&refined_points), runs.iter().map(|run| dups(run)).collect::<Vec<_>>(), seed_points.len(), branch.points.len(), refined_points.len()
2521 );
2522 }
2523 if let Some(started) = clip_started {
2524 profile.clip += started.elapsed();
2525 }
2526 for run in runs {
2527 if debug_pairs {
2528 eprintln!(
2529 "pair {}x{}: clip run len_pts={} length={:.4e}",
2530 first.face.id,
2531 second.face.id,
2532 run.len(),
2533 run.windows(2)
2534 .map(|pair| pair[1].sub(pair[0]).length())
2535 .sum::<f64>()
2536 );
2537 }
2538 if run.len() < 2 {
2539 continue;
2540 }
2541 let length: f64 = run
2542 .windows(2)
2543 .map(|pair| pair[1].sub(pair[0]).length())
2544 .sum();
2545 if length <= options.tolerance * 100.0 {
2546 continue;
2547 }
2548 let shared_started = profile.enabled.then(Instant::now);
2549 let follows_shared = branch_follows_shared_boundary(
2550 &run,
2551 first,
2552 second,
2553 &builder.edges,
2554 options.tolerance,
2555 builder.scale,
2556 )?;
2557 if let Some(started) = shared_started {
2558 profile.shared_boundary += started.elapsed();
2559 }
2560 if follows_shared {
2561 if debug_pairs {
2562 eprintln!(
2563 "pair {}x{}: run dropped (follows shared boundary)",
2564 first.face.id, second.face.id
2565 );
2566 }
2567 continue;
2568 }
2569 if let Some(census) = pair_census.as_mut() {
2570 census.record_stations(&run);
2571 }
2572 let pieces_before = builder.pieces.len();
2573 let chunk_points = options.fit_chunk_points.unwrap_or(run.len()).max(2);
2574 let mut start = 0;
2575 while start + 1 < run.len() {
2576 let end = (start + chunk_points - 1).min(run.len() - 1);
2577 // HERMITE EMISSION (`imprint/hermite.rs`): with the lane on
2578 // and every span of this run within the budget, the section
2579 // is the march's own cubic Hermite with its two parameter
2580 // Hermites, and the fit lane below is skipped.
2581 let hermite_started = profile.enabled.then(Instant::now);
2582 let hermite: Option<Result<super::hermite::HermiteSection, super::hermite::HermiteDecline>> = if hermite_lane {
2583 Some(match march_span_budget {
2584 Some(budget) => super::hermite::hermite_section(&run[start..=end], &branch, first_march, second_march, budget)
2585 .or_refuse(KernelStage::Intersect, "csg.imprint.driver.hermite")?,
2586 None => Err(super::hermite::HermiteDecline::NoTangents),
2587 })
2588 } else {
2589 None
2590 };
2591 if let Some(started) = hermite_started {
2592 profile.hermite += started.elapsed();
2593 }
2594 let (section_curve, exact_pcurves) = match hermite {
2595 Some(Ok(emitted)) => {
2596 profile.hermite_runs += 1;
2597 span_census(|| {
2598 format!(
2599 "pair {}x{} budget {} branch_stations {} run_stations {} kept {} span_deviation {:.3e} tangents true route None met_requested true lane hermite inserted {}",
2600 first.face.id,
2601 second.face.id,
2602 march_span_budget.map_or("none".to_string(), |budget| format!("{budget:.3e}")),
2603 branch.points.len(),
2604 emitted.stations,
2605 emitted.stations,
2606 emitted.deviation,
2607 emitted.inserted,
2608 )
2609 });
2610 (emitted.curve, Some([emitted.pcurve_a, emitted.pcurve_b]))
2611 }
2612 other => {
2613 let hermite_decline = other.map(|lane| lane.err());
2614 profile.fit_runs += 1;
2615 let fit_started = profile.enabled.then(Instant::now);
2616 let fit_tolerance = options.tolerance.max(1e-7);
2617 let (mut section, mut fit, recovered, evening) = even_sliver_ends_measured(
2618 run[start..=end].to_vec(),
2619 &first.face.surface,
2620 &second.face.surface,
2621 options.tolerance,
2622 fit_tolerance,
2623 options.maximum_fit_points,
2624 options.local_fit,
2625 )
2626 .or_refuse(KernelStage::Intersect, "csg.imprint.driver.sliver_end")?;
2627 if evening != SliverEvening::NoSliver {
2628 if let Ok(path) = std::env::var("BREP_SECTION_SLIVER_CENSUS") {
2629 use std::io::Write;
2630 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
2631 let _ = writeln!(file, "sliver-end census: pair {}x{} {evening:?} stations {}", first.face.id, second.face.id, section.len());
2632 }
2633 }
2634 }
2635 // A recovered fit may reproduce every march point yet
2636 // still wander off the carriers between sparse stations.
2637 // Measure it against both supports before accepting that
2638 // recovery, and use the existing bounded SSI refiner.
2639 let met_requested = fit.report.met_requested_tolerance();
2640 let refine_route = section_refine_route(
2641 section_refine_gate(),
2642 recovered,
2643 || {
2644 paired_grazing_crossings
2645 && std::env::var("BREP_GRAZE_SECTION_REFINE").as_deref() != Ok("0")
2646 },
2647 met_requested,
2648 )
2649 .or_else(|| {
2650 // `BREP_SECTION_CARRIER_MISS=0` restores the fast path
2651 // for an attribution A/B only (default on).
2652 if std::env::var("BREP_SECTION_CARRIER_MISS").as_deref() == Ok("0") {
2653 return None;
2654 }
2655 let (station_upper, midspan_upper) =
2656 fast_path_carrier_readings(&fit, §ion, &first.face.surface, &second.face.surface);
2657 carrier_miss_route(station_upper, midspan_upper, fit_tolerance)
2658 });
2659 if let Some((gate, route)) = refine_route {
2660 let stations_before = fit.kept.len();
2661 let refine_census = std::env::var("BREP_SECTION_REFINE_CENSUS").ok();
2662 let (census_head, census_tail) = (section[0], section[section.len() - 1]);
2663 let refine_started = refine_census.is_some().then(Instant::now);
2664 // The wrapper's own two lines, inlined so the receipt
2665 // can be read (behaviour identical to
2666 // `refine_section_against_carriers`).
2667 let representatives = std::env::var("BREP_SECTION_REPRESENTATIVES").as_deref() != Ok("0");
2668 let mut receipt = RefineReceipt::default();
2669 let (refined, worst, unrefined, rounds, exit) = refine_section_against_carriers_with(
2670 &mut section,
2671 fit,
2672 &first.face.surface,
2673 &second.face.surface,
2674 fit_tolerance,
2675 options.tolerance,
2676 options.local_fit,
2677 gate * fit_tolerance,
2678 representatives,
2679 &mut receipt,
2680 ).or_refuse(KernelStage::Intersect, "csg.imprint.driver.midspan")?;
2681 let _ = &receipt;
2682 // `BREP_SECTION_REFINE_CENSUS=<file>`: one line per section,
2683 // appended, so a corpus replay (whose children's stderr is
2684 // captured) can be read for trip counts and growth.
2685 // The route, whether the standing fit met its request, and
2686 // the refiner's own wall time close the line, so a run can
2687 // total the UNMET-REQUEST route's cost apart from the
2688 // global gate's (which reads every section).
2689 if let Some(path) = refine_census {
2690 use std::io::Write;
2691 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
2692 let _ = writeln!(
2693 file,
2694 "section-refine census: pair {}x{} exit {:?} rounds {} stations {} -> {} unrefined {:.3e} worst {:.3e} route {:?} met_requested {} ms {:.3} head ({:.9}, {:.9}, {:.9}) tail ({:.9}, {:.9}, {:.9})",
2695 first.face.id, second.face.id, exit, rounds, stations_before, refined.kept.len(), unrefined, worst,
2696 route, met_requested, refine_started.map_or(0.0, |started| started.elapsed().as_secs_f64() * 1e3),
2697 census_head.x, census_head.y, census_head.z, census_tail.x, census_tail.y, census_tail.z
2698 );
2699 }
2700 }
2701 if debug_pairs && (rounds > 0 || !matches!(exit, MidspanExit::Converged | MidspanExit::BelowGate)) {
2702 eprintln!(
2703 "pair {}x{}: mid-span refinement {:?} after {} round(s): {} stations, worst {:.3e}",
2704 first.face.id, second.face.id, exit, rounds, section.len(), worst
2705 );
2706 }
2707 fit = refined;
2708 }
2709 span_census(|| {
2710 format!(
2711 "pair {}x{} budget {} branch_stations {} run_stations {} kept {} span_deviation {} tangents {} route {:?} met_requested {} lane fit hermite {}",
2712 first.face.id,
2713 second.face.id,
2714 march_span_budget.map_or("none".to_string(), |budget| format!("{budget:.3e}")),
2715 branch.points.len(),
2716 section.len(),
2717 fit.kept.len(),
2718 branch.span_deviation.map_or("none".to_string(), |deviation| format!("{deviation:.3e}")),
2719 branch.tangents.is_some(),
2720 refine_route.map(|(_, route)| route),
2721 met_requested,
2722 hermite_decline.map_or("off".to_string(), |decline| decline.map_or("none".to_string(), |decline| format!("{decline:?}"))),
2723 )
2724 });
2725 // `BREP_SECTION_OVERSHOOT_CENSUS=<file>` (debug only; nothing
2726 // reads it): EVERY section's final fit, on any route, is read
2727 // against both carriers at its kept stations and at its
2728 // mid-spans, so a fit that stands further off the carriers
2729 // between its stations than at them (fixture 28 t217: 5.07e-4
2730 // shipped against a 1.32e-4 worst station) is counted, not
2731 // guessed. Projector distances are upper readings; the line
2732 // says so by its field names.
2733 {
2734 if let Ok(path) = std::env::var("BREP_SECTION_OVERSHOOT_CENSUS") {
2735 // An unreadable station makes the whole reading NaN
2736 // (f64::max would silently drop it).
2737 let read = |surface: &NurbsSurface, point: Vec3| {
2738 project_point_to_surface(surface, point).map_or(f64::NAN, |projection| projection.distance)
2739 };
2740 let mut station_worst = 0.0f64;
2741 for point in &fit.kept {
2742 let (a, b) = (read(&first.face.surface, *point), read(&second.face.surface, *point));
2743 station_worst = if a.is_nan() || b.is_nan() || station_worst.is_nan() { f64::NAN } else { station_worst.max(a).max(b) };
2744 }
2745 let (run_worst, _) = fast_path_carrier_readings(&fit, §ion, &first.face.surface, &second.face.surface);
2746 let interior_worst = knot_interior_carrier_upper(&fit.curve, &first.face.surface, &second.face.surface);
2747 let misses = midspan_misses_only(&fit, &first.face.surface, &second.face.surface).unwrap_or_default();
2748 let (worst_span, midspan_worst) = misses
2749 .iter()
2750 .copied()
2751 .enumerate()
2752 .fold((usize::MAX, 0.0f64), |best, (span, miss)| if miss > best.1 { (span, miss) } else { best });
2753 let line = format!(
2754 "section-overshoot census: pair {}x{} route {:?} kept {} spans_read {} station_upper {:.3e} midspan_upper {:.3e} worst_span {} fit_tolerance {:.1e} ratio {:.3} run {} run_upper {run_worst:.3e} knot_interior_upper {interior_worst:.3e} head ({:.9}, {:.9}, {:.9}) tail ({:.9}, {:.9}, {:.9})\n",
2755 first.face.id, second.face.id, refine_route.map(|(_, route)| route), fit.kept.len(), misses.len(), station_worst, midspan_worst,
2756 if worst_span == usize::MAX { -1 } else { worst_span as i64 }, fit_tolerance,
2757 midspan_worst / station_worst.max(fit_tolerance),
2758 section.len(),
2759 section[0].x, section[0].y, section[0].z,
2760 section[section.len() - 1].x, section[section.len() - 1].y, section[section.len() - 1].z
2761 );
2762 use std::io::Write;
2763 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
2764 let _ = file.write_all(line.as_bytes());
2765 }
2766 }
2767 }
2768 if let Some(started) = fit_started {
2769 profile.fit += started.elapsed();
2770 }
2771 if let Some(census) = pair_census.as_mut() {
2772 census.record_fit(&fit);
2773 }
2774 (fit.curve, None)
2775 }
2776 };
2777 let process_started = profile.enabled.then(Instant::now);
2778 builder.process_curve(
2779 section_curve,
2780 first,
2781 second,
2782 &[first, second],
2783 &[first, second],
2784 true,
2785 exact_pcurves,
2786 )?;
2787 if let Some(started) = process_started {
2788 profile.process += started.elapsed();
2789 }
2790 start = end;
2791 }
2792 for piece in &builder.pieces[pieces_before..] {
2793 builder.marched_pieces.insert(piece.id);
2794 }
2795 if debug_pairs {
2796 eprintln!(
2797 "pair {}x{}: run -> {} pieces",
2798 first.face.id,
2799 second.face.id,
2800 builder.pieces.len() - pieces_before
2801 );
2802 }
2803 }
2804 }
2805 profile.clip_and_fit += profile.lap(&mut lap_start);
2806 }
2807 }
2808 for (span, first, second, target) in deferred_boundary_spans {
2809 builder.process_curve(span, first, second, &[target], &[target], false, None)?;
2810 }
2811 if let Some(scope) = pairs_fits {
2812 profile.pairs_fits = scope.close().fits;
2813 }
2814 profile.report();
2815 let hull_culls = super::builder::take_hull_culls();
2816 if profile.enabled && super::builder::hull_cull_enabled() {
2817 eprintln!("imprint.hull_culls edges={hull_culls}");
2818 }
2819 // SELF-TOUCH SPLIT: a face whose loops touch at an edge interior (a hole
2820 // tangent to a fillet setback) gets a vertex minted at the touch on BOTH
2821 // edges, so the fragment arrangement's pinch resolution assembles — see
2822 // `imprint/self_touch.rs`. Escape hatch: BREP_SELF_TOUCH_SPLIT=0.
2823 let mut tail_lap: Option<Instant> = None;
2824 // Beside each stage's time, the pcurve fits it recorded (a nested
2825 // `PcurveFitScope` per stage, folded into the operation's ledger).
2826 let mut tail_fits: Option<crate::PcurveFitScope> = None;
2827 let mut tail_ms = Vec::<(&str, f64, u64)>::new();
2828 let mut lap = |name: &'static str,
2829 started: &mut Option<Instant>,
2830 fits: &mut Option<crate::PcurveFitScope>,
2831 tail_ms: &mut Vec<(&str, f64, u64)>| {
2832 if profile.enabled {
2833 let now = Instant::now();
2834 let recorded = fits.take().map_or(0, |scope| scope.close().fits);
2835 if let Some(previous) = *started {
2836 tail_ms.push((name, (now - previous).as_secs_f64() * 1_000.0, recorded));
2837 }
2838 *started = Some(now);
2839 *fits = Some(crate::PcurveFitScope::open());
2840 }
2841 };
2842 lap("pairs", &mut tail_lap, &mut tail_fits, &mut tail_ms);
2843 if std::env::var("BREP_SELF_TOUCH_SPLIT").as_deref() != Ok("0") {
2844 builder.split_self_touching_loops(&first_faces, &face_edge_lists, &cached_subcurve)?;
2845 builder.split_self_touching_loops(&second_faces, &face_edge_lists, &cached_subcurve)?;
2846 }
2847 // PIECE-ENDPOINT EXCHANGE post-pass: with every pair's process_curve
2848 // done, the piece set is final for the ridden-edge class — imprint each
2849 // open riding piece's junction endpoints onto the ridden edges (see the
2850 // method doc; hatch BREP_OVERLAP_PIECE_ENDPOINT_SPLIT=0).
2851 builder.exchange_piece_endpoint_junctions()?;
2852 // TANGENT-SECTION IMPRINT (`ImprintOptions::tangent_curve_imprint`), after
2853 // every pair: whether a run splits a face is read against the face's trim
2854 // AND the pieces the other pairs minted on it, so the runs are minted
2855 // once the piece set is complete (the through-bore's contact ends on the
2856 // bore's cap circles, which the box's top and bottom minted).
2857 for (index, run, first, second) in std::mem::take(&mut tangent_imprint_candidates) {
2858 let closed = tangent_sections[index].closed;
2859 tangent_sections[index].piece_ids = imprint_tangent_run(
2860 &mut builder,
2861 &run,
2862 closed,
2863 first,
2864 second,
2865 &face_edge_lists,
2866 options,
2867 std::env::var("BREP_DEBUG_PAIRS").is_ok(),
2868 )?;
2869 }
2870 // TANGENT-SECTION T-JUNCTIONS (`ImprintOptions::tangent_curve_imprint`):
2871 // an imprinted contact ends on operand edges, which `process_curve` split,
2872 // and on earlier SECTIONS of the same carrier, which nothing splits — the
2873 // through-bore's contact line ends on the two cap circles the box's top
2874 // and bottom minted on the bore. Split those pieces at the contact's end
2875 // vertices so both faces of each circle carry the vertex the bore's
2876 // arrangement needs there (see `split_pieces_at_tangent_ends`).
2877 if tangent_sections.iter().any(|section| !section.piece_ids.is_empty()) {
2878 split_pieces_at_tangent_ends(
2879 &mut builder,
2880 &tangent_sections,
2881 &first_faces,
2882 &second_faces,
2883 options,
2884 std::env::var("BREP_DEBUG_PAIRS").is_ok(),
2885 )?;
2886 }
2887 lap("self_touch", &mut tail_lap, &mut tail_fits, &mut tail_ms);
2888 let mut edge_splits = builder
2889 .edge_splits
2890 .into_iter()
2891 .map(|((operand, edge_id), mut parameters)| {
2892 parameters.sort_by(f64::total_cmp);
2893 EdgeSplitRecord {
2894 operand,
2895 edge_id,
2896 parameters,
2897 }
2898 })
2899 .collect::<Vec<_>>();
2900 edge_splits.sort_by_key(|record| (record.operand, record.edge_id));
2901 let mut by_face = builder
2902 .by_face
2903 .into_iter()
2904 .map(|(face, piece_ids)| FaceImprints {
2905 operand: face.operand,
2906 face_id: face.face_id,
2907 piece_ids,
2908 })
2909 .collect::<Vec<_>>();
2910 by_face.sort_by_key(|record| (record.operand, record.face_id));
2911 let section_evidence = section_evidence || !builder.pieces.is_empty();
2912 let marched_pieces = std::mem::take(&mut builder.marched_pieces);
2913 let mut emitted_pcurves = std::mem::take(&mut builder.emitted_pcurves);
2914 let junction_evidence = JunctionEvidence {
2915 radii: std::mem::take(&mut builder.vertex_radii),
2916 sources: std::mem::take(&mut builder.vertex_sources),
2917 };
2918 let mut result = ImprintResultRecord {
2919 tangent_nodes,
2920 tangent_sections,
2921 vertices: builder.vertices,
2922 pieces: builder.pieces,
2923 by_face,
2924 edge_splits,
2925 barrier_edges: builder.barrier_edges.into_iter().collect(),
2926 section_evidence,
2927 cosurface_pairs,
2928 };
2929 RESCUE_FIRINGS.with(|cell| cell.set((march_swap_rescues, clip_swap_rescues)));
2930 if profile.enabled {
2931 eprintln!("imprint.rescues march_swap={march_swap_rescues} clip_swap={clip_swap_rescues}");
2932 }
2933 // COINCIDENT-PIECE MERGE (problemInbox equator-tangent, and the generic
2934 // one-circle-from-many-pairs class): the SAME section curve can be minted
2935 // by several pairs — a cosurface boundary-edge copy (the cylinder cap
2936 // ring lying ON the inscribed sphere) AND the cap-plane's analytic
2937 // section ring are one circle minted twice, each carrying only its own
2938 // pair's supports/pcurves. Assembly then builds duplicate edges that
2939 // cannot both be two-use → one-use strands. Merge pieces whose curves
2940 // coincide along their whole span (bidirectional max deviation within
2941 // the weld band): keep the first, union the supports/pcurves/by_face
2942 // registrations of the rest into it. Escape hatch:
2943 // BREP_COINCIDENT_PIECE_MERGE=0.
2944 lap("collect", &mut tail_lap, &mut tail_fits, &mut tail_ms);
2945 if std::env::var("BREP_COINCIDENT_PIECE_MERGE").as_deref() != Ok("0") {
2946 let weld = assembler_weld(options.tolerance).max(options.tolerance * 10.0);
2947 // Two pieces that coincide along their WHOLE spans (deviation within
2948 // the weld both ways) also have matching end points, up to direction
2949 // and a weld or two of overhang, so a piece whose ends are nowhere
2950 // near the other's needs no 33-station projection sweep. The sweep is
2951 // what made this pass quadratic in wall time: 630 pieces cost 3.2 s
2952 // of a 3.8 s imprint on the 2026-09-12 mesh-import report, all of it
2953 // rejecting pairs an end-point look already rules out.
2954 let mut ends: Vec<[Vec3; 2]> = Vec::with_capacity(result.pieces.len());
2955 for piece in &result.pieces {
2956 let [d0, d1] = piece.curve.domain().or_refuse(KernelStage::Intersect, "domain")?;
2957 ends.push([
2958 piece.curve.evaluate(d0).or_refuse(KernelStage::Intersect, "evaluate")?,
2959 piece.curve.evaluate(d1).or_refuse(KernelStage::Intersect, "evaluate")?,
2960 ]);
2961 }
2962 let end_band = 4.0 * weld;
2963 // CONTROL-HULL CULL (2026-10-10): the plate minus 36 cylinders mints 72
2964 // CLOSED rings, every one of which took the full sweep against every other —
2965 // 2 556 pairs, 230 of the imprint's 288 ms — although rings 16 units apart
2966 // cannot coincide. A NURBS curve lies in the convex hull of its control
2967 // points, so two curves within `weld` of each other along their whole spans
2968 // have control boxes that overlap once each is grown by `weld`; a pair whose
2969 // boxes do not is skipped before any projection. Exact, so the merged set is
2970 // the one the sweep alone found. `BREP_COINCIDENT_MERGE_BOX=0` restores the
2971 // unculled sweep for a before/after reading.
2972 let box_cull = std::env::var("BREP_COINCIDENT_MERGE_BOX").as_deref() != Ok("0");
2973 let mut boxes: Vec<[Vec3; 2]> = Vec::with_capacity(result.pieces.len());
2974 for piece in &result.pieces {
2975 boxes.push(control_hull_box(&piece.curve, weld));
2976 }
2977 // A CLOSED piece (a full ring) has no end points to speak of: two
2978 // rings of the same circle seamed at different azimuths coincide
2979 // along their whole spans while their domain ends sit anywhere on
2980 // the ring, so a closed piece always takes the full sweep.
2981 let ends_match = |a: &[Vec3; 2], b: &[Vec3; 2]| -> bool {
2982 let near = |p: Vec3, q: Vec3| p.sub(q).length() <= end_band;
2983 near(a[0], a[1])
2984 || near(b[0], b[1])
2985 || (near(a[0], b[0]) && near(a[1], b[1]))
2986 || (near(a[0], b[1]) && near(a[1], b[0]))
2987 };
2988 let mut removed: Vec<u64> = Vec::new();
2989 let mut index = 0;
2990 while index < result.pieces.len() {
2991 let mut other = index + 1;
2992 while other < result.pieces.len() {
2993 let coincide = (!box_cull || boxes_overlap(&boxes[index], &boxes[other]))
2994 && ends_match(&ends[index], &ends[other])
2995 && {
2996 let a = &result.pieces[index];
2997 let b = &result.pieces[other];
2998 max_curve_deviation(&a.curve, &b.curve)? <= weld
2999 && max_curve_deviation(&b.curve, &a.curve)? <= weld
3000 };
3001 if coincide {
3002 let absorbed = result.pieces.remove(other);
3003 ends.remove(other);
3004 boxes.remove(other);
3005 removed.push(absorbed.id);
3006 let keeper = &mut result.pieces[index];
3007 for pcurve in absorbed.pcurves {
3008 if !keeper
3009 .pcurves
3010 .iter()
3011 .any(|existing| {
3012 existing.operand == pcurve.operand
3013 && existing.face_id == pcurve.face_id
3014 })
3015 {
3016 let key = FaceKey { operand: pcurve.operand, face_id: pcurve.face_id };
3017 if emitted_pcurves.remove(&(absorbed.id, key)) {
3018 emitted_pcurves.insert((keeper.id, key));
3019 }
3020 keeper.pcurves.push(pcurve);
3021 }
3022 }
3023 let keeper_id = keeper.id;
3024 for record in &mut result.by_face {
3025 if let Some(position) =
3026 record.piece_ids.iter().position(|&id| id == absorbed.id)
3027 {
3028 if record.piece_ids.contains(&keeper_id) {
3029 record.piece_ids.remove(position);
3030 } else {
3031 record.piece_ids[position] = keeper_id;
3032 }
3033 }
3034 }
3035 } else {
3036 other += 1;
3037 }
3038 }
3039 index += 1;
3040 }
3041 if !removed.is_empty() && std::env::var("BREP_DEBUG_BOOL").is_ok() {
3042 eprintln!("coincident-piece merge: absorbed {:?}", removed);
3043 }
3044 }
3045 // Rescue near-tangent SSI truncations BEFORE canonicalization so the added
3046 // bridge pieces' endpoints (existing crossing/stub vertices) fold into the
3047 // same junction merges as every other section.
3048 lap("coincident_merge", &mut tail_lap, &mut tail_fits, &mut tail_ms);
3049 extend_truncated_sections(
3050 &mut result,
3051 &face_edge_lists,
3052 solid_a,
3053 solid_b,
3054 &charts,
3055 options.tolerance,
3056 )?;
3057 lap("extend_truncated", &mut tail_lap, &mut tail_fits, &mut tail_ms);
3058 canonicalize_imprint_junctions(
3059 &mut result,
3060 solid_a,
3061 solid_b,
3062 &charts,
3063 options.tolerance,
3064 &emitted_pcurves,
3065 &junction_evidence,
3066 junction_combine_mode(options),
3067 )?;
3068 lap("canonicalize", &mut tail_lap, &mut tail_fits, &mut tail_ms);
3069 // ONE RIM, ONE EDGE: with vertex identity final, dissolve the section
3070 // vertices that no operand edge passes through — the marcher's own
3071 // parameterization origin, which `process_curve` cannot tell from a
3072 // junction because it sees one pair at a time. See the method doc.
3073 let dissolved = dissolve_section_origin_vertices(
3074 &mut result,
3075 &marched_pieces,
3076 solid_a,
3077 solid_b,
3078 &charts,
3079 options.tolerance,
3080 )?;
3081 if dissolved > 0 && std::env::var("BREP_DEBUG_BOOL").is_ok() {
3082 eprintln!("origin-dissolve: {dissolved} parameterization vertex/vertices removed");
3083 }
3084 lap("origin_dissolve", &mut tail_lap, &mut tail_fits, &mut tail_ms);
3085 // B2: reuse an existing boundary edge as the shared section edge wherever a
3086 // section coincides with one along its whole span (vertices are final after
3087 // canonicalization; `face_edge_lists` holds each face's boundary edges on
3088 // the healed operands). Runs here so both operands reference ONE edge.
3089 reuse_boundary_section_edges(
3090 &mut result,
3091 &face_edge_lists,
3092 solid_a,
3093 solid_b,
3094 options.tolerance,
3095 )?;
3096 // Capstone step 1 — instrumentation only, zero behavior change: report
3097 // every (section piece × boundary edge) contact where the piece runs
3098 // within the scale-derived band of the edge over a real span. Measuring
3099 // the bands here first validates the graze-contact model on the
3100 // acceptance suite before step 2's common-block machinery replaces a
3101 // grazed overlap with a shared edge.
3102 lap("reuse_boundary", &mut tail_lap, &mut tail_fits, &mut tail_ms);
3103 report_graze_contacts(&result, &face_edge_lists, solid_a, solid_b, options.tolerance)?;
3104 if profile.enabled {
3105 if let Some(scope) = tail_fits.take() {
3106 scope.close();
3107 }
3108 let line = tail_ms
3109 .iter()
3110 .map(|(name, ms, _)| format!("{name}={ms:.2}"))
3111 .collect::<Vec<_>>()
3112 .join(" ");
3113 eprintln!("imprint.stages {line}");
3114 let fits = tail_ms
3115 .iter()
3116 .map(|(name, _, fits)| format!("{name}={fits}"))
3117 .collect::<Vec<_>>()
3118 .join(" ");
3119 eprintln!("imprint.stage_fits {fits}");
3120 }
3121 if let Ok(path) = std::env::var("BREP_IMPRINT_SECTION_CENSUS") {
3122 use std::io::Write;
3123 // A private observation of the complete imprint, including section
3124 // curves, pcurves and junctions. Canonicalize only the set-valued
3125 // barrier list on a COPY; leave every returned record untouched.
3126 let mut observed = result.clone();
3127 observed.barrier_edges.sort_unstable();
3128 let bytes = serde_json::to_vec(&observed).expect("serialize imprint census");
3129 let digest = bytes.iter().fold(0xcbf29ce484222325_u64,
3130 |hash, byte| (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3));
3131 let mut file = std::fs::OpenOptions::new().create(true).append(true).open(path)
3132 .expect("open imprint section census");
3133 writeln!(file, "imprint.sections digest={digest:016x} bytes={} pieces={} vertices={}",
3134 bytes.len(), observed.pieces.len(), observed.vertices.len()).expect("append imprint section census");
3135 }
3136 Ok(result)
3137}
3138
3139/// Debug-only graze-contact survey (`BREP_DEBUG_GRAZE=1`): for each section
3140/// piece and each boundary edge of its support faces, sample the piece and
3141/// measure distance to the edge; report contacts whose in-band span exceeds
3142/// both the weld scale and 4× the minimum deviation (span-wise proximity, not
3143/// a point touch). `band_cap` reuses the residual-merge `sep_cap` ceiling —
3144/// measured, never grown. The output is the raw material for capstone step 2
3145/// (partial-span common-block): which contacts exist, their spans, and their
3146/// measured bands.
3147fn report_graze_contacts(
3148 result: &ImprintResultRecord,
3149 face_edge_lists: &HashMap<FaceKey, Vec<&EdgeRecord>>,
3150 solid_a: &BrepSolid,
3151 solid_b: &BrepSolid,
3152 tolerance: f64,
3153) -> Result<(), KernelRefusal> {
3154 if std::env::var("BREP_DEBUG_GRAZE").as_deref() != Ok("1") {
3155 return Ok(());
3156 }
3157 let raw_extent = raw_solid_extent(solid_a).max(raw_solid_extent(solid_b));
3158 let (_, band_cap) = residual_merge_bands(raw_extent, tolerance);
3159 const SAMPLES: usize = 17;
3160 for piece in &result.pieces {
3161 let [t0, t1] = [piece.t0, piece.t1];
3162 if !(t1 > t0) {
3163 continue;
3164 }
3165 for key in piece.support_faces {
3166 let Some(edges) = face_edge_lists.get(&key) else {
3167 continue;
3168 };
3169 for edge in edges {
3170 if edge.degenerate {
3171 continue;
3172 }
3173 let mut in_band = 0usize;
3174 let mut min_dev = f64::INFINITY;
3175 let mut max_dev_in_band = 0.0f64;
3176 let mut span = 0.0f64;
3177 let mut prev: Option<(bool, Vec3)> = None;
3178 for k in 0..SAMPLES {
3179 let t = t0 + (t1 - t0) * k as f64 / (SAMPLES - 1) as f64;
3180 let point = piece.curve.evaluate(t).or_refuse(KernelStage::Intersect, "evaluate")?;
3181 let deviation = project_point_to_curve(&edge.curve, point).or_refuse(KernelStage::Intersect, "project_point_to_curve")?.distance;
3182 min_dev = min_dev.min(deviation);
3183 let inside = deviation <= band_cap;
3184 if inside {
3185 in_band += 1;
3186 max_dev_in_band = max_dev_in_band.max(deviation);
3187 if let Some((true, prev_point)) = prev {
3188 span += point.sub(prev_point).length();
3189 }
3190 }
3191 prev = Some((inside, point));
3192 }
3193 // Span-wise contact: several consecutive samples in band and a
3194 // span that dwarfs the closest-approach (not a transversal
3195 // crossing, which dips in and out at one sample).
3196 if in_band >= 3 && span > (4.0 * min_dev).max(assembler_weld(tolerance)) {
3197 eprintln!(
3198 "graze: piece {} sup=[{}:{},{}:{}] ~ edge {}:{} span={:.3e} band=[{:.3e},{:.3e}] samples_in_band={}/{}",
3199 piece.id,
3200 piece.support_faces[0].operand,
3201 piece.support_faces[0].face_id,
3202 piece.support_faces[1].operand,
3203 piece.support_faces[1].face_id,
3204 key.operand,
3205 edge.id,
3206 span,
3207 min_dev,
3208 max_dev_in_band,
3209 in_band,
3210 SAMPLES
3211 );
3212 }
3213 }
3214 }
3215 }
3216 Ok(())
3217}
3218
3219/// Does `edge` CROSS `carrier` at parameter `t` — leave one side of the
3220/// surface for the other — rather than touch it or lie on it? Read from the
3221/// signed distance of the edge to the carrier a step either side of `t`
3222/// (one hundredth of the edge's live window): a crossing changes sign with
3223/// both readings clear of the tolerance; a lies-on edge reads zero on both
3224/// sides and a kiss reads one sign on both. A hit at the edge's end has no
3225/// far side and is not a crossing. The signed distance is read against the
3226/// carrier itself (projection), never against the tangent plane at the hit,
3227/// where a curve lying ON a curved carrier would read a same-signed sagitta
3228/// on both sides that outweighs a grazing crossing's first-order departure.
3229/// A CLOSED edge (a cap's rim circle) has no end: a hit within a step of its
3230/// seam reads its far side across the seam (the G1 elbow's arm cap pierces
3231/// the lower torus half at t = 0.0037 of its rim circle).
3232fn edge_crosses_carrier(
3233 edge: &EdgeRecord,
3234 t: f64,
3235 carrier: &NurbsSurface,
3236 tolerance: f64,
3237) -> Result<bool, KernelRefusal> {
3238 let window = edge.t1 - edge.t0;
3239 if !(window > 0.0) {
3240 return Ok(false);
3241 }
3242 let delta = window * 1e-2;
3243 let (mut before, mut after) = (t - delta, t + delta);
3244 if before < edge.t0 || after > edge.t1 {
3245 let start = edge.curve.evaluate(edge.t0).or_refuse(KernelStage::Intersect, "evaluate")?;
3246 let end = edge.curve.evaluate(edge.t1).or_refuse(KernelStage::Intersect, "evaluate")?;
3247 if start.sub(end).length() > tolerance {
3248 return Ok(false);
3249 }
3250 if before < edge.t0 {
3251 before += window;
3252 }
3253 if after > edge.t1 {
3254 after -= window;
3255 }
3256 }
3257 let mut readings = [0.0f64; 2];
3258 for (slot, parameter) in [before, after].into_iter().enumerate() {
3259 let point = edge.curve.evaluate(parameter).or_refuse(KernelStage::Intersect, "evaluate")?;
3260 let projection = project_point_to_surface(carrier, point).or_refuse(KernelStage::Intersect, "project_point_to_surface")?;
3261 let normal = match carrier.normal(projection.u, projection.v) {
3262 Ok(normal) => normal,
3263 Err(_) => return Ok(false),
3264 };
3265 readings[slot] = point.sub(projection.point).dot(normal);
3266 }
3267 let clear = tolerance * 10.0;
3268 Ok(readings[0] * readings[1] < 0.0 && readings[0].abs() > clear && readings[1].abs() > clear)
3269}
3270
3271
3272/// `BREP_IMPRINT_LIES_ON_SEED_SKIP=0` runs the seed solve on an edge that lies
3273/// on the other carrier as before (see the seed loop); unset or anything else
3274/// skips it. Read once.
3275fn lies_on_seed_skip_enabled() -> bool {
3276 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3277 *ENABLED.get_or_init(|| std::env::var("BREP_IMPRINT_LIES_ON_SEED_SKIP").as_deref() != Ok("0"))
3278}
3279
3280/// TANGENT-RIM LANDING (hosted report 2026-10-17, "boolean fails": a box
3281/// with a bore, its top face revolved 25° about a top edge and subtracted).
3282///
3283/// The revolve turns the profile's hole into a torus whose start rim IS the
3284/// bore's top rim, so the bore cylinder and the torus are tangent along that
3285/// whole circle — a shared TRIM EDGE of both faces, not an interior contact —
3286/// and inside the trims they cross along two transverse branches that leave
3287/// the rim at the two points where the torus's revolution-direction curvature
3288/// vanishes against the cylinder's. The classifier admits those as isolated
3289/// nodes (`tangent_contact.rs`: a third-order cusp reads as a saddle at every
3290/// stopping rule) and the branch is marched — but the marcher's step direction
3291/// `n_a × n_b` collapses as it nears the node, so the trace STALLS a few 1e-5
3292/// short of the rim instead of running past the trim with its usual slack.
3293/// Measured: the run ends 6.95e-5 and 1.17e-4 below y = 20, which on the
3294/// cylinder's 30-tall chart is 2.3e-6 and 3.9e-6 — just over the arrangement's
3295/// 1.4e-6 band — so the cut never reaches the boundary, the cylinder face is
3296/// not split, and six edges stand one-use (the torus faces, whose pcurve is
3297/// clamped at the rim's domain wall, DID split there, which is the asymmetry
3298/// the one-use count reads). The exact `A − B` is a manifold solid (two
3299/// slivers of box material inside the torus tube pinching to a zero-dihedral
3300/// edge along the rim), so the refusal was wrong, not the model.
3301///
3302/// A transverse section cannot end strictly inside both trims; a run end that
3303/// stops within the on-boundary clearance (`polish_endpoint`'s band) of a trim
3304/// edge which lies on BOTH carriers at the foot of that stop has merely been
3305/// truncated, and the foot — on the edge, hence on both carriers exactly — is
3306/// where the exact section continues to. Land the end there and split the
3307/// edge at it, so the ordinary machinery (pcurve projection, the arrangement's
3308/// boundary split, the assembler's vertex weld) takes the junction from here.
3309/// Reach: pairs carrying an admitted tangent node (the only place the march
3310/// stalls short — elsewhere it overshoots the trim), a foot within
3311/// `1e-2·(1 + |foot|)` of that node, and an end not already on the edge. The
3312/// moved sample is the run's own last station, so the section polyline keeps
3313/// every marched point and the landing segment is at most the clearance long.
3314/// Returns the splits to register, one per edge sharing the landing point:
3315/// `(operand, edge id, parameter, from, to)`.
3316///
3317/// Without an admitted node (`node` is `None`: the pair was marched from
3318/// sub-gate pierces, or its branch simply ran into a tangency — the −10°
3319/// sibling of the revolve-into-bore report, a near-tangent pair with no node
3320/// admitted whose ordinary march stalled 4e-5 and 2.4e-4 short of its rim),
3321/// the certificate is the run end itself: it must sit where the carriers are
3322/// TANGENT (`|n_a × n_b|` under the seed gate), which is the only place a
3323/// march stalls short. With or without a node the reach is `1e-2 (1 +
3324/// |point|)`, and a gap beyond the plain clearance is earned sample by
3325/// sample: the straight landing segment must stay within the on-boundary
3326/// clearance of BOTH carriers at every sample — a section through a tangency
3327/// band is within tolerance of both carriers all the way to the rim, so a
3328/// segment that leaves either is not the missing stretch (the −20° sibling's
3329/// 7x6 end stops 3e-4 short, over the plain clearance of 2.1e-4). A gap
3330/// within the plain clearance lands exactly as before.
3331fn land_run_ends_on_tangent_rim(
3332 run: &mut [Vec3],
3333 node: Option<Vec3>,
3334 first: TaggedFace<'_>,
3335 second: TaggedFace<'_>,
3336 face_edge_lists: &HashMap<FaceKey, Vec<&EdgeRecord>>,
3337 tolerance: f64,
3338) -> Result<Vec<(u8, u64, f64, Vec3, Vec3)>, KernelRefusal> {
3339 let mut landings = Vec::new();
3340 if run.len() < 2 || std::env::var("BREP_TANGENT_RIM_LANDING").as_deref() == Ok("0") {
3341 return Ok(landings);
3342 }
3343 for index in [0, run.len() - 1] {
3344 let point = run[index];
3345 let clearance = (tolerance * 100.0).max(1e-6) * (1.0 + point.length());
3346 // No node: the end must stand in the tangency band, and the reach
3347 // beyond the plain clearance is earned sample by sample below.
3348 let stalled = match node {
3349 Some(_) => true,
3350 None => pair_normals_parallel_at(first, second, point)?,
3351 };
3352 if !stalled {
3353 continue;
3354 }
3355 let reach = 1e-2 * (1.0 + point.length());
3356 // Every qualifying foot over both faces' trim edges; the nearest is
3357 // where the end lands, and every edge whose foot coincides with it
3358 // (the two operands' copies of the shared rim) is split there, so
3359 // neither side depends on its pcurve happening to clamp at a domain
3360 // wall for its vertex.
3361 let mut feet: Vec<(f64, u8, u64, f64, Vec3)> = Vec::new();
3362 for face in [first, second] {
3363 let Some(edges) = face_edge_lists.get(&face.key()) else {
3364 continue;
3365 };
3366 for edge in edges {
3367 if edge.degenerate {
3368 continue;
3369 }
3370 let projection = project_point_to_curve(&edge.curve, point)
3371 .or_refuse(KernelStage::Intersect, "project_point_to_curve")?;
3372 let parameter = projection.u.clamp(edge.t0.min(edge.t1), edge.t0.max(edge.t1));
3373 let foot = edge.curve.evaluate(parameter).or_refuse(KernelStage::Intersect, "evaluate")?;
3374 let gap = foot.sub(point).length();
3375 if gap <= tolerance || gap > reach {
3376 continue;
3377 }
3378 if let Some(node) = node {
3379 if foot.sub(node).length() > 1e-2 * (1.0 + foot.length()) {
3380 continue;
3381 }
3382 }
3383 if gap > clearance
3384 && !landing_segment_within(&first.face.surface, &second.face.surface, point, foot, clearance)
3385 {
3386 continue;
3387 }
3388 // The edge must ride BOTH carriers at the foot: that is what
3389 // makes the foot a point of the section.
3390 let on_both = [first, second].iter().all(|face| {
3391 project_point_to_surface(&face.face.surface, foot)
3392 .map(|projection| projection.distance <= clearance)
3393 .unwrap_or(false)
3394 });
3395 if !on_both {
3396 continue;
3397 }
3398 feet.push((gap, face.operand, edge.id, parameter, foot));
3399 }
3400 }
3401 let Some(&(_, _, _, _, landing)) = feet.iter().min_by(|a, b| a.0.total_cmp(&b.0)) else {
3402 continue;
3403 };
3404 for (_, operand, edge_id, parameter, foot) in feet {
3405 if foot.sub(landing).length() <= tolerance * 10.0 {
3406 landings.push((operand, edge_id, parameter, point, landing));
3407 }
3408 }
3409 run[index] = landing;
3410 }
3411 Ok(landings)
3412}
3413/// The widened landing's certificate: is the straight segment from the
3414/// stalled run end `from` to the foot `to` within `clearance` of BOTH
3415/// carriers at every interior sample?
3416///
3417/// Why this is a construction guarantee and not a reading: the foot is on a
3418/// trim edge that rides both carriers (checked by the caller), so it is a
3419/// point of the carriers' intersection set to within the clearance. The run
3420/// end is a marched station, on both carriers to the corrector's residual.
3421/// A segment whose every sample is within the clearance of both carriers is
3422/// a chord of the band where the two carriers are within tolerance of each
3423/// other — exactly the set a section through a tangency is confined to —
3424/// and the section polyline's last chord is held to the same deviation every
3425/// marched chord is. A segment that leaves either carrier mid-way (a chord
3426/// across a curved rim: its sagitta clears the clearance by orders) is NOT
3427/// the missing stretch, whatever its ends read, and is refused here.
3428fn landing_segment_within(
3429 first: &NurbsSurface,
3430 second: &NurbsSurface,
3431 from: Vec3,
3432 to: Vec3,
3433 clearance: f64,
3434) -> bool {
3435 const SAMPLES: usize = 8;
3436 (1..SAMPLES).all(|k| {
3437 let sample = from.add(to.sub(from).scale(k as f64 / SAMPLES as f64));
3438 [first, second].iter().all(|surface| {
3439 project_point_to_surface(surface, sample)
3440 .map(|projection| projection.distance <= clearance)
3441 .unwrap_or(false)
3442 })
3443 })
3444}
3445
3446/// Are the two carriers TANGENT (normals parallel) where they both pass through
3447/// `point`?
3448///
3449/// The test the tangential-only refusal above needs: a point on the marched
3450/// intersection is a TANGENT NODE when the two surface normals there are
3451/// parallel. The threshold is the transversality bound the supplemental
3452/// detector already accepts seeds by (`TRANSVERSE_SEED_CROSS`), not the far
3453/// tighter pair-classifier bound — a node the march merely passes CLOSE to
3454/// still poisons the assembly, so this errs toward calling a pair singular.
3455/// The tangent-curve lane's recognition of one pair: find the pair's
3456/// tangential contact without a witness, keep only an EXTENDED one (a node or
3457/// a point contact is the existing paths' business), and with one planar
3458/// carrier march it along the null direction of `II_a − II_b` and clip the
3459/// curve to both trims. `None` when there is no extended contact, or its trace
3460/// has no run inside both trims (a contact on the carriers' extensions
3461/// imprints nothing, exactly as today).
3462///
3463/// The section is RECORDED, not imprinted: a tangent edge whose two sides
3464/// both survive is a four-coedge incidence the assembler cannot represent
3465/// (the pinch gate in `csg/boolean/mod.rs` names the subtract case), and a
3466/// tangent edge only one side of which survives dissolves into its face.
3467/// What the record buys is the boolean's attribution: a refusal whose faces
3468/// the contact involves names the contact instead of the arrangement's own
3469/// symptom. The involved faces are the two carriers' and every face sharing
3470/// an edge a run end lies on (`TangentSectionRecord::faces`).
3471#[allow(clippy::too_many_arguments)]
3472fn tangent_curve_section(
3473 first: TaggedFace<'_>,
3474 second: TaggedFace<'_>,
3475 options: &ImprintOptions,
3476 first_faces: &[TaggedFace<'_>],
3477 second_faces: &[TaggedFace<'_>],
3478 face_edge_lists: &HashMap<FaceKey, Vec<&EdgeRecord>>,
3479 debug_pairs: bool,
3480) -> Result<Option<(TangentSectionRecord, Vec<Vec3>)>, KernelRefusal> {
3481 let _clock = ImprintSubphaseClock::new("tangent_section");
3482 let Some(contact) =
3483 crate::find_tangent_contact(&first.face.surface, &second.face.surface, options.tolerance)
3484 .or_refuse(KernelStage::Intersect, "find_tangent_contact")?
3485 else {
3486 return Ok(None);
3487 };
3488 if debug_pairs {
3489 eprintln!(
3490 "pair {}x{}: tangent-curve lane contact {:?}",
3491 first.face.id, second.face.id, contact
3492 );
3493 }
3494 if !contact.is_extended() {
3495 return Ok(None);
3496 }
3497 let is_plane = |face: TaggedFace<'_>| {
3498 matches!(face.face.surface.analytic(), Some(crate::AnalyticSurface::Plane { .. }))
3499 };
3500 let planar = is_plane(first) || is_plane(second);
3501 if !planar {
3502 // The curved/curved branch: recognised and named, not traced.
3503 return Ok(Some((
3504 TangentSectionRecord {
3505 first: first.key(),
3506 second: second.key(),
3507 start: contact.point,
3508 end: contact.point,
3509 length: 0.0,
3510 closed: false,
3511 stations: 0,
3512 planar: false,
3513 faces: vec![first.key(), second.key()],
3514 piece_ids: Vec::new(),
3515 grazing: Vec::new(),
3516 },
3517 Vec::new(),
3518 )));
3519 }
3520 let Some(curve) = crate::trace_tangent_curve(
3521 &first.face.surface,
3522 &second.face.surface,
3523 &contact,
3524 &SurfaceIntersectionOptions {
3525 tolerance: options.tolerance,
3526 maximum_step: options.maximum_ssi_step,
3527 ..Default::default()
3528 },
3529 )
3530 .or_refuse(KernelStage::Intersect, "trace_tangent_curve")?
3531 else {
3532 return Ok(None);
3533 };
3534 let mut longest: Option<Vec<Vec3>> = None;
3535 let mut longest_length = 0.0f64;
3536 for run in clip_branch_to_trims(&curve.points, first, second)? {
3537 if run.len() < 2 {
3538 continue;
3539 }
3540 let length: f64 = run.windows(2).map(|pair| pair[1].sub(pair[0]).length()).sum();
3541 if length > longest_length {
3542 longest_length = length;
3543 longest = Some(run);
3544 }
3545 }
3546 let Some(run) = longest else {
3547 return Ok(None);
3548 };
3549 if longest_length <= options.tolerance * 100.0 {
3550 return Ok(None);
3551 }
3552 if debug_pairs {
3553 eprintln!(
3554 "pair {}x{}: tangent-curve lane traced {} stations, run of {} inside both trims, length {:.6}, closed {}",
3555 first.face.id, second.face.id, curve.points.len(), run.len(), longest_length, curve.closed
3556 );
3557 }
3558 let closed = curve.closed && run.len() == curve.points.len();
3559 // The faces the contact involves: the carriers' own, and across every
3560 // edge of theirs that a run end lies on, the faces sharing that edge.
3561 let mut faces = vec![first.key(), second.key()];
3562 if !closed {
3563 let band = options.tolerance * 100.0;
3564 for end in [run[0], run[run.len() - 1]] {
3565 for face in [first, second] {
3566 let Some(edges) = face_edge_lists.get(&face.key()) else {
3567 continue;
3568 };
3569 for edge in edges {
3570 if edge.degenerate {
3571 continue;
3572 }
3573 let Ok(projection) = project_point_to_curve(&edge.curve, end) else {
3574 continue;
3575 };
3576 if projection.distance > band {
3577 continue;
3578 }
3579 let siblings = if face.operand == first.operand { first_faces } else { second_faces };
3580 for sibling in siblings {
3581 let shares = sibling
3582 .face
3583 .loops
3584 .iter()
3585 .flat_map(|loop_record| &loop_record.coedges)
3586 .any(|coedge| coedge.edge_id == edge.id);
3587 if shares && !faces.contains(&sibling.key()) {
3588 faces.push(sibling.key());
3589 }
3590 }
3591 }
3592 }
3593 }
3594 }
3595 if debug_pairs {
3596 eprintln!(
3597 "pair {}x{}: tangent-curve lane involves faces {:?}",
3598 first.face.id, second.face.id, faces
3599 );
3600 }
3601 Ok(Some((
3602 TangentSectionRecord {
3603 first: first.key(),
3604 second: second.key(),
3605 start: run[0],
3606 end: run[run.len() - 1],
3607 length: longest_length,
3608 closed,
3609 stations: run.len(),
3610 planar: true,
3611 faces,
3612 piece_ids: Vec::new(),
3613 grazing: Vec::new(),
3614 },
3615 run,
3616 )))
3617}
3618
3619/// Imprint a traced tangent run ([`ImprintOptions::tangent_curve_imprint`]):
3620/// the run's stations become a section curve — the exact line through its
3621/// ends when they are collinear (a plane's contact with a cylinder or cone is
3622/// a generatrix), the marched lane's polyline fit otherwise (a torus parallel)
3623/// — and `process_curve` mints it on both carriers exactly as it mints a
3624/// transverse section: pieces with pcurves on each face, the operand edges the
3625/// ends lie on split there. Returns the ids of the pieces minted, in order.
3626///
3627/// Only a run that SPLITS both faces is minted: a closed run, or one each of
3628/// whose ends lies, on each carrier, on that face's trim or on a section
3629/// piece already minted on it (the through-bore's contact ends on the wall's
3630/// top and bottom edges and, on the bore, on the circles the box's top and
3631/// bottom cut there). A run with an end inside a face — the blind pocket's
3632/// contact ends on the bore's cap rings but inside the wall, where both of
3633/// its sides are one face — would be a slit, and a slit is not a boundary:
3634/// measured with the slit minted, the wall's four edges came out one-use and
3635/// the pocket refused `NonIntegralGenus`. Such a contact stays recorded only,
3636/// as before.
3637#[allow(clippy::too_many_arguments)]
3638fn imprint_tangent_run(
3639 builder: &mut ImprintBuilder<'_>,
3640 run: &[Vec3],
3641 closed: bool,
3642 first: TaggedFace<'_>,
3643 second: TaggedFace<'_>,
3644 face_edge_lists: &HashMap<FaceKey, Vec<&EdgeRecord>>,
3645 options: &ImprintOptions,
3646 debug: bool,
3647) -> Result<Vec<u64>, KernelRefusal> {
3648 if run.len() < 2 {
3649 return Ok(Vec::new());
3650 }
3651 if !closed {
3652 let band = options.tolerance * 100.0;
3653 let within = |curve: &NurbsCurve, t0: f64, t1: f64, point: Vec3| -> bool {
3654 project_point_to_curve(curve, point)
3655 .map(|projection| {
3656 projection.distance <= band
3657 && projection.u >= t0.min(t1) - 1e-9
3658 && projection.u <= t0.max(t1) + 1e-9
3659 })
3660 .unwrap_or(false)
3661 };
3662 let on_boundary = |face: TaggedFace<'_>, point: Vec3| -> bool {
3663 let on_trim = face_edge_lists
3664 .get(&face.key())
3665 .map(|edges| {
3666 edges
3667 .iter()
3668 .any(|edge| !edge.degenerate && within(&edge.curve, edge.t0, edge.t1, point))
3669 })
3670 .unwrap_or(false);
3671 on_trim
3672 || builder
3673 .pieces
3674 .iter()
3675 .any(|piece| piece.support_faces.contains(&face.key()) && within(&piece.curve, piece.t0, piece.t1, point))
3676 };
3677 let ends = [run[0], run[run.len() - 1]];
3678 for face in [first, second] {
3679 if !ends.iter().all(|end| on_boundary(face, *end)) {
3680 if debug {
3681 eprintln!(
3682 "pair {}x{}: tangent-curve lane run not imprinted: an end lies inside face {}:{}'s trim (a slit)",
3683 first.face.id, second.face.id, face.operand, face.face.id
3684 );
3685 }
3686 return Ok(Vec::new());
3687 }
3688 }
3689 }
3690 let start = run[0];
3691 let end = run[run.len() - 1];
3692 let chord = end.sub(start);
3693 let chord_length = chord.length();
3694 let fit_tolerance = options.tolerance.max(1e-7);
3695 let collinear = chord_length > fit_tolerance && {
3696 let direction = chord.scale(1.0 / chord_length);
3697 run.iter().all(|station| {
3698 let offset = station.sub(start);
3699 offset.sub(direction.scale(offset.dot(direction))).length() <= 10.0 * fit_tolerance
3700 })
3701 };
3702 let curve = if collinear {
3703 crate::make_line(start, end).or_refuse(KernelStage::Intersect, "make_line")?
3704 } else {
3705 fit_polyline_of_degree(
3706 run,
3707 fit_tolerance,
3708 run.len().min(MAX_FIT_STATIONS),
3709 options.local_fit,
3710 section_fit_degree(),
3711 )
3712 .or_refuse(KernelStage::Intersect, "fit_polyline_of_degree")?
3713 .curve
3714 };
3715 let before = builder.pieces.len();
3716 builder.process_curve(curve, first, second, &[first, second], &[first, second], true, None)?;
3717 let ids: Vec<u64> = builder.pieces[before..].iter().map(|piece| piece.id).collect();
3718 if debug {
3719 eprintln!(
3720 "pair {}x{}: tangent-curve lane imprinted the run as {} ({} piece(s) {:?})",
3721 first.face.id,
3722 second.face.id,
3723 if collinear { "a line" } else { "a fitted section" },
3724 ids.len(),
3725 ids
3726 );
3727 }
3728 Ok(ids)
3729}
3730
3731/// Split every piece an imprinted tangent section ENDS ON, at that end
3732/// (`ImprintOptions::tangent_curve_imprint`). A tangent run is clipped to both
3733/// trims, so its ends lie on operand edges — split by `process_curve` — or on
3734/// the trim of the OTHER carrier's neighbours, where they also lie on the
3735/// sections those neighbours minted on this carrier: the through-bore's
3736/// contact line ends on the circles the box's top and bottom cut on the bore,
3737/// at a point no edge pierce put a vertex on (the box edge grazes the bore
3738/// there, and a grazing pierce is deliberately not a junction). Without the
3739/// vertex, the bore's arrangement would split the circle where the line
3740/// meets it while the cap's arrangement keeps the circle whole — one closed
3741/// edge against two arcs, one-use at assembly. The piece is split here, at
3742/// the imprint, so every face it is minted on sees the vertex: the halves
3743/// take the tangent end's vertex id, the pcurves are split with the curve
3744/// where they were emitted on its knots and rebuilt on the face's chart
3745/// otherwise, and the piece's face lists and lane sets follow the halves. A
3746/// piece that resolves to an operand edge (`shared_edge`) is left as it is:
3747/// that edge's own split is the exchange pass's.
3748fn split_pieces_at_tangent_ends(
3749 builder: &mut ImprintBuilder<'_>,
3750 sections: &[TangentSectionRecord],
3751 first_faces: &[TaggedFace<'_>],
3752 second_faces: &[TaggedFace<'_>],
3753 options: &ImprintOptions,
3754 debug: bool,
3755) -> Result<(), KernelRefusal> {
3756 let tangent_ids: HashSet<u64> = sections.iter().flat_map(|section| section.piece_ids.iter().copied()).collect();
3757 if tangent_ids.is_empty() {
3758 return Ok(());
3759 }
3760 let overlap_limit = options.tolerance.max(1e-5);
3761 let vertex_points: HashMap<u64, Vec3> =
3762 builder.vertices.iter().map(|vertex| (vertex.id, vertex.point)).collect();
3763 // (end vertex, its point, the tangent piece's supports)
3764 let mut ends: Vec<(u64, Vec3, [FaceKey; 2])> = Vec::new();
3765 for piece in builder.pieces.iter().filter(|piece| tangent_ids.contains(&piece.id)) {
3766 for vertex_id in [piece.start_vertex_id, piece.end_vertex_id] {
3767 if let Some(point) = vertex_points.get(&vertex_id) {
3768 if !ends.iter().any(|(id, _, _)| *id == vertex_id) {
3769 ends.push((vertex_id, *point, piece.support_faces));
3770 }
3771 }
3772 }
3773 }
3774 let face_of = |key: FaceKey| {
3775 first_faces
3776 .iter()
3777 .chain(second_faces)
3778 .copied()
3779 .find(|face| face.key() == key)
3780 };
3781 let mut index = 0;
3782 while index < builder.pieces.len() {
3783 let piece = &builder.pieces[index];
3784 if tangent_ids.contains(&piece.id) || piece.shared_edge.is_some() || !(piece.t1 > piece.t0) {
3785 index += 1;
3786 continue;
3787 }
3788 let mut split: Option<(u64, f64, Vec3)> = None;
3789 for (vertex_id, point, supports) in &ends {
3790 if *vertex_id == piece.start_vertex_id || *vertex_id == piece.end_vertex_id {
3791 continue;
3792 }
3793 if !supports.iter().any(|support| piece.support_faces.contains(support)) {
3794 continue;
3795 }
3796 let projection = project_point_to_curve(&piece.curve, *point)
3797 .or_refuse(KernelStage::Intersect, "project_point_to_curve")?;
3798 if projection.distance > overlap_limit {
3799 continue;
3800 }
3801 let span = piece.t1 - piece.t0;
3802 let margin = (span * KNOT_IDENTITY_TOL).max(2e-9);
3803 if projection.u <= piece.t0 + margin || projection.u >= piece.t1 - margin {
3804 continue;
3805 }
3806 // Interior in PARAMETER but at an END in space: a closed piece's
3807 // origin seen from the far side of its domain.
3808 if projection.point.sub(vertex_points[&piece.start_vertex_id]).length() <= overlap_limit
3809 || projection.point.sub(vertex_points[&piece.end_vertex_id]).length() <= overlap_limit
3810 {
3811 continue;
3812 }
3813 split = Some((*vertex_id, projection.u, *point));
3814 break;
3815 }
3816 let Some((vertex_id, parameter, point)) = split else {
3817 index += 1;
3818 continue;
3819 };
3820 let piece = builder.pieces.remove(index);
3821 let (left, right) = piece.curve.split(parameter).or_refuse(KernelStage::Intersect, "split")?;
3822 let [left_t0, _] = left.domain().or_refuse(KernelStage::Intersect, "domain")?;
3823 let [_, right_t1] = right.domain().or_refuse(KernelStage::Intersect, "domain")?;
3824 let start_point = vertex_points[&piece.start_vertex_id];
3825 let end_point = vertex_points[&piece.end_vertex_id];
3826 let left = snap_curve_ends(&left, start_point, point)?;
3827 let right = snap_curve_ends(&right, point, end_point)?;
3828 let left_id = builder.next_id;
3829 let right_id = builder.next_id + 1;
3830 builder.next_id += 2;
3831 let mut left_pcurves = Vec::with_capacity(piece.pcurves.len());
3832 let mut right_pcurves = Vec::with_capacity(piece.pcurves.len());
3833 for face_pcurve in &piece.pcurves {
3834 let key = FaceKey { operand: face_pcurve.operand, face_id: face_pcurve.face_id };
3835 let emitted = builder.emitted_pcurves.remove(&(piece.id, key));
3836 let halves = if emitted {
3837 match face_pcurve.pcurve.split(parameter) {
3838 Ok((a, b)) => {
3839 builder.emitted_pcurves.insert((left_id, key));
3840 builder.emitted_pcurves.insert((right_id, key));
3841 Some((a, b))
3842 }
3843 Err(_) => None,
3844 }
3845 } else {
3846 None
3847 };
3848 let (a, b) = match halves {
3849 Some(halves) => halves,
3850 None => {
3851 let Some(face) = face_of(key) else {
3852 return Err(KernelRefusal::internal(
3853 KernelStage::Intersect,
3854 "imprint.tangent_split",
3855 format!("tangent-section split: piece {} has a pcurve on an unknown face {key:?}", piece.id),
3856 ));
3857 };
3858 (
3859 build_pcurve_on_surface_marched(face.chart(), &left)
3860 .or_refuse(KernelStage::Intersect, "build_pcurve_on_surface_marched")?,
3861 build_pcurve_on_surface_marched(face.chart(), &right)
3862 .or_refuse(KernelStage::Intersect, "build_pcurve_on_surface_marched")?,
3863 )
3864 }
3865 };
3866 left_pcurves.push(FacePcurve { operand: key.operand, face_id: key.face_id, pcurve: a });
3867 right_pcurves.push(FacePcurve { operand: key.operand, face_id: key.face_id, pcurve: b });
3868 }
3869 if builder.marched_pieces.remove(&piece.id) {
3870 builder.marched_pieces.insert(left_id);
3871 builder.marched_pieces.insert(right_id);
3872 }
3873 for ids in builder.by_face.values_mut() {
3874 if let Some(slot) = ids.iter().position(|id| *id == piece.id) {
3875 ids[slot] = left_id;
3876 ids.insert(slot + 1, right_id);
3877 }
3878 }
3879 if debug {
3880 eprintln!(
3881 "tangent-curve lane: piece {} split at vertex {} (t={parameter:.6}) into pieces {left_id} and {right_id}",
3882 piece.id, vertex_id
3883 );
3884 }
3885 builder.pieces.insert(
3886 index,
3887 ImprintPieceRecord {
3888 id: left_id,
3889 curve: left,
3890 t0: left_t0,
3891 t1: parameter,
3892 start_vertex_id: piece.start_vertex_id,
3893 end_vertex_id: vertex_id,
3894 pcurves: left_pcurves,
3895 support_faces: piece.support_faces,
3896 shared_edge: None,
3897 },
3898 );
3899 builder.pieces.insert(
3900 index + 1,
3901 ImprintPieceRecord {
3902 id: right_id,
3903 curve: right,
3904 t0: parameter,
3905 t1: right_t1,
3906 start_vertex_id: vertex_id,
3907 end_vertex_id: piece.end_vertex_id,
3908 pcurves: right_pcurves,
3909 support_faces: piece.support_faces,
3910 shared_edge: None,
3911 },
3912 );
3913 // Re-examine the halves: a run may end on the same piece twice.
3914 }
3915 Ok(())
3916}
3917
3918fn pair_normals_parallel_at(
3919 first: TaggedFace<'_>,
3920 second: TaggedFace<'_>,
3921 point: Vec3,
3922) -> Result<bool, KernelRefusal> {
3923 let mut normals = [Vec3::default(); 2];
3924 for (slot, face) in normals.iter_mut().zip([first, second]) {
3925 let projection = project_point_to_surface(&face.face.surface, point)
3926 .or_refuse(KernelStage::Intersect, "project_point_to_surface")?;
3927 let Ok(normal) = face.face.surface.normal(projection.u, projection.v) else {
3928 // A pole/singular parameter point cannot witness transversality;
3929 // treat it as tangential so the pair stays refused.
3930 return Ok(true);
3931 };
3932 *slot = normal;
3933 }
3934 Ok(normals[0].cross(normals[1]).length() <= crate::TRANSVERSE_SEED_CROSS)
3935}
3936