brep_kernel/geometry/pcurve.rs
1use crate::{
2 interpolate_curve, project_point_to_surface, project_point_to_surface_seeded, AnalyticSurface,
3 DiagnosticSeverity, KernelDiagnostics, KernelStage, KnotVector, NurbsCurve, NurbsSurface, Vec3,
4 Vec4, KNOT_IDENTITY_TOL,
5};
6
7const EPSILON: f64 = 1e-12;
8const LINEAR_TOLERANCE: f64 = 1e-7;
9
10/// Coarse clocks only: no per-station timing overhead in a measured fit.
11struct PcurvePhaseClock {
12 started: Option<web_time::Instant>,
13 phase: &'static str,
14 samples: usize,
15 tracking: bool,
16}
17impl PcurvePhaseClock {
18 fn new(phase: &'static str, samples: usize, tracking: bool) -> Self {
19 Self { started: std::env::var_os("BREP_PROFILE").is_some().then(web_time::Instant::now), phase, samples, tracking }
20 }
21}
22impl Drop for PcurvePhaseClock {
23 fn drop(&mut self) {
24 if let Some(started) = self.started {
25 eprintln!("pcurve.phase_time phase={} initial_samples={} tracking={} ms={:.3}", self.phase, self.samples, self.tracking, started.elapsed().as_secs_f64()*1000.0);
26 }
27 }
28}
29
30fn surface_domains(surface: &NurbsSurface) -> Result<([f64; 2], [f64; 2]), String> {
31 Ok((
32 KnotVector::new(surface.knots_u.clone(), surface.degree_u)?.domain(),
33 KnotVector::new(surface.knots_v.clone(), surface.degree_v)?.domain(),
34 ))
35}
36
37/// The carrier's closed directions, as [`NurbsSurface::closed_directions`]
38/// reads them (cached on the surface, seam tolerance 1e-6, and a sliver patch
39/// whose rows coincide without the surface travelling between them is OPEN).
40/// Until 2026-09-26 this was a second reading with its own tolerance; the
41/// unwrap in [`build_interpolant`] and the wrap in `evaluate_extended` must
42/// agree on what a seam is, or a trim across a sliver reads a period wide.
43fn surface_closedness(surface: &NurbsSurface) -> Result<(bool, bool), String> {
44 surface.closed_directions()
45}
46
47thread_local! {
48 static PCURVE_REJECT_BOUNDS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
49}
50struct PcurveRejectScope(bool);
51impl PcurveRejectScope {
52 fn open(enabled: bool) -> Self {
53 Self(PCURVE_REJECT_BOUNDS.with(|state| state.replace(enabled)))
54 }
55}
56impl Drop for PcurveRejectScope {
57 fn drop(&mut self) { PCURVE_REJECT_BOUNDS.with(|state| state.set(self.0)); }
58}
59
60thread_local! {
61 static PCURVE_STACK_POINT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
62}
63struct PcurveStackPointScope(bool);
64impl PcurveStackPointScope {
65 fn open(enabled: bool) -> Self { Self(PCURVE_STACK_POINT.with(|state| state.replace(enabled))) }
66}
67impl Drop for PcurveStackPointScope {
68 fn drop(&mut self) { PCURVE_STACK_POINT.with(|state| state.set(self.0)); }
69}
70
71/// The count-zero basis recurrence has the same operations as the value
72/// basis recurrence. Its homogeneous sum uses the same row/column order.
73/// Preserve derivative dehomogenization (multiply by reciprocal, not divide)
74/// while avoiding both the derivative grids and their allocation. Other
75/// degrees and open-direction extension keep the original API. Periodic
76/// wrap retains the original parameter arithmetic before value evaluation.
77fn section_extended_point(surface: &NurbsSurface, u: f64, v: f64, enabled: bool) -> Result<Vec3, String> {
78 if enabled && u.is_finite() && v.is_finite()
79 && surface.degree_u <= crate::curve::MAX_STACK_DEGREE
80 && surface.degree_v <= crate::curve::MAX_STACK_DEGREE {
81 let [u0,u1] = surface.domain_u()?;
82 let [v0,v1] = surface.domain_v()?;
83 let mut uu = u;
84 let mut vv = v;
85 if u < u0 || u > u1 || v < v0 || v > v1 {
86 let (closed_u, closed_v) = surface_closedness(surface)?;
87 // Identical to derivatives_extended: keep the original add,
88 // subtract and remainder order before taking the count-zero path.
89 if closed_u && (u < u0 || u > u1) { uu = u0 + (u - u0).rem_euclid(u1 - u0); }
90 if closed_v && (v < v0 || v > v1) { vv = v0 + (v - v0).rem_euclid(v1 - v0); }
91 }
92 if uu >= u0 && uu <= u1 && vv >= v0 && vv <= v1 {
93 let point = surface.evaluate_homogeneous(uu, vv)?;
94 if point.w.abs() <= EPSILON {
95 return Err("NurbsSurface: zero evaluated weight".into());
96 }
97 return Ok(Vec3::new(point.x, point.y, point.z).scale(1.0 / point.w));
98 }
99 }
100 surface.evaluate_extended(u,v)
101}
102
103/// One immutable sphere/section pair; nested fits own separate maps. Keys
104/// preserve all parameter bits, and failed evaluations are never retained.
105struct PcurveSampleCache {
106 surface: usize,
107 curve: usize,
108 curves: rustc_hash::FxHashMap<u64, Vec3>,
109 surfaces: rustc_hash::FxHashMap<(u64, u64, bool), Vec3>,
110 inversions: rustc_hash::FxHashMap<[u64; 3], ([f64; 2], f64)>,
111 requests: [usize; 4],
112 hits: [usize; 4],
113 speeds: rustc_hash::FxHashMap<(u64, u64), f64>,
114}
115thread_local! {
116 static PCURVE_SAMPLES: std::cell::RefCell<Vec<Option<PcurveSampleCache>>> = const { std::cell::RefCell::new(Vec::new()) };
117}
118struct PcurveSampleScope;
119impl PcurveSampleScope {
120 fn open(surface: &NurbsSurface, curve: &NurbsCurve, enabled: bool) -> Self {
121 let cache = (enabled && matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })))
122 .then(|| PcurveSampleCache { surface: surface as *const _ as usize,
123 curve: curve as *const _ as usize, curves: Default::default(),
124 surfaces: Default::default(), inversions: Default::default(), requests: [0; 4], hits: [0; 4], speeds: Default::default() });
125 PCURVE_SAMPLES.with(|stack| stack.borrow_mut().push(cache));
126 Self
127 }
128}
129impl Drop for PcurveSampleScope {
130 fn drop(&mut self) {
131 PCURVE_SAMPLES.with(|stack| {
132 if let Some(Some(cache)) = stack.borrow_mut().pop() {
133 if std::env::var_os("BREP_PROFILE").is_some() {
134 eprintln!("pcurve.samples requests={:?} hits={:?} retained={:?}", cache.requests,
135 cache.hits, [cache.curves.len(), cache.surfaces.len(), cache.inversions.len(), cache.speeds.len()]);
136 }
137 }
138 });
139 }
140}
141const PCURVE_SAMPLE_CACHE_LIMIT: usize = 16384;
142fn section_u_speed(surface: &NurbsSurface, u: f64, v: f64) -> Result<f64, String> {
143 let key = (u.to_bits(), v.to_bits());
144 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
145 let mut stack = stack.borrow_mut();
146 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
147 if cache.surface != surface as *const _ as usize { return (false, None) }
148 cache.requests[3] += 1;
149 let hit = cache.speeds.get(&key).copied();
150 cache.hits[3] += usize::from(hit.is_some());
151 (true, hit)
152 });
153 if let Some(hit) = hit { return Ok(hit) }
154 let speed = surface.deriv1(u, v)?.1.length();
155 if active { PCURVE_SAMPLES.with(|stack| {
156 let mut stack = stack.borrow_mut();
157 let cache = stack.last_mut().unwrap().as_mut().unwrap();
158 if cache.speeds.len() < PCURVE_SAMPLE_CACHE_LIMIT { cache.speeds.insert(key, speed); }
159 }); }
160 Ok(speed)
161}
162
163fn section_curve_point(curve: &NurbsCurve, parameter: f64) -> Result<Vec3, String> {
164 let key = parameter.to_bits();
165 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
166 let mut stack = stack.borrow_mut();
167 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
168 if cache.curve != curve as *const _ as usize { return (false, None) }
169 cache.requests[0] += 1;
170 let hit = cache.curves.get(&key).copied();
171 cache.hits[0] += usize::from(hit.is_some());
172 (true, hit)
173 });
174 if let Some(hit) = hit { return Ok(hit) }
175 let point = curve.evaluate(parameter)?;
176 if active { PCURVE_SAMPLES.with(|stack| {
177 let mut stack = stack.borrow_mut();
178 let cache = stack.last_mut().unwrap().as_mut().unwrap();
179 if cache.curves.len() < PCURVE_SAMPLE_CACHE_LIMIT { cache.curves.insert(key, point); }
180 }); }
181 Ok(point)
182}
183fn section_surface_point(surface: &NurbsSurface, u: f64, v: f64, extended: bool) -> Result<Vec3, String> {
184 let key = (u.to_bits(), v.to_bits(), extended);
185 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
186 let mut stack = stack.borrow_mut();
187 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
188 if cache.surface != surface as *const _ as usize { return (false, None) }
189 cache.requests[1] += 1;
190 let hit = cache.surfaces.get(&key).copied();
191 cache.hits[1] += usize::from(hit.is_some());
192 (true, hit)
193 });
194 if let Some(hit) = hit { return Ok(hit) }
195 let point = if extended { section_extended_point(surface, u, v, PCURVE_STACK_POINT.with(|state| state.get()))? } else { surface.evaluate(u, v)? };
196 if active { PCURVE_SAMPLES.with(|stack| {
197 let mut stack = stack.borrow_mut();
198 let cache = stack.last_mut().unwrap().as_mut().unwrap();
199 if cache.surfaces.len() < PCURVE_SAMPLE_CACHE_LIMIT { cache.surfaces.insert(key, point); }
200 }); }
201 Ok(point)
202}
203fn invert_checked(surface: &NurbsSurface, point: Vec3) -> Result<([f64; 2], f64), String> {
204 let key = [point.x.to_bits(), point.y.to_bits(), point.z.to_bits()];
205 let (active, hit) = PCURVE_SAMPLES.with(|stack| {
206 let mut stack = stack.borrow_mut();
207 let Some(Some(cache)) = stack.last_mut() else { return (false, None) };
208 if cache.surface != surface as *const _ as usize { return (false, None) }
209 cache.requests[2] += 1;
210 let hit = cache.inversions.get(&key).copied();
211 cache.hits[2] += usize::from(hit.is_some());
212 (true, hit)
213 });
214 if let Some(hit) = hit { return Ok(hit) }
215 let projection = invert_checked_uncached(surface, point)?;
216 if active { PCURVE_SAMPLES.with(|stack| {
217 let mut stack = stack.borrow_mut();
218 let cache = stack.last_mut().unwrap().as_mut().unwrap();
219 if cache.inversions.len() < PCURVE_SAMPLE_CACHE_LIMIT { cache.inversions.insert(key, projection); }
220 }); }
221 Ok(projection)
222}
223
224fn invert_checked_uncached(surface: &NurbsSurface, point: Vec3) -> Result<([f64; 2], f64), String> {
225 if surface.is_affine()? {
226 let ([u0, _], [v0, _]) = surface_domains(surface)?;
227 let (origin, du, dv) = surface.deriv1(u0, v0)?;
228 let delta = point.sub(origin);
229 let uu = du.dot(du);
230 let uv = du.dot(dv);
231 let vv = dv.dot(dv);
232 let along_u = delta.dot(du);
233 let along_v = delta.dot(dv);
234 let determinant = uu * vv - uv * uv;
235 if determinant.abs() > EPSILON {
236 let parameter = [
237 u0 + (along_u * vv - along_v * uv) / determinant,
238 v0 + (along_v * uu - along_u * uv) / determinant,
239 ];
240 // The standoff is a MEASUREMENT here as on every other carrier:
241 // until 2026-09-26 this returned 0.0 for "affine, not measured",
242 // and a curve 1e-2 off its plane read as lying on it.
243 let foot = origin.add(du.scale(parameter[0] - u0)).add(dv.scale(parameter[1] - v0));
244 return Ok((parameter, point.sub(foot).length()));
245 }
246 }
247 let projection = project_point_to_surface(surface, point)?;
248 Ok(([projection.u, projection.v], projection.distance))
249}
250
251/// Whether an end station at a cone's apex takes its nearest regular
252/// station's u (`true`), as in `build_interpolant`, or the projector's
253/// arbitrary u there as before 2026-09-27. `BREP_PCURVE_CONE_APEX_U=0`
254/// restores the projector's u, for a same-binary A/B.
255fn cone_apex_u() -> bool {
256 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
257 *ON.get_or_init(|| std::env::var_os("BREP_PCURVE_CONE_APEX_U").is_none_or(|value| value != "0"))
258}
259
260/// Whether a round with more candidates than the cap has room for keeps its
261/// WORST probes (`true`), or the first in span order as before 2026-09-27.
262/// `BREP_PCURVE_WORST_FIRST=0` restores span order, for a same-binary A/B.
263fn worst_first() -> bool {
264 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
265 *ON.get_or_init(|| std::env::var_os("BREP_PCURVE_WORST_FIRST").is_none_or(|value| value != "0"))
266}
267
268/// `BREP_PCURVE_C0_JOIN=0` builds one C2 interpolant through the stations as
269/// before 2026-10-03 (the C0 joins at the curve's corners off), for a
270/// same-binary A/B. Read once per process.
271fn c0_joins() -> bool {
272 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
273 *ON.get_or_init(|| std::env::var_os("BREP_PCURVE_C0_JOIN").is_none_or(|value| value != "0"))
274}
275
276fn seeded_inserts() -> bool {
277 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
278 *ON.get_or_init(|| std::env::var_os("BREP_PCURVE_SEEDED_INSERTS").is_none_or(|value| value != "0"))
279}
280
281/// [`invert_checked`] for a station inserted between two whose feet are known:
282/// Newton from the pcurve's own `(u, v)` at that fraction, which already lies
283/// between the neighbours' feet on their branch, and the global search only
284/// when that local foot stands off further than the neighbours do (plus the
285/// refinement floor) — a local minimum the curve does not sit at. The global
286/// search was 30 s of a 54 s `abc_00000011` import (732k calls, 41 µs each,
287/// measured 2026-09-26). Planes and analytic carriers keep their closed forms.
288/// `BREP_PCURVE_SEEDED_INSERTS=0` restores the global search at every insert,
289/// for a same-binary A/B.
290fn invert_near(
291 surface: &NurbsSurface,
292 point: Vec3,
293 seed: [f64; 2],
294 neighbour_gap: f64,
295) -> Result<([f64; 2], f64), String> {
296 if !seeded_inserts() || surface.is_affine()? || surface.analytic().is_some() {
297 return invert_checked(surface, point);
298 }
299 let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
300 if seeded.distance <= neighbour_gap + PCURVE_REFINEMENT_TOLERANCE {
301 return Ok(([seeded.u, seeded.v], seeded.distance));
302 }
303 invert_checked(surface, point)
304}
305
306fn unwrap_periodic(values: &mut [f64], minimum: f64, maximum: f64) {
307 let period = maximum - minimum;
308 for index in 1..values.len() {
309 while values[index] - values[index - 1] > period / 2.0 {
310 values[index] -= period;
311 }
312 while values[index] - values[index - 1] < -period / 2.0 {
313 values[index] += period;
314 }
315 }
316 if values.len() > 1 {
317 while values[0] - values[1] > period / 2.0 {
318 values[0] -= period;
319 }
320 while values[0] - values[1] < -period / 2.0 {
321 values[0] += period;
322 }
323 }
324 // Recenter the whole (now continuous) sequence into the domain by the
325 // whole-period shift that leaves the LEAST parameter outside [minimum,
326 // maximum]. A single middle/mean sample is NOT representative of a curve
327 // that bulges out to touch — or straddle — the periodic seam: when that
328 // sample is a boundary-kissing point (the at-seam projection unwraps a hair
329 // past the boundary), a single-sample test misfires and shoves the entire
330 // curve a full period out of range (ABC helmet 00000011/12 side channels:
331 // a touching edge's parameter run snapped a whole period off its true
332 // interior, tearing the loop open in parameter space).
333 if values.len() > 1 {
334 let excursion = |shift: f64| -> f64 {
335 values
336 .iter()
337 .map(|value| {
338 let v = value + shift;
339 (minimum - v).max(0.0) + (v - maximum).max(0.0)
340 })
341 .sum::<f64>()
342 };
343 let mean = values.iter().copied().sum::<f64>() / values.len() as f64;
344 let center = 0.5 * (minimum + maximum);
345 let base_k = ((center - mean) / period).round() as i64;
346 let mut best_shift = 0.0;
347 let mut best_excursion = f64::INFINITY;
348 for k in (base_k - 1)..=(base_k + 1) {
349 let shift = k as f64 * period;
350 let value = excursion(shift);
351 if value < best_excursion {
352 best_excursion = value;
353 best_shift = shift;
354 }
355 }
356 if best_shift != 0.0 {
357 for value in values.iter_mut() {
358 *value += best_shift;
359 }
360 }
361 }
362}
363
364/// The interpolant through `raw_parameters`, and what building it CLAMPED:
365/// the largest parameter distance a sample was pulled back into an open
366/// direction's domain, and the 3D distance between the extended surface at
367/// the raw parameter and the clamped foot. Both zero when nothing was pulled.
368/// A closed direction wraps and is never clamped.
369fn build_interpolant(
370 surface: &NurbsSurface,
371 raw_parameters: &[[f64; 2]],
372 curve_parameters: &[f64],
373) -> Result<(NurbsCurve, f64, f64), String> {
374 build_interpolant_joined(surface, raw_parameters, curve_parameters, &[])
375}
376
377/// [`build_interpolant`] with C0 JOINS: `joins` are values of
378/// `curve_parameters` at which the edge curve itself has a corner (a knot of
379/// multiplicity ≥ its degree, `range_breaks`). One cubic through all the
380/// stations is C2 everywhere, so across a corner it overshoots on both sides
381/// by a first-order amount however many stations are inserted: `abc_00000011`
382/// face #585 edge 179 (79 such knots on a 241-point curve) read 2.055e-4 off
383/// at 153 stations with the file's curve 2.1e-7 off the carrier (measured
384/// 2026-10-03). The stations have been planted at those knots since
385/// 2026-09-26; what was missing was the interpolant honouring them. Each
386/// piece between joins is interpolated on its own and the pieces are
387/// concatenated at knots of multiplicity 3 — the join `blend/track_fit.rs`
388/// makes for a blend track. A piece shorter than four stations cannot carry
389/// a cubic, so its join is dropped and it merges with its neighbour.
390fn build_interpolant_joined(
391 surface: &NurbsSurface,
392 raw_parameters: &[[f64; 2]],
393 curve_parameters: &[f64],
394 joins: &[f64],
395) -> Result<(NurbsCurve, f64, f64), String> {
396 let _clock = PcurvePhaseClock::new("interpolant", raw_parameters.len(), false);
397 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
398 let (closed_u, closed_v) = surface_closedness(surface)?;
399 let mut parameters = raw_parameters.to_vec();
400 // A cone's apex has no longitude either: every u there is the same point,
401 // and the projector answers one arbitrarily (0, which unwraps to 1). A trim
402 // that ENDS at the apex then turns sideways in its last span —
403 // `abc 00008080` face #163 edge 476, a generatrix at u = 0.80255 whose
404 // apex station read u = 1.0 (2026-09-27). An apex station at either end of
405 // the run takes the u of its nearest regular station, so a generatrix's
406 // pcurve is the straight u = const it is. Interior apex crossings are left
407 // to the branch logic below. `BREP_PCURVE_CONE_APEX_U=0` switches this off,
408 // for a same-binary A/B.
409 if cone_apex_u() && matches!(surface.analytic(), Some(AnalyticSurface::RuledRevolution { .. })) && parameters.len() >= 2 {
410 let speeds = parameters
411 .iter()
412 .map(|parameter| section_u_speed(surface, parameter[0], parameter[1]))
413 .collect::<Result<Vec<_>, _>>()?;
414 let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
415 let singular = |index: usize| speeds[index] <= reference * 1e-2;
416 if let (Some(first), Some(last)) = (
417 (0..parameters.len()).find(|&index| !singular(index)),
418 (0..parameters.len()).rfind(|&index| !singular(index)),
419 ) {
420 for index in 0..first {
421 parameters[index][0] = parameters[first][0];
422 }
423 for index in last + 1..parameters.len() {
424 parameters[index][0] = parameters[last][0];
425 }
426 }
427 }
428 // Longitude is undefined at a sphere pole: the surface's u tangent
429 // vanishes there, and closest-point inversion is free to return any u.
430 // Letting that arbitrary value participate in periodic unwrapping can move
431 // the first real meridian by a whole period (e.g. 0.75 -> -0.25). Analytic
432 // carriers are subsequently clamped, collapsing that meridian onto u=0.
433 //
434 // Keep this deliberately sphere- and singularity-gated. Split the u values
435 // into maximal non-pole runs, unwrap each run independently, and leave the
436 // pole samples untouched. The existing interpolant degree stays unchanged;
437 // lowering it here would also change endpoint-tangent decisions made later
438 // while stitching the face loop.
439 let sphere_u_singular = if matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) {
440 let speeds = parameters
441 .iter()
442 .map(|parameter| {
443 section_u_speed(surface, parameter[0], parameter[1])
444 })
445 .collect::<Result<Vec<_>, _>>()?;
446 let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
447 speeds
448 .into_iter()
449 // Uniform edge stations need not hit the pole exactly (ABC 5360
450 // bottoms out at v=5.5e-4). In this small conditioning band,
451 // longitude is already numerically arbitrary and must not connect
452 // the two meridian runs across the pole.
453 .map(|speed| speed <= reference * 1e-2)
454 .collect::<Vec<_>>()
455 } else {
456 vec![false; parameters.len()]
457 };
458 // Do not perturb ordinary pole-touching trims. The special path is needed
459 // only when a pole band is bracketed by nonsingular samples and its raw
460 // longitude changes by at least half a period: a true through-pole branch
461 // reset whose two meridian runs must be unwrapped independently.
462 // Prefix/suffix pole samples on normal sphere caps retain the legacy
463 // interpolation byte-for-byte. Include the exactly-half-period case so
464 // floating-point noise cannot decide which entire run is lifted (ABC 5603).
465 let has_interior_pole_crossing = sphere_u_singular
466 .iter()
467 .position(|singular| !singular)
468 .zip(sphere_u_singular.iter().rposition(|singular| !singular))
469 .is_some_and(|(first, last)| sphere_u_singular[first..=last].iter().any(|value| *value));
470 let period = (u1 - u0).abs();
471 let interior_pole_branch_reset = closed_u
472 && has_interior_pole_crossing
473 && parameters.windows(2).zip(sphere_u_singular.windows(2)).any(
474 |(parameter_pair, singular_pair)| {
475 (singular_pair[0] || singular_pair[1])
476 && (parameter_pair[1][0] - parameter_pair[0][0]).abs()
477 >= 0.5 * period - 1e-12 * period.max(1.0)
478 },
479 );
480 if closed_u {
481 let mut values = parameters.iter().map(|value| value[0]).collect::<Vec<_>>();
482 if interior_pole_branch_reset {
483 let mut start = 0;
484 while start < values.len() {
485 while start < values.len() && sphere_u_singular[start] {
486 start += 1;
487 }
488 let mut end = start;
489 while end < values.len() && !sphere_u_singular[end] {
490 end += 1;
491 }
492 unwrap_periodic(&mut values[start..end], u0, u1);
493 start = end;
494 }
495 } else {
496 unwrap_periodic(&mut values, u0, u1);
497 }
498 for (parameter, value) in parameters.iter_mut().zip(values) {
499 parameter[0] = value;
500 }
501 }
502 if closed_v {
503 let mut values = parameters.iter().map(|value| value[1]).collect::<Vec<_>>();
504 unwrap_periodic(&mut values, v0, v1);
505 for (parameter, value) in parameters.iter_mut().zip(values) {
506 parameter[1] = value;
507 }
508 }
509 // A closed direction WRAPS: an edge that straddles the seam has no single
510 // in-domain parameter run, so keep the unwrapped (possibly slightly
511 // out-of-domain) values and let the periodic evaluator wrap them — clamping
512 // them onto the seam boundary would collapse the straddling geometry (and,
513 // via the interpolant, spike neighbouring stations). This applies to
514 // analytic carriers too: a refitted cylinder can have stepped rims that
515 // cross its seam. evaluate_extended wraps every closed direction.
516 // Open directions must stay in-domain; their extension is a ruling.
517 //
518 // The clamp is MEASURED, not silent: a sample pulled back into the domain
519 // is reported as the parameter distance it moved and the 3D distance
520 // between the extended surface at its raw parameter and the clamped foot
521 // ([`PcurveFitReport::clamped_excursion`]). Until 2026-09-30 a trim that
522 // ran past its chart came back folded onto the chart's boundary with
523 // nothing in the report to say so; on the rib-base document that was
524 // 3.03e-2 of volume on a body `validate()` passes.
525 let mut excursion = 0.0_f64;
526 let mut distance = 0.0_f64;
527 let mut points = Vec::with_capacity(parameters.len());
528 for parameter in parameters {
529 let u = if closed_u {
530 parameter[0]
531 } else {
532 parameter[0].clamp(u0, u1)
533 };
534 let v = if closed_v {
535 parameter[1]
536 } else {
537 parameter[1].clamp(v0, v1)
538 };
539 let pulled = ((parameter[0] - u).powi(2) + (parameter[1] - v).powi(2)).sqrt();
540 if pulled > 0.0 {
541 excursion = excursion.max(pulled);
542 let extended = surface.evaluate_extended(parameter[0], parameter[1])?;
543 distance = distance.max(extended.sub(surface.evaluate(u, v)?).length());
544 }
545 points.push(Vec3::new(u, v, 0.0));
546 }
547 let degree = 3usize.min(points.len() - 1);
548 let mut bounds: Vec<usize> = joins
549 .iter()
550 .filter_map(|join| curve_parameters.iter().position(|parameter| parameter == join))
551 .filter(|&index| index > 0 && index + 1 < curve_parameters.len())
552 .collect();
553 bounds.sort_unstable();
554 bounds.dedup();
555 if degree < 3 || bounds.is_empty() || !c0_joins() {
556 let curve = interpolate_curve(&points, degree, curve_parameters)?;
557 return Ok((curve, excursion, distance));
558 }
559 // Every piece needs four stations for a cubic: drop the join that
560 // closes a shorter piece, from the left, until all pieces carry one.
561 let mut pieces = vec![0usize];
562 for bound in bounds {
563 if bound + 1 - pieces[pieces.len() - 1] >= 4 {
564 pieces.push(bound);
565 }
566 }
567 if curve_parameters.len() - pieces[pieces.len() - 1] < 4 {
568 pieces.pop();
569 }
570 pieces.push(curve_parameters.len() - 1);
571 if pieces.len() < 3 {
572 let curve = interpolate_curve(&points, degree, curve_parameters)?;
573 return Ok((curve, excursion, distance));
574 }
575 let mut knots: Vec<f64> = Vec::with_capacity(curve_parameters.len() + 4);
576 let mut control: Vec<Vec4> = Vec::with_capacity(curve_parameters.len());
577 let last_piece = pieces.len() - 2;
578 for piece in 0..=last_piece {
579 let (from, to) = (pieces[piece], pieces[piece + 1]);
580 let fitted = interpolate_curve(&points[from..=to], 3, &curve_parameters[from..=to])?;
581 if piece == 0 {
582 knots.extend(fitted.knots[..4].iter().copied());
583 }
584 knots.extend(fitted.knots[4..fitted.knots.len() - 4].iter().copied());
585 let multiplicity = if piece == last_piece { 4 } else { 3 };
586 knots.extend(std::iter::repeat(curve_parameters[to]).take(multiplicity));
587 control.extend(fitted.control_points.iter().skip(usize::from(piece > 0)).copied());
588 }
589 let curve = NurbsCurve::new(3, knots, control)?;
590 Ok((curve, excursion, distance))
591}
592
593/// The 3D residual a pcurve's image must reach before `build_pcurve_on_surface`
594/// accepts it, and the sample ceiling it refines up to.
595///
596/// A trimmed face's boundary is only ever as accurate as these two numbers, and
597/// every mass property is an integral over that boundary — so this is the floor
598/// under area, volume and centroid for every curved trim in the kernel.
599///
600/// These were `min(1e-3, 1e-4 * scale)` and 160, where `scale` is
601/// `1 + ||curve midpoint||` — the DISTANCE FROM THE WORLD ORIGIN of the curve's
602/// midpoint, not the part's size. That expression pinned to an absolute 1e-3
603/// once the midpoint sat more than about nine units from the origin, so on an
604/// ordinary part the bar was 1e-3 and tracked nothing.
605///
606/// Measured on `offset-shell-box-bore-dished-face`, whose dished wall is a
607/// closed-form identity at 2468.431782123 (three independent routes agree). The
608/// floor is one of TWO errors there; the other is the refitted sphere carrier
609/// that `sphere_offset_surface` now builds in closed form, and before both moved
610/// they partly CANCELLED — which is why either alone reads as a regression:
611///
612/// ```text
613/// floor Box_O.S2 abs err relative gate total worst case
614/// 1e-3 2468.442270355 +1.049e-02 4.25e-06 (old)
615/// 1e-6 2468.431785429 +3.306e-06 1.34e-09 181.4 s 31.2 s
616/// 1e-7 2468.431782170 +4.746e-08 1.92e-11 218.5 s 32.1 s
617/// 1e-9 2468.431782103 -2.037e-08 -8.25e-12 305.5 s 46.8 s
618/// ```
619///
620/// 1e-7 is chosen: 70x the accuracy of 1e-6 for +20% gate time, where 1e-9 costs
621/// a further +40% for a 2.3x residual no oracle in this corpus could resolve.
622/// All four cells are one session on one binary, so the times are comparable to
623/// each other. Every case carrying a closed-form oracle moves TOWARD it and none
624/// away (`PushFaceTest` 339x, `PushFaceTest2` 275x); no case changes class at
625/// any floor.
626///
627/// A flat ABSOLUTE is what was measured, not what was designed. This codebase
628/// otherwise keys identity bands to part size (`KernelTolerances::heal_band`,
629/// the `1e-11*(1+scale)` blend family), and on a 3000 mm part 1e-7 is 3e-11
630/// relative — refinement will exhaust `MAX_PCURVE_SAMPLES` and return whatever
631/// it reached. Until 2026-09-12 it did so SILENTLY; now the fit's
632/// [`PcurveFitReport`] carries the residual actually achieved and a
633/// [`PcurveFitExit::SampleCeiling`] exit, and the owning operation's
634/// diagnostics count it (`pcurve.unmet_floor`). That is far better than the
635/// 1e-3 it replaces but it is not a size-aware bar; deriving one from the
636/// surface's own extent is separate, unmeasured work. Being absolute does remove this gate's dependence on
637/// distance from the origin as a side effect — a real translation-VARIANCE
638/// defect, the same anti-pattern `tolerance::merge_scale` records being removed
639/// elsewhere. `drop_tolerance` and `endpoint_tolerance` below still carry it.
640pub const PCURVE_REFINEMENT_TOLERANCE: f64 = 1e-7;
641
642/// Sample ceiling for a fit's FIRST refinement pass. 160 could not reach
643/// [`PCURVE_REFINEMENT_TOLERANCE`] on a curved trim of any length; 600 is the
644/// value every cell of the table above was measured at.
645///
646/// It BINDS. On `boolean_fuzz_corpus/25_cube_pierce_solid` the fitter stopped
647/// here 1.65e-3 of volume short of the converged answer (2000 and 4000 samples
648/// agree exactly). Raising it globally to 2000 was costed and rejected: +23%
649/// case-gate time, concentrated in three offset/fillet cases, and one case
650/// (`inbox-20260910-offset-shell-collapsed-fillet`) driven 26x outside its
651/// volume band. So it is not raised here: a fit that stops at this ceiling
652/// with a residual still worse than its edge curve's own distance from the
653/// surface re-enters refinement with [`PCURVE_RAISED_SAMPLES`]
654/// ([`refine_pcurve`]); one whose residual is already that gap stops here and
655/// says so ([`PcurveFitReport::off_surface`]), because no sample count can
656/// close it.
657const MAX_PCURVE_SAMPLES: usize = 600;
658
659/// Sample ceiling for the ONE raised pass a fit may take past
660/// [`MAX_PCURVE_SAMPLES`]. 2000 is the ceiling the convergence sweep measured
661/// on `25_cube_pierce_solid`: its 2000 and 4000 cells agree to every printed
662/// digit because refinement there stops itself near 1540 samples, so the cap
663/// is not what binds.
664const PCURVE_RAISED_SAMPLES: usize = 2000;
665
666/// What a pcurve fit ACHIEVED, returned beside the curve so a ceiling exit can
667/// never be mistaken for a met floor.
668///
669/// [`fit_pcurve_on_surface`] asks for [`PCURVE_REFINEMENT_TOLERANCE`] and stops
670/// at [`MAX_PCURVE_SAMPLES`] — or, where that ceiling bound and the floor is
671/// reachable, at [`PCURVE_RAISED_SAMPLES`]. Until 2026-09-12 a trim that ran
672/// out of samples came back exactly like one that met the floor; on
673/// `boolean_fuzz_corpus/25_cube_pierce_solid` that silence hid a 1.65e-3 volume
674/// error. Now every fit says which it was: `residual` is measured on the curve
675/// that is actually returned and `exit` names why refinement stopped.
676///
677/// This is a MEASUREMENT taken at a construction, not a band.
678#[derive(Clone, Copy, Debug, PartialEq)]
679pub struct PcurveFitReport {
680 /// Largest 3D distance between the returned pcurve's image on the surface
681 /// and the edge curve. On a [`PcurveFitExit::Converged`] exit whose final
682 /// round probed every span this is that round's maximum — the refinement's
683 /// own acceptance test. On every other exit, including a converged round
684 /// that skipped spans narrower than 1e-3 of the parameter range, it is a
685 /// sweep over EVERY span of the returned curve.
686 pub residual: f64,
687 /// The residual the refinement was asked to reach.
688 pub floor: f64,
689 /// Interpolation samples in the returned curve.
690 pub samples: usize,
691 /// Why refinement stopped.
692 pub exit: PcurveFitExit,
693 /// Largest distance from the edge curve to the surface at every station
694 /// the fit inverted — the part of `residual` no pcurve can remove, since a
695 /// pcurve's image lies ON the surface. Zero for an affine carrier. A value
696 /// above `floor` says the floor is unreachable for this input; a ceiling
697 /// exit is retried only while `residual` exceeds it by more than the floor,
698 /// i.e. while the returned curve is worse than its input.
699 pub off_surface: f64,
700 /// Whether the fit re-entered refinement with [`PCURVE_RAISED_SAMPLES`]
701 /// after [`MAX_PCURVE_SAMPLES`] bound with the floor unmet.
702 pub raised: bool,
703 /// Largest parameter distance a station's inversion was pulled back into
704 /// an OPEN direction's domain when the returned curve was interpolated: a
705 /// trim asked for outside its chart is folded onto the chart's boundary,
706 /// the edge stays right and the face becomes the wrong region on a body
707 /// `validate()` passes (3.03e-2 of volume on the rib-base document). Zero
708 /// when nothing was pulled. A REPORT, not a bar: nothing here refuses on
709 /// it. Its coverage is the clamp in `build_interpolant`, which is the only
710 /// place a raw out-of-domain parameter reaches today — an affine carrier's
711 /// inversion is unclamped, so an affine plane fitted through the station
712 /// lane reads here. It does NOT see a clamp the projector applied before
713 /// the fit: the analytic `Plane` closed form (a non-affine net) clamps to
714 /// its domain, `RuledRevolution` clamps its axial parameter to `[0, 1]`
715 /// and the general Newton clamps through `fit_parameter`; those stations
716 /// arrive in-domain and read as `off_surface` instead. The exact affine
717 /// lanes (`fit_pcurve_on_surface` on an affine carrier,
718 /// `affine_pcurve_on_range`) never clamp and read zero by construction.
719 pub clamped_excursion: f64,
720 /// The 3D distance the clamp corresponds to: between the extended surface
721 /// at the raw parameter and the clamped foot, at the station of the
722 /// largest such distance. Zero when nothing was pulled.
723 pub clamped_distance: f64,
724}
725
726impl PcurveFitReport {
727 /// Whether the returned curve reaches the floor it was asked for.
728 pub fn met_floor(&self) -> bool {
729 self.residual <= self.floor
730 }
731
732 /// Whether the returned curve is on the ACCEPTANCE BAR: the floor it was
733 /// asked for, plus the stations' own standoff from the carrier.
734 ///
735 /// A pcurve's image lies on the carrier, so at a station standing `g` off
736 /// it no image can come closer than `g`; a fit whose residual is within
737 /// the floor of `off_surface` is as good as the input allows, and one over
738 /// it is worse than its input — the same condition [`refine_pcurve`] uses
739 /// to decide that a raise can still buy accuracy. This is the bar every
740 /// station builder refuses over ([`build_pcurve_on_surface_stations`]).
741 /// It is blind to sideways error quadratically (a station tracked `d`
742 /// aside of the projection of a point `g` off the surface reads only
743 /// `d²/(2g)` further away), which is stated rather than hidden: on the
744 /// import corpus the stations that stand off their carriers are the
745 /// FILE's curves, and the bar there is the file's own number.
746 pub fn on_bar(&self) -> bool {
747 self.residual <= self.floor + self.off_surface
748 }
749}
750
751/// Why a pcurve refinement stopped. Only [`PcurveFitExit::Converged`] means the
752/// floor was met; every other variant is a BUDGET exit, and the report's
753/// residual was measured on the returned curve rather than assumed.
754#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
755pub enum PcurveFitExit {
756 /// A probe round found every station within the floor. An affine carrier
757 /// maps exactly and reports this with a zero residual.
758 Converged,
759 /// The sample ceiling bound before the floor was met: [`MAX_PCURVE_SAMPLES`]
760 /// when the raise was declined (the residual is already explained by
761 /// [`PcurveFitReport::off_surface`]), [`PCURVE_RAISED_SAMPLES`] when it was
762 /// taken and bound too.
763 SampleCeiling,
764 /// Every refinement round was spent and the last one still inserted
765 /// samples, so the curve it built was never re-checked by the loop.
766 RoundBudget,
767 /// A round found stations over the floor but could improve none of them:
768 /// the edge point could not be inverted inside the drop band, or (in the
769 /// raised pass) the pcurve already tracks the surface's nearest point to
770 /// within the floor. Either way the edge curve itself sits off the surface
771 /// there and no pcurve can close that gap; the residual reports how far.
772 /// The range lane's end-span challenger (its station fitter's TRACKING
773 /// mode) also exits here when a round's probes all track their feet
774 /// within the floor while the deviation residual, which includes the
775 /// edge's standoff, stays over it.
776 Stalled,
777 /// The last probe round found every span it PROBED within the floor, but
778 /// skipped spans narrower than the loop's own resolution floor — 1e-3 of
779 /// the parameter range in [`refine_pcurve`], [`SPAN_FLOOR`] (1e-6) in
780 /// [`fit_pcurve_on_surface_stations`] — and a sweep over those found the
781 /// floor unmet.
782 /// Until 2026-09-12 this exit was reported as `Converged` with the probed
783 /// spans' maximum: on `25_cube_pierce_solid` a 1539-sample fit with 1537
784 /// narrow spans read 7e-8 that way while every span read 1.9e-4.
785 SpanFloor,
786}
787
788/// A fitted pcurve with what the fit achieved.
789#[derive(Clone, Debug)]
790pub struct PcurveFit {
791 pub curve: NurbsCurve,
792 pub report: PcurveFitReport,
793}
794
795/// Per-operation tally of pcurve fits and how many missed their floor.
796///
797/// [`build_pcurve_on_surface`] and its marched twin are called from some forty
798/// sites, most of which have no diagnostics record to write into. Rather than
799/// thread one through every layer, an operation that returns
800/// [`KernelDiagnostics`] opens a [`PcurveFitScope`]; every fit made on that
801/// thread while the scope is open is tallied here, and closing the scope hands
802/// the tally back for [`PcurveFitLedger::report_into`]. This is the shape the
803/// boolean's `CONFORMANCE_REPAIRS` counter already uses. With no scope open,
804/// recording is a no-op.
805///
806/// Scopes nest: CLOSING a child folds it into its parent, so an offset shell
807/// sees the fits of the booleans it ran. A scope that is DROPPED without being
808/// closed is discarded, not folded — a boolean attempt that refused (and may be
809/// retried under perturbation) must not leave its fits on the caller's tally.
810///
811/// Thread-local by construction: a fit made on a rayon worker (the `parallel`
812/// feature's STEP body import) is not seen by a scope on the calling thread. No
813/// diagnostics-returning operation builds pcurves off-thread today.
814#[derive(Clone, Copy, Debug, Default, PartialEq)]
815pub struct PcurveFitLedger {
816 /// Fits recorded, exact affine ones included.
817 pub fits: u64,
818 /// Fits whose returned curve did not reach its floor.
819 pub unmet: u64,
820 /// Unmet fits that stopped at a sample ceiling.
821 pub sample_ceiling: u64,
822 /// Unmet fits that spent every refinement round.
823 pub round_budget: u64,
824 /// Unmet fits that could insert nothing.
825 pub stalled: u64,
826 /// Unmet fits whose remaining over-floor spans were all below the loop's
827 /// 1e-3 span floor.
828 pub span_floor: u64,
829 /// Largest residual among the unmet fits; zero when there are none.
830 pub worst_unmet_residual: f64,
831 /// Fits that re-entered refinement with [`PCURVE_RAISED_SAMPLES`], met or
832 /// not.
833 pub raised: u64,
834 /// Unmet ceiling exits that were NOT raised because the residual is
835 /// already within the floor of the edge curve's own gap from the surface.
836 pub raise_declined: u64,
837 /// Largest [`PcurveFitReport::off_surface`] over every fit.
838 pub worst_off_surface: f64,
839 /// Fits whose interpolant was clamped into an open direction's domain
840 /// ([`PcurveFitReport::clamped_excursion`] above zero), met or not.
841 pub clamped: u64,
842 /// Largest [`PcurveFitReport::clamped_excursion`] over every fit.
843 pub worst_clamped_excursion: f64,
844 /// Largest [`PcurveFitReport::clamped_distance`] over every fit.
845 pub worst_clamped_distance: f64,
846}
847
848impl PcurveFitLedger {
849 fn record(&mut self, report: &PcurveFitReport) {
850 self.fits += 1;
851 if report.raised {
852 self.raised += 1;
853 }
854 self.worst_off_surface = self.worst_off_surface.max(report.off_surface);
855 if report.clamped_excursion > 0.0 {
856 self.clamped += 1;
857 self.worst_clamped_excursion = self.worst_clamped_excursion.max(report.clamped_excursion);
858 self.worst_clamped_distance = self.worst_clamped_distance.max(report.clamped_distance);
859 }
860 if report.met_floor() {
861 return;
862 }
863 self.unmet += 1;
864 match report.exit {
865 PcurveFitExit::Converged => {}
866 PcurveFitExit::SampleCeiling => {
867 self.sample_ceiling += 1;
868 if !report.raised {
869 self.raise_declined += 1;
870 }
871 }
872 PcurveFitExit::RoundBudget => self.round_budget += 1,
873 PcurveFitExit::Stalled => self.stalled += 1,
874 PcurveFitExit::SpanFloor => self.span_floor += 1,
875 }
876 self.worst_unmet_residual = self.worst_unmet_residual.max(report.residual);
877 }
878
879 fn fold(&mut self, child: &PcurveFitLedger) {
880 self.fits += child.fits;
881 self.unmet += child.unmet;
882 self.sample_ceiling += child.sample_ceiling;
883 self.round_budget += child.round_budget;
884 self.stalled += child.stalled;
885 self.span_floor += child.span_floor;
886 self.worst_unmet_residual = self.worst_unmet_residual.max(child.worst_unmet_residual);
887 self.raised += child.raised;
888 self.raise_declined += child.raise_declined;
889 self.worst_off_surface = self.worst_off_surface.max(child.worst_off_surface);
890 self.clamped += child.clamped;
891 self.worst_clamped_excursion = self.worst_clamped_excursion.max(child.worst_clamped_excursion);
892 self.worst_clamped_distance = self.worst_clamped_distance.max(child.worst_clamped_distance);
893 }
894
895 /// Write the tally into an operation's diagnostics: the `pcurve.*`
896 /// counters and measurements always, plus one `pcurve.unmet_floor` event at
897 /// [`DiagnosticSeverity::Degraded`] when any fit missed its floor. Degraded,
898 /// not Error: the result is still the kernel's best construction and stays
899 /// shippable, but it no longer claims the accuracy its floor states.
900 pub fn report_into(&self, diagnostics: &mut KernelDiagnostics) {
901 diagnostics.count_n("pcurve.fits", self.fits);
902 diagnostics.count_n("pcurve.unmet_floor", self.unmet);
903 diagnostics.count_n("pcurve.exit.sample_ceiling", self.sample_ceiling);
904 diagnostics.count_n("pcurve.exit.round_budget", self.round_budget);
905 diagnostics.count_n("pcurve.exit.stalled", self.stalled);
906 diagnostics.count_n("pcurve.exit.span_floor", self.span_floor);
907 diagnostics.count_n("pcurve.budget_raised", self.raised);
908 diagnostics.count_n("pcurve.budget_raise_declined", self.raise_declined);
909 diagnostics.measure_max("pcurve.worst_unmet_residual", self.worst_unmet_residual);
910 diagnostics.measure_max("pcurve.worst_off_surface", self.worst_off_surface);
911 diagnostics.count_n("pcurve.clamped", self.clamped);
912 diagnostics.measure_max("pcurve.worst_clamped_excursion", self.worst_clamped_excursion);
913 diagnostics.measure_max("pcurve.worst_clamped_distance", self.worst_clamped_distance);
914 if self.unmet > 0 {
915 diagnostics.event(
916 DiagnosticSeverity::Degraded,
917 KernelStage::Refine,
918 "pcurve.unmet_floor",
919 format!(
920 "{} of {} pcurve fits did not reach the {:.0e} floor \
921 (sample ceiling {}, round budget {}, stalled {}, span floor {}); \
922 worst residual {:.3e}; budget raised on {}, declined on {}; \
923 edge curves sit off their surfaces by up to {:.3e}{}",
924 self.unmet,
925 self.fits,
926 PCURVE_REFINEMENT_TOLERANCE,
927 self.sample_ceiling,
928 self.round_budget,
929 self.stalled,
930 self.span_floor,
931 self.worst_unmet_residual,
932 self.raised,
933 self.raise_declined,
934 self.worst_off_surface,
935 if self.clamped > 0 {
936 format!(
937 "; {} fit(s) clamped into their chart by up to {:.3e} in parameter, {:.3e} in 3D",
938 self.clamped, self.worst_clamped_excursion, self.worst_clamped_distance
939 )
940 } else {
941 String::new()
942 },
943 ),
944 );
945 }
946 }
947}
948
949thread_local! {
950 /// The open [`FootMemo`] on this thread, if any ([`FootMemoScope`]).
951 static FOOT_MEMO: std::cell::RefCell<Option<FootMemo>> = const { std::cell::RefCell::new(None) };
952}
953
954/// Exact-input memo of the common reader's projections on ONE surface: the
955/// global foot of an edge point and the foot seeded at a (u, v), keyed by the
956/// bit patterns of their inputs. Both projectors are pure functions of
957/// (surface, point[, seed]), so a hit returns the value a fresh call would:
958/// the readings, decisions and fits are bit-identical with or without it.
959/// It exists for cost: within one coedge the same edge points are read by
960/// the trigger read, every tracking-mode verification round, the
961/// challenger's final read and the importer's keep read, each of which
962/// projected them again (`importTestWorking`, 2026-10-05: about 10k probes
963/// read 3-5 times on each of nine off-carrier refits, 3-5 s apiece).
964struct FootMemo {
965 surface: *const NurbsSurface,
966 global: rustc_hash::FxHashMap<[u64; 3], Option<crate::SurfaceProjection>>,
967 seeded: rustc_hash::FxHashMap<[u64; 5], Option<crate::SurfaceProjection>>,
968}
969
970/// Opens a [`FootMemo`] for `surface` on this thread until dropped. Opening
971/// one where a memo for the SAME surface is already open reuses it (an inner
972/// scope, e.g. the range challenger inside an importer coedge); a memo for
973/// another surface is set aside and restored on drop. The borrow of `surface`
974/// keeps it alive and unmodified for the scope, so the pointer key cannot be
975/// reused by another surface while the memo answers for it.
976pub(crate) struct FootMemoScope<'a> {
977 previous: Option<Option<FootMemo>>,
978 _surface: std::marker::PhantomData<&'a NurbsSurface>,
979 /// Not `Send`: the scope must close on the thread that opened it.
980 _thread_bound: std::marker::PhantomData<*const ()>,
981}
982
983impl<'a> FootMemoScope<'a> {
984 pub(crate) fn open(surface: &'a NurbsSurface) -> Self {
985 let key = surface as *const NurbsSurface;
986 let previous = FOOT_MEMO.with(|memo| {
987 let mut memo = memo.borrow_mut();
988 if memo.as_ref().is_some_and(|open| std::ptr::eq(open.surface, key)) {
989 None
990 } else {
991 Some(memo.replace(FootMemo { surface: key, global: Default::default(), seeded: Default::default() }))
992 }
993 });
994 Self { previous, _surface: std::marker::PhantomData, _thread_bound: std::marker::PhantomData }
995 }
996}
997
998impl Drop for FootMemoScope<'_> {
999 fn drop(&mut self) {
1000 if let Some(previous) = self.previous.take() {
1001 FOOT_MEMO.with(|memo| *memo.borrow_mut() = previous);
1002 }
1003 }
1004}
1005
1006fn point_bits(point: Vec3) -> [u64; 3] {
1007 [point.x.to_bits(), point.y.to_bits(), point.z.to_bits()]
1008}
1009
1010/// [`project_point_to_surface`] as `.ok()`, through the open memo for this
1011/// surface when there is one. `#[track_caller]`: the projector still sees the
1012/// reader's own call site. Under a `BREP_PROJECTION_GLOBAL_SITES`/`_EXCEPT`
1013/// filter the projector's answer depends on that site, so the memo is not
1014/// consulted then.
1015#[track_caller]
1016fn memo_global_foot(surface: &NurbsSurface, point: Vec3) -> Option<crate::SurfaceProjection> {
1017 if crate::projection::global_site_filter_active() {
1018 return project_point_to_surface(surface, point).ok();
1019 }
1020 let key = point_bits(point);
1021 let hit = FOOT_MEMO.with(|memo| {
1022 memo.borrow().as_ref().filter(|open| std::ptr::eq(open.surface, surface)).map(|open| open.global.get(&key).copied())
1023 });
1024 match hit {
1025 Some(Some(foot)) => foot,
1026 Some(None) => {
1027 let foot = project_point_to_surface(surface, point).ok();
1028 FOOT_MEMO.with(|memo| {
1029 if let Some(open) = memo.borrow_mut().as_mut().filter(|open| std::ptr::eq(open.surface, surface)) {
1030 open.global.insert(key, foot);
1031 }
1032 });
1033 foot
1034 }
1035 None => project_point_to_surface(surface, point).ok(),
1036 }
1037}
1038
1039/// [`project_point_to_surface_seeded`] as `.ok()`, through the open memo for
1040/// this surface when there is one.
1041fn memo_seeded_foot(surface: &NurbsSurface, point: Vec3, seed_u: f64, seed_v: f64) -> Option<crate::SurfaceProjection> {
1042 let [x, y, z] = point_bits(point);
1043 let key = [x, y, z, seed_u.to_bits(), seed_v.to_bits()];
1044 let hit = FOOT_MEMO.with(|memo| {
1045 memo.borrow().as_ref().filter(|open| std::ptr::eq(open.surface, surface)).map(|open| open.seeded.get(&key).copied())
1046 });
1047 match hit {
1048 Some(Some(foot)) => foot,
1049 Some(None) => {
1050 let foot = project_point_to_surface_seeded(surface, point, seed_u, seed_v).ok();
1051 FOOT_MEMO.with(|memo| {
1052 if let Some(open) = memo.borrow_mut().as_mut().filter(|open| std::ptr::eq(open.surface, surface)) {
1053 open.seeded.insert(key, foot);
1054 }
1055 });
1056 foot
1057 }
1058 None => project_point_to_surface_seeded(surface, point, seed_u, seed_v).ok(),
1059 }
1060}
1061
1062thread_local! {
1063 /// The open [`PcurveFitScope`]s on this thread, innermost last.
1064 static PCURVE_FIT_SCOPES: std::cell::RefCell<Vec<PcurveFitLedger>> =
1065 const { std::cell::RefCell::new(Vec::new()) };
1066}
1067
1068/// `BREP_DEBUG_PCURVE_FIT` only: a process-wide id per [`refine_pcurve`]
1069/// call, so its first-pass and candidate `PCURVE-TRACK` lines pair up even
1070/// when fits run on several threads.
1071static PCURVE_TRACK_CALL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1072
1073/// An open tally of the pcurve fits made on this thread — see
1074/// [`PcurveFitLedger`]. Open it where an operation starts, [`close`] it where
1075/// the operation returns its diagnostics; a `?` that unwinds past it drops the
1076/// tally.
1077///
1078/// [`close`]: PcurveFitScope::close
1079pub struct PcurveFitScope {
1080 /// Not `Send`: the scope must close on the thread that opened it.
1081 _thread_bound: std::marker::PhantomData<*const ()>,
1082}
1083
1084impl PcurveFitScope {
1085 pub fn open() -> Self {
1086 PCURVE_FIT_SCOPES.with(|scopes| scopes.borrow_mut().push(PcurveFitLedger::default()));
1087 Self {
1088 _thread_bound: std::marker::PhantomData,
1089 }
1090 }
1091
1092 /// Take the tally, folding it into the enclosing scope if there is one.
1093 pub fn close(self) -> PcurveFitLedger {
1094 let ledger = PCURVE_FIT_SCOPES.with(|scopes| {
1095 let mut scopes = scopes.borrow_mut();
1096 let ledger = scopes.pop().unwrap_or_default();
1097 if let Some(parent) = scopes.last_mut() {
1098 parent.fold(&ledger);
1099 }
1100 ledger
1101 });
1102 std::mem::forget(self);
1103 ledger
1104 }
1105}
1106
1107impl Drop for PcurveFitScope {
1108 fn drop(&mut self) {
1109 PCURVE_FIT_SCOPES.with(|scopes| {
1110 scopes.borrow_mut().pop();
1111 });
1112 }
1113}
1114
1115pub(crate) fn record_fit(report: &PcurveFitReport) {
1116 // Per-fit trace for the convergence probes (`trim_floor_sweep_probe` and
1117 // its offset-shell sibling): one line per fit with the whole report, the
1118 // way `BREP_DEBUG_PCURVE` traces an endpoint failure.
1119 if std::env::var_os("BREP_DEBUG_PCURVE_FIT").is_some() {
1120 eprintln!("PCURVE-FIT {report:?}");
1121 }
1122 PCURVE_FIT_SCOPES.with(|scopes| {
1123 if let Some(open) = scopes.borrow_mut().last_mut() {
1124 open.record(report);
1125 }
1126 });
1127}
1128
1129/// Largest 3D distance between `pcurve`'s image and `curve` at the
1130/// quarter-points of every span of `parameters` — the stations refinement
1131/// probes, over EVERY span. Evaluations only; nothing is inverted. The sweep
1132/// measures, it does not decide.
1133fn probe_residual(
1134 surface: &NurbsSurface,
1135 curve: &NurbsCurve,
1136 pcurve: &NurbsCurve,
1137 parameters: &[f64],
1138 domain: [f64; 2],
1139 _u_domain: [f64; 2],
1140 _v_domain: [f64; 2],
1141) -> Result<f64, String> {
1142 probe_residual_at(surface, curve, pcurve, parameters, domain, &[0.25, 0.5, 0.75])
1143}
1144
1145/// Extra interior points per sample span, at `j/17` for `j = 1..=16`, that
1146/// [`tracking_acceptance_residual`] reads IN ADDITION to [`probe_residual`]'s
1147/// quarter, mid and three-quarter points (nineteen fractions per span in
1148/// all), so it can never read below the three-point maximum on the same
1149/// curve. The three quarter-points under-read the sixteen-point maximum by
1150/// 3.3–6.5 % on the constructed-cylinder union's imprint pcurves, and sixteen
1151/// sit within 0.4 % of sixty-four there (2026-10-04); the Converged path keeps
1152/// its own read.
1153const TRACKING_ACCEPTANCE_POINTS: usize = 16;
1154
1155/// The same-parameter image-vs-edge distance of `pcurve` at `locals` (span
1156/// fractions) inside every span of `parameters`: the largest, or an error if
1157/// any is not finite.
1158fn probe_residual_at(
1159 surface: &NurbsSurface,
1160 curve: &NurbsCurve,
1161 pcurve: &NurbsCurve,
1162 parameters: &[f64],
1163 [t0, t1]: [f64; 2],
1164 locals: &[f64],
1165) -> Result<f64, String> {
1166 let mut worst = 0.0_f64;
1167 for span in parameters.windows(2) {
1168 for &local in locals {
1169 let fraction = span[0] + (span[1] - span[0]) * local;
1170 let parameter = pcurve.evaluate(fraction)?;
1171 let on_surface =
1172 section_surface_point(surface, parameter.x, parameter.y, true)?;
1173 let on_curve = section_curve_point(curve, t0 + (t1 - t0) * fraction)?;
1174 // `f64::max` drops a NaN operand, so a non-finite distance would
1175 // leave `worst` reading the finite stations only. A residual that
1176 // can accept a curve must have measured every station: fail
1177 // closed instead.
1178 let distance = on_surface.sub(on_curve).length();
1179 if !distance.is_finite() {
1180 return Err(format!(
1181 "probe_residual: non-finite image distance {distance} at fraction {fraction}"
1182 ));
1183 }
1184 worst = worst.max(distance);
1185 }
1186 }
1187 Ok(worst)
1188}
1189
1190/// The probe fractions a native read takes for the given pcurves: both
1191/// domain ENDPOINTS, and the quarter points plus 16 interior points of every
1192/// span between each curve's OWN distinct knots (the installed interpolant's
1193/// spans, not the construction stations). Merged over all the curves, sorted
1194/// and deduplicated, so a standing fit and a candidate are read at the SAME
1195/// fractions.
1196pub(crate) fn native_probe_fractions(curves: &[&NurbsCurve]) -> Result<Vec<f64>, String> {
1197 let locals: Vec<f64> = [0.25, 0.5, 0.75]
1198 .into_iter()
1199 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
1200 .collect();
1201 let mut fractions = Vec::new();
1202 for curve in curves {
1203 let [q0, q1] = curve.domain()?;
1204 fractions.push(q0);
1205 fractions.push(q1);
1206 let mut knots: Vec<f64> = curve.knots.iter().copied().filter(|k| *k >= q0 && *k <= q1).collect();
1207 knots.dedup();
1208 for span in knots.windows(2).filter(|w| w[1] > w[0]) {
1209 for &local in &locals {
1210 fractions.push(span[0] + (span[1] - span[0]) * local);
1211 }
1212 }
1213 }
1214 fractions.sort_by(f64::total_cmp);
1215 fractions.dedup();
1216 Ok(fractions)
1217}
1218
1219/// One pcurve read at given probe fractions against the edge curve (whose
1220/// parameter is t0 + (t1 - t0) x fraction): the image's distance from the
1221/// edge point's FOOT on the carrier (its TRACKING, the part a pcurve can
1222/// improve; the foot is seeded at the pcurve's own uv, closed form on an
1223/// analytic carrier), the image's distance from the edge point itself (the
1224/// same-parameter DEVIATION), and the edge point's distance from the carrier
1225/// (its INHERITED STANDOFF). Maxima over the probes; fails closed on any
1226/// non-finite point or distance.
1227#[derive(Clone, Copy, Debug, PartialEq)]
1228struct NativeRead {
1229 tracking: f64,
1230 deviation: f64,
1231 standoff: f64,
1232}
1233
1234fn native_read(surface: &NurbsSurface, curve: &NurbsCurve, pcurve: &NurbsCurve, [t0, t1]: [f64; 2], fractions: &[f64]) -> Result<NativeRead, String> {
1235 let mut read = NativeRead { tracking: 0.0, deviation: 0.0, standoff: 0.0 };
1236 for &fraction in fractions {
1237 let parameter = pcurve.evaluate(fraction)?;
1238 let image = section_surface_point(surface, parameter.x, parameter.y, true)?;
1239 let on_curve = section_curve_point(curve, t0 + (t1 - t0) * fraction)?;
1240 let foot = crate::projection::project_point_to_surface_from_seed(surface, on_curve, [parameter.x, parameter.y])?;
1241 let tracking = image.sub(foot.point).length();
1242 let deviation = image.sub(on_curve).length();
1243 if !(tracking.is_finite() && deviation.is_finite() && foot.distance.is_finite()) {
1244 return Err(format!("native read: non-finite reading at fraction {fraction}"));
1245 }
1246 read.tracking = read.tracking.max(tracking);
1247 read.deviation = read.deviation.max(deviation);
1248 read.standoff = read.standoff.max(foot.distance);
1249 }
1250 Ok(read)
1251}
1252
1253/// Whether an off-carrier tracking candidate replaces the standing fit, both
1254/// read at the SAME probes ([`native_probe_fractions`] of both curves): its
1255/// tracking finite, within the unchanged floor and strictly lower, AND its
1256/// same-parameter deviation no worse (the reported total, inherited standoff
1257/// included, must not grow), AND its own refinement fields finite.
1258fn standoff_candidate_accepted(standing: NativeRead, candidate: NativeRead, candidate_fields_finite: bool, floor: f64) -> bool {
1259 candidate_fields_finite
1260 && candidate.tracking.is_finite()
1261 && candidate.deviation.is_finite()
1262 && candidate.tracking <= floor
1263 && candidate.tracking < standing.tracking
1264 && candidate.deviation <= standing.deviation
1265}
1266
1267/// [`probe_residual_at`] at [`probe_residual`]'s three quarter-points plus
1268/// [`TRACKING_ACCEPTANCE_POINTS`] evenly spaced interior points of every span
1269/// (nineteen per span): the maximum of the old three-point reading and the
1270/// sixteen-point one. It is the reading a budget-exit tracking candidate is
1271/// accepted on, and the residual it is then reported with.
1272fn tracking_acceptance_residual(
1273 surface: &NurbsSurface,
1274 curve: &NurbsCurve,
1275 pcurve: &NurbsCurve,
1276 parameters: &[f64],
1277 domain: [f64; 2],
1278) -> Result<f64, String> {
1279 let locals: Vec<f64> = [0.25, 0.5, 0.75]
1280 .into_iter()
1281 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
1282 .collect();
1283 probe_residual_at(surface, curve, pcurve, parameters, domain, &locals)
1284}
1285
1286/// Whether a projection-tracking candidate replaces the fit standing in
1287/// [`refine_pcurve`], given the `reading` it is judged on (see
1288/// [`judge_tracking_candidate`]). `standing_dense` says the standing fit's
1289/// reading is itself a nineteen-point one: a budget-exit candidate was taken.
1290///
1291/// While the standing reading is the fitter's own, a `Converged` candidate is
1292/// taken exactly as before (within the floor, better). A BUDGET exit
1293/// (`SampleCeiling`, `RoundBudget`, `Stalled`, `SpanFloor`) is taken only to
1294/// repair a standing fit that MISSES the floor, and only when its
1295/// nineteen-point reading is finite, within the unchanged floor and strictly
1296/// better. Once the standing reading is a nineteen-point one, every later
1297/// candidate is compared nineteen against nineteen: a Converged one must read
1298/// finite, within the floor and strictly better on the same stencil, and a
1299/// budget exit is never taken (the standing fit is then within the floor).
1300/// The exit label says how the loop stopped, not whether the curve is good: on
1301/// the 15° constructed-cylinder union all six imprint pcurves that shipped
1302/// 1.3e-7..7.0e-6 off had a tracking candidate reading 1.4e-8..9.95e-8 at
1303/// three points per span, discarded for its label (2026-10-04). A standing
1304/// fit already within the floor is never replaced by a budget exit, so every
1305/// in-floor fit is unchanged; a taken candidate keeps its own exit, samples
1306/// and raise, and reports the reading it was taken on.
1307fn tracking_candidate_accepted(standing: f64, standing_dense: bool, floor: f64, exit: PcurveFitExit, reading: f64) -> bool {
1308 if exit == PcurveFitExit::Converged && !standing_dense {
1309 return reading <= floor && reading < standing;
1310 }
1311 if exit != PcurveFitExit::Converged && !(standing > floor) {
1312 return false;
1313 }
1314 reading.is_finite() && reading <= floor && reading < standing
1315}
1316
1317/// The reading a tracking candidate is judged on, and whether it replaces the
1318/// standing fit: `own` (the candidate's own residual) for a Converged
1319/// candidate while the standing reading is the fitter's own; otherwise
1320/// `dense` ([`tracking_acceptance_residual`]'s nineteen points per span), read
1321/// only when the candidate could be taken. An error from `dense` declines.
1322/// [`refine_pcurve`] decides and traces through this one function.
1323fn judge_tracking_candidate(
1324 standing: f64,
1325 standing_dense: bool,
1326 floor: f64,
1327 exit: PcurveFitExit,
1328 own: f64,
1329 dense: impl FnOnce() -> Result<f64, String>,
1330) -> (Result<Option<f64>, String>, bool) {
1331 judge_tracking_candidate_with_bounds(standing, standing_dense, floor, exit, own, dense, false)
1332}
1333
1334fn judge_tracking_candidate_with_bounds(
1335 standing: f64,
1336 standing_dense: bool,
1337 floor: f64,
1338 exit: PcurveFitExit,
1339 own: f64,
1340 dense: impl FnOnce() -> Result<f64, String>,
1341 bounds: bool,
1342) -> (Result<Option<f64>, String>, bool) {
1343 // A budget exit's own residual probes the quarter points of every
1344 // returned span. They are a subset of the nineteen-point stencil, so its
1345 // maximum cannot fall below this already over-floor lower bound.
1346 if bounds && exit != PcurveFitExit::Converged && own > floor {
1347 if std::env::var_os("BREP_PROFILE").is_some() { eprintln!("pcurve.reject_bound kind=quarter_residual"); }
1348 return (Ok(Some(own)), false);
1349 }
1350 let reading = if exit == PcurveFitExit::Converged && !standing_dense {
1351 Ok(Some(own))
1352 } else if exit == PcurveFitExit::Converged || standing > floor {
1353 dense().map(Some)
1354 } else {
1355 Ok(None)
1356 };
1357 let accepted = matches!(
1358 reading,
1359 Ok(Some(value)) if tracking_candidate_accepted(standing, standing_dense, floor, exit, value)
1360 );
1361 (reading, accepted)
1362}
1363
1364/// What one call to [`refine_pcurve`] achieved, beside the curve it left in
1365/// place.
1366struct Refinement {
1367 exit: PcurveFitExit,
1368 residual: f64,
1369 off_surface: f64,
1370 raised: bool,
1371 /// [`PcurveFitReport::clamped_excursion`] and
1372 /// [`PcurveFitReport::clamped_distance`] of the curve left in place.
1373 clamped: [f64; 2],
1374}
1375
1376/// Adaptive 3D-residual refinement of an interpolant, with the PER-TRIM sample
1377/// budget. Shared by [`fit_pcurve_on_surface`] (global inversion) and
1378/// [`fit_pcurve_on_surface_marched`] (seeded projection); `insert` is the one
1379/// step that differs — given a probe station's fraction, the current pcurve's
1380/// parameter there and the edge point, it returns the surface parameter to
1381/// interpolate through (or `None` to decline the station) and the distance
1382/// from the edge point to the surface at that station, its GAP.
1383///
1384/// **First pass.** Four rounds at [`MAX_PCURVE_SAMPLES`], each probing the
1385/// quarter-points of every span wider than 1e-3 of the parameter range and
1386/// inserting the stations over `floor`. Byte-for-byte the pre-2026-09-12
1387/// loop; every exit is classified ([`PcurveFitExit`]) and the residual is
1388/// measured on the RETURNED curve — over every span, unless the final round
1389/// probed every span, in which case that round's maximum already is.
1390///
1391/// **The raise.** A [`PcurveFitExit::SampleCeiling`] exit re-enters the four
1392/// rounds ONCE with [`PCURVE_RAISED_SAMPLES`], continuing from the samples it
1393/// has, when the residual exceeds `off_surface` by more than the floor — the
1394/// returned curve is worse than the edge curve's own distance from the
1395/// surface, so more samples CAN improve it. Measured on `25_cube_pierce_solid`
1396/// (2026-09-12): the edge curves there sit off their surfaces by up to 1.9e-4,
1397/// four of the eight ceiling exits already read their gap to four digits, and
1398/// raising those spent 900 inversions each for no change in the swept
1399/// residual. The raised pass therefore inserts only where the pcurve's surface
1400/// point is further than the floor from the PROJECTION of the edge point — its
1401/// tracking error, which the inversion just computed: it stops when the pcurve
1402/// tracks the surface's nearest point to the floor, which is the best any
1403/// pcurve can do, and reports [`PcurveFitExit::Stalled`] with the deviation it
1404/// actually reached when that is still over the floor. The limit of that pass
1405/// as the floor tightens is the projected edge curve itself, so a volume built
1406/// on it can be shown to converge; the deviation test alone cannot say that on
1407/// off-surface input. Grow-once, capped, deterministic.
1408#[allow(clippy::too_many_arguments)]
1409fn refine_pcurve(
1410 surface: &NurbsSurface,
1411 curve: &NurbsCurve,
1412 domain: [f64; 2],
1413 u_domain: [f64; 2],
1414 v_domain: [f64; 2],
1415 floor: f64,
1416 joins: &[f64],
1417 parameters: &mut Vec<f64>,
1418 raw: &mut Vec<[f64; 2]>,
1419 pcurve: &mut NurbsCurve,
1420 off_surface: f64,
1421 clamped: [f64; 2],
1422 mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
1423) -> Result<Refinement, String> {
1424 // Trace only (`BREP_DEBUG_PCURVE_FIT`): reads values the passes already
1425 // computed and decides nothing.
1426 let trace = std::env::var_os("BREP_DEBUG_PCURVE_FIT").is_some();
1427 let call = if trace {
1428 PCURVE_TRACK_CALL.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1
1429 } else {
1430 0
1431 };
1432 let reject_bounds = PCURVE_REJECT_BOUNDS.with(|state| state.get());
1433 let mut best = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
1434 floor, joins, parameters, raw, pcurve, off_surface, clamped, false, 1.0, false, &mut insert, false)?;
1435 if trace {
1436 eprintln!(
1437 "PCURVE-TRACK call={call} first-pass exit={:?} residual={:e} floor={floor:e} off_surface={:e} samples={} raised={} tracking={}",
1438 best.exit,
1439 best.residual,
1440 best.off_surface,
1441 parameters.len(),
1442 best.raised,
1443 if best.off_surface <= floor { "eligible" } else { "standoff-tracking" }
1444 );
1445 }
1446 if best.off_surface > floor {
1447 // The edge curve itself stands off the carrier by more than the floor
1448 // (its INHERITED standoff), so no pcurve can bring the image within the
1449 // floor of the edge point. What a pcurve CAN improve is its tracking
1450 // error: the image's distance from the edge point's foot. Measured
1451 // 2026-10-04 (a2b401d4d, 15-degree crossing, turn 295): a first pass
1452 // ended RoundBudget at 6.726e-6 over a 1.316e-7 standoff and every
1453 // tracking pass was skipped. No candidate refinement when the ACTUAL
1454 // tracking request is met: the first pass's own tracking, read on its
1455 // native spans and endpoints (a diagnostic read; it adds no station),
1456 // within the floor ([`standoff_tracking_rungs`]). A total residual
1457 // under standoff + floor does not prove that: a tangential slip t over
1458 // a standoff g reads sqrt(g^2 + t^2) < g + t.
1459 // `BREP_PCURVE_STANDOFF_TRACKING=0`: diagnostic A/B hatch, not a
1460 // policy. It restores the pre-lane answer (35c8610a1's parent: an
1461 // off-carrier first pass is returned as it stands) for a same-binary
1462 // attribution of a volume or census delta to this lane. Default (unset
1463 // or any other value) is unchanged. It does not touch the in-floor
1464 // tracking rungs or their raise rule, nor the range lane's end-span
1465 // challenger.
1466 if !standoff_tracking_enabled() {
1467 return Ok(best);
1468 }
1469 return standoff_tracking_rungs(surface, curve, domain, u_domain, v_domain, floor, joins,
1470 parameters, raw, pcurve, best, trace, call, &mut insert);
1471 }
1472 // Each target starts from the same baseline station set: retaining the
1473 // coarser target's inserts can exhaust the cap before the finer target
1474 // places its own error-directed stations. Keep the best rung
1475 // [`tracking_candidate_accepted`] takes.
1476 let baseline_parameters = parameters.clone();
1477 let baseline_raw = raw.clone();
1478 let baseline_curve = pcurve.clone();
1479 let baseline_off_surface = best.off_surface;
1480 let baseline_clamped = best.clamped;
1481 // Whether the first pass MISSED the floor: only then may a tracking
1482 // candidate that reaches the sample ceiling still missing its request take
1483 // the existing one-time raise (2026-10-04, 20-degree crossing, turn 295:
1484 // a 1.964e-6 first pass, a candidate at 600 samples reading 1.068e-7,
1485 // declined; the raise needed residual > off_surface + floor). A first pass
1486 // within the floor keeps exactly the work it did before.
1487 // (On this path the edge stands within the floor of the carrier, so a
1488 // first pass over the floor is a tracking miss, not inherited standoff.)
1489 let standing_misses = best.residual > floor;
1490 Ok(tracking_rungs(
1491 best,
1492 floor,
1493 trace,
1494 call,
1495 true,
1496 |fraction| {
1497 let mut refined_parameters = baseline_parameters.clone();
1498 let mut refined_raw = baseline_raw.clone();
1499 let mut refined_curve = baseline_curve.clone();
1500 let candidate = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
1501 floor, joins, &mut refined_parameters, &mut refined_raw, &mut refined_curve,
1502 baseline_off_surface, baseline_clamped, true, fraction, standing_misses, &mut insert, reject_bounds && !standing_misses);
1503 (candidate, (refined_parameters, refined_raw, refined_curve))
1504 },
1505 |state: &RungState| tracking_acceptance_residual(surface, curve, &state.2, &state.0, domain),
1506 |state: &RungState| state.0.len(),
1507 |(refined_parameters, refined_raw, refined_curve): RungState| {
1508 *parameters = refined_parameters;
1509 *raw = refined_raw;
1510 *pcurve = refined_curve;
1511 },
1512 ))
1513}
1514
1515/// A tracking candidate's station parameters, raw surface parameters and
1516/// pcurve, as [`refine_pcurve`] hands them to [`tracking_rungs`].
1517type RungState = (Vec<f64>, Vec<[f64; 2]>, NurbsCurve);
1518
1519/// The tracking rungs of [`refine_pcurve`] after an eligible first pass: for
1520/// each tracking fraction, skip it if the standing fit is already that far
1521/// inside the floor, otherwise run the candidate pass (`run_pass`), judge it
1522/// ([`judge_tracking_candidate`], reading `dense` only when needed), trace
1523/// it, and either take it (`adopt` its state; it becomes the standing fit) or
1524/// stop. `samples` is the candidate's station count, for the trace. Separated
1525/// so the accepted-budget state can be driven with scripted passes;
1526/// `thread_dense` is `true` in production and `false` only to show, in a
1527/// test, what dropping the nineteen-point standing flag would take.
1528#[allow(clippy::too_many_arguments)]
1529fn tracking_rungs<S>(
1530 mut best: Refinement,
1531 floor: f64,
1532 trace: bool,
1533 call: u64,
1534 thread_dense: bool,
1535 mut run_pass: impl FnMut(f64) -> (Result<Refinement, String>, S),
1536 mut dense: impl FnMut(&S) -> Result<f64, String>,
1537 samples: impl Fn(&S) -> usize,
1538 mut adopt: impl FnMut(S),
1539) -> Refinement {
1540 // Whether the standing reading is a nineteen-point one (a budget-exit
1541 // candidate was taken); every later candidate is then judged on it.
1542 let mut standing_dense = false;
1543 let reject_bounds = PCURVE_REJECT_BOUNDS.with(|state| state.get());
1544 for fraction in [0.1, 0.01] {
1545 if best.residual <= floor * fraction {
1546 if trace {
1547 eprintln!(
1548 "PCURVE-TRACK call={call} candidate fraction={fraction} skipped best_residual={:e} <= {:e}",
1549 best.residual,
1550 floor * fraction
1551 );
1552 }
1553 continue;
1554 }
1555 let (candidate, state) = run_pass(fraction);
1556 // The reading the candidate is judged on and the decision (see
1557 // judge_tracking_candidate); an unreadable read declines like a
1558 // failed pass.
1559 let judged_dense = matches!(&candidate, Ok(seen) if seen.exit != PcurveFitExit::Converged || standing_dense);
1560 let (effective, accepted) = match &candidate {
1561 Ok(seen) => judge_tracking_candidate_with_bounds(best.residual, standing_dense, floor, seen.exit, seen.residual, || {
1562 dense(&state)
1563 }, reject_bounds),
1564 Err(_) => (Ok(None), false),
1565 };
1566 if trace {
1567 let judged_on = match &effective {
1568 Ok(Some(reading)) => format!("{reading:e}"),
1569 Ok(None) => "-".to_string(),
1570 Err(message) => format!("error:{message:?}"),
1571 };
1572 match &candidate {
1573 Ok(seen) => eprintln!(
1574 "PCURVE-TRACK call={call} candidate fraction={fraction} exit={:?} residual={:e} judged_on={judged_on} standing_dense={standing_dense} floor={floor:e} off_surface={:e} samples={} raised={} residual_meets_floor={} decision={}",
1575 seen.exit,
1576 seen.residual,
1577 seen.off_surface,
1578 samples(&state),
1579 seen.raised,
1580 seen.residual <= floor,
1581 if accepted { "accept" } else { "decline-stop" }
1582 ),
1583 Err(message) => eprintln!(
1584 "PCURVE-TRACK call={call} candidate fraction={fraction} error={message:?} decision=decline-stop"
1585 ),
1586 }
1587 }
1588 match candidate {
1589 Ok(mut candidate) if accepted => {
1590 // Report the reading it was taken on (its own, if Converged).
1591 if let Ok(Some(reading)) = effective {
1592 candidate.residual = reading;
1593 }
1594 // From here on the standing reading is a nineteen-point one.
1595 if thread_dense {
1596 standing_dense |= judged_dense;
1597 }
1598 adopt(state);
1599 best = candidate;
1600 }
1601 _ => break,
1602 }
1603 }
1604 best
1605}
1606
1607/// Whether [`refine_pcurve`] takes [`standoff_tracking_rungs`] for an
1608/// off-carrier first pass: true unless `BREP_PCURVE_STANDOFF_TRACKING=0`
1609/// (diagnostic A/B hatch, read once per process).
1610fn standoff_tracking_enabled() -> bool {
1611 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1612 *ON.get_or_init(|| std::env::var_os("BREP_PCURVE_STANDOFF_TRACKING").is_none_or(|value| value != "0"))
1613}
1614
1615/// The tracking rungs for an edge curve that stands OFF its carrier by more
1616/// than the floor ([`refine_pcurve`]). The first pass is read natively
1617/// ([`native_read`] on its own knot spans and endpoints); when its tracking is
1618/// within the floor, or the read fails, nothing more is done. Otherwise each
1619/// candidate pass (fractions 0.1 then 0.01 of the floor, from the same
1620/// baseline stations, taking the one-time raise when its own tracking target
1621/// is cut short by the ceiling) is read with the standing fit at the SAME
1622/// probes (both curves' native spans and endpoints) and taken only per
1623/// [`standoff_candidate_accepted`]: tracking finite, within the unchanged
1624/// floor and strictly lower, same-parameter deviation no worse, refinement
1625/// fields finite. A taken fit is REPORTED with its same-probe deviation
1626/// (inherited standoff included, so still over the floor) and with an
1627/// `off_surface` that is the larger of its station reading and the probes'
1628/// standoff. Any error, or a candidate that does not qualify, stops the loop
1629/// and keeps the standing curve and report.
1630#[allow(clippy::too_many_arguments)]
1631fn standoff_tracking_rungs(
1632 surface: &NurbsSurface,
1633 curve: &NurbsCurve,
1634 domain: [f64; 2],
1635 u_domain: [f64; 2],
1636 v_domain: [f64; 2],
1637 floor: f64,
1638 joins: &[f64],
1639 parameters: &mut Vec<f64>,
1640 raw: &mut Vec<[f64; 2]>,
1641 pcurve: &mut NurbsCurve,
1642 mut best: Refinement,
1643 trace: bool,
1644 call: u64,
1645 insert: &mut impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
1646) -> Result<Refinement, String> {
1647 let first = native_probe_fractions(&[&*pcurve]).and_then(|probes| native_read(surface, curve, pcurve, domain, &probes));
1648 if trace {
1649 eprintln!("PCURVE-TRACK call={call} standoff-tracking first-pass native={first:?} floor={floor:e}");
1650 }
1651 let Ok(first) = first else { return Ok(best) };
1652 if first.tracking <= floor {
1653 return Ok(best);
1654 }
1655 let baseline_parameters = parameters.clone();
1656 let baseline_raw = raw.clone();
1657 let baseline_curve = pcurve.clone();
1658 for fraction in [0.1, 0.01] {
1659 let mut refined_parameters = baseline_parameters.clone();
1660 let mut refined_raw = baseline_raw.clone();
1661 let mut refined_curve = baseline_curve.clone();
1662 let candidate = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain, floor, joins,
1663 &mut refined_parameters, &mut refined_raw, &mut refined_curve, best.off_surface, best.clamped,
1664 true, fraction, true, &mut *insert, false);
1665 let reads = candidate.as_ref().ok().map(|_| {
1666 native_probe_fractions(&[&*pcurve, &refined_curve]).and_then(|probes| {
1667 Ok((native_read(surface, curve, pcurve, domain, &probes)?, native_read(surface, curve, &refined_curve, domain, &probes)?))
1668 })
1669 });
1670 let fields_finite = matches!(&candidate, Ok(seen) if seen.residual.is_finite() && seen.off_surface.is_finite());
1671 let decision = match &reads {
1672 Some(Ok((standing, read))) => standoff_rung_decision(*standing, *read, fields_finite, floor),
1673 _ => RungDecision::Stop,
1674 };
1675 if trace {
1676 eprintln!(
1677 "PCURVE-TRACK call={call} standoff-tracking fraction={fraction} candidate={} reads={reads:?} decision={}",
1678 match &candidate { Ok(seen) => format!("{:?}/{:e}/raised={}", seen.exit, seen.residual, seen.raised), Err(message) => format!("error:{message:?}") },
1679 match decision { RungDecision::Accept => "accept", RungDecision::Continue => "decline-continue", RungDecision::Stop => "decline-stop" }
1680 );
1681 }
1682 let (Ok(mut taken), Some(Ok((_, read))), RungDecision::Accept) = (candidate, reads, decision) else {
1683 if decision == RungDecision::Continue {
1684 continue;
1685 }
1686 break;
1687 };
1688 taken.residual = read.deviation;
1689 taken.off_surface = taken.off_surface.max(read.standoff);
1690 *parameters = refined_parameters;
1691 *raw = refined_raw;
1692 *pcurve = refined_curve;
1693 best = taken;
1694 if read.tracking <= floor * 0.01 {
1695 break;
1696 }
1697 }
1698 Ok(best)
1699}
1700
1701/// What one off-carrier tracking rung does with its candidate
1702/// ([`standoff_tracking_rungs`]).
1703#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1704enum RungDecision {
1705 /// Taken ([`standoff_candidate_accepted`]).
1706 Accept,
1707 /// Not taken, but it read finite and LOWER tracking than the standing fit
1708 /// while over the floor: the finer fraction is still tried from the same
1709 /// baseline (fixture 25 on e90c30c94: one use read 2.33e-7 at 0.1 and
1710 /// stopped while its partner was taken at 9.0e-9).
1711 Continue,
1712 /// Not taken and not improving (or unreadable): the rungs end.
1713 Stop,
1714}
1715
1716fn standoff_rung_decision(standing: NativeRead, candidate: NativeRead, fields_finite: bool, floor: f64) -> RungDecision {
1717 if standoff_candidate_accepted(standing, candidate, fields_finite, floor) {
1718 RungDecision::Accept
1719 } else if candidate.tracking.is_finite() && candidate.tracking < standing.tracking {
1720 RungDecision::Continue
1721 } else {
1722 RungDecision::Stop
1723 }
1724}
1725
1726/// The one-time raise decision of [`refine_pcurve_pass`]: a ceiling exit not
1727/// yet raised, when the returned curve is worse than the edge's own gap by
1728/// more than the floor (the original rule), or when a tracking pass for an
1729/// unmet first pass returned a curve whose own tracking still misses the
1730/// pass's target (`unmet_tracking`, read on the returned curve). Nothing else
1731/// raises.
1732fn takes_raise(exit: PcurveFitExit, raised: bool, residual: f64, off_surface: f64, floor: f64, unmet_tracking: bool) -> bool {
1733 exit == PcurveFitExit::SampleCeiling && !raised && (residual > off_surface + floor || unmet_tracking)
1734}
1735
1736fn refine_pcurve_pass(
1737 surface: &NurbsSurface,
1738 curve: &NurbsCurve,
1739 [t0, t1]: [f64; 2],
1740 [u0, u1]: [f64; 2],
1741 [v0, v1]: [f64; 2],
1742 floor: f64,
1743 joins: &[f64],
1744 parameters: &mut Vec<f64>,
1745 raw: &mut Vec<[f64; 2]>,
1746 pcurve: &mut NurbsCurve,
1747 mut off_surface: f64,
1748 mut clamped: [f64; 2],
1749 track_projection: bool,
1750 tracking_fraction: f64,
1751 raise_unmet_tracking: bool,
1752 mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
1753 reject_budget: bool,
1754) -> Result<Refinement, String> {
1755 let _clock = PcurvePhaseClock::new("refinement", parameters.len(), track_projection);
1756 let tracking_floor = if track_projection { floor * tracking_fraction } else { floor };
1757 let mut max_samples = MAX_PCURVE_SAMPLES;
1758 let mut raised = false;
1759 loop {
1760 // Every way out of this loop is CLASSIFIED (`PcurveFitExit`); a budget
1761 // exit returns the curve it built, but says so. Falling off the end
1762 // after a fourth round of inserts is the round-budget exit.
1763 let mut exit = PcurveFitExit::RoundBudget;
1764 let mut residual = None;
1765 for round in 0..4 {
1766 if parameters.len() >= max_samples {
1767 exit = PcurveFitExit::SampleCeiling;
1768 break;
1769 }
1770 let mut inserts = Vec::new();
1771 let mut round_max = 0.0_f64;
1772 let mut truncated = false;
1773 let mut skipped_narrow = false;
1774 for index in 0..parameters.len() - 1 {
1775 if parameters.len() + inserts.len() >= max_samples {
1776 truncated = true;
1777 break;
1778 }
1779 let start = parameters[index];
1780 let end = parameters[index + 1];
1781 // Keep construction's existing first-pass policy. Extended
1782 // residual probes must not silently change its station ladder;
1783 // projection tracking retains its established finer floor.
1784 if end - start < if track_projection { SPAN_FLOOR } else { 1e-3 } {
1785 skipped_narrow = true;
1786 continue;
1787 }
1788 for local_fraction in [0.25, 0.5, 0.75] {
1789 if parameters.len() + inserts.len() >= max_samples {
1790 truncated = true;
1791 break;
1792 }
1793 let fraction = start + (end - start) * local_fraction;
1794 let parameter = pcurve.evaluate(fraction)?;
1795 let on_surface =
1796 section_surface_point(surface, parameter.x, parameter.y, true)?;
1797 let on_curve = section_curve_point(curve, t0 + (t1 - t0) * fraction)?;
1798 let deviation = on_surface.sub(on_curve).length();
1799 // A non-finite distance must not reach `f64::max` (which
1800 // drops NaN) or the classification below, where a round
1801 // with an unreadable station could still exit Converged on
1802 // its readable ones. Fail closed: a first pass's error
1803 // goes to its caller, a tracking candidate's declines it.
1804 if !deviation.is_finite() {
1805 return Err(format!(
1806 "refine_pcurve_pass: non-finite station distance {deviation} at fraction {fraction}"
1807 ));
1808 }
1809 round_max = round_max.max(deviation);
1810 if deviation <= tracking_floor {
1811 continue;
1812 }
1813 let (surface_parameter, gap) = insert(fraction, parameter, on_curve)?;
1814 off_surface = off_surface.max(gap);
1815 if let Some(surface_parameter) = surface_parameter {
1816 // The raised pass stops where the pcurve TRACKS the
1817 // edge curve's projection to the floor — the best any
1818 // pcurve can do at this station. Not `deviation - gap`:
1819 // a surface point at tangential distance d from the
1820 // projection of a point g off the surface is only
1821 // d^2/(2g) further from it, so the deviation test is
1822 // quadratically blind to sideways error on off-surface
1823 // input (6e-6 of slop at g = 1.9e-4, floor 1e-7).
1824 if raised || track_projection {
1825 let projected =
1826 section_surface_point(surface, surface_parameter[0], surface_parameter[1], false)?;
1827 if on_surface.sub(projected).length() <= tracking_floor {
1828 continue;
1829 }
1830 }
1831 inserts.push((index + 1, fraction, surface_parameter));
1832 }
1833 }
1834 }
1835 if inserts.is_empty() {
1836 // Nothing to insert: every probed station is within the floor
1837 // (converged — unless a narrow span was skipped, in which case
1838 // the sweep below decides), or the ones over it could not be
1839 // improved (stalled — the edge curve is off the surface there).
1840 exit = if truncated {
1841 PcurveFitExit::SampleCeiling
1842 } else if round_max > floor {
1843 PcurveFitExit::Stalled
1844 } else if skipped_narrow {
1845 PcurveFitExit::SpanFloor
1846 } else {
1847 residual = Some(round_max);
1848 PcurveFitExit::Converged
1849 };
1850 break;
1851 }
1852 // With inserts still needed after the fourth non-truncated
1853 // round, this exit is RoundBudget. An in-floor standing fit
1854 // never adopts any budget exit; rebuilding and probing this
1855 // discarded candidate cannot affect its curve or report.
1856 if reject_budget && round == 3 && !truncated {
1857 if std::env::var_os("BREP_PROFILE").is_some() { eprintln!("pcurve.reject_bound kind=fifth_round"); }
1858 return Err("pcurve tracking candidate requires a fifth round".into());
1859 }
1860 for (at, fraction, surface_parameter) in inserts.into_iter().rev() {
1861 parameters.insert(at, fraction);
1862 raw.insert(at, surface_parameter);
1863 }
1864 let (rebuilt, excursion, distance) = build_interpolant_joined(surface, raw, parameters, joins)?;
1865 *pcurve = rebuilt;
1866 clamped = [excursion, distance];
1867 if truncated {
1868 exit = PcurveFitExit::SampleCeiling;
1869 break;
1870 }
1871 }
1872 let residual = match residual {
1873 Some(measured) => measured,
1874 // A budget exit never re-checked the curve it is returning, and a
1875 // converged round that skipped narrow spans never checked those:
1876 // measure every span.
1877 None => probe_residual(
1878 surface,
1879 curve,
1880 pcurve,
1881 parameters,
1882 [t0, t1],
1883 [u0, u1],
1884 [v0, v1],
1885 )?,
1886 };
1887 if exit == PcurveFitExit::SpanFloor && residual <= floor {
1888 exit = PcurveFitExit::Converged;
1889 }
1890 // The per-trim budget decision. Deterministic, grow-once, capped (I3):
1891 // the ceiling bound and the returned curve is worse than the edge
1892 // curve's own gap from the surface by more than the floor, so samples
1893 // can still buy accuracy. A curve already at its gap stops here —
1894 // reported, not retried.
1895 // A TRACKING pass for a standing fit that missed the floor (and only
1896 // then) also takes the same one-time raise when it hits the ceiling
1897 // still over the floor: its request is the projection's tracking, and
1898 // the ceiling cut it short. The raised maximum itself is unchanged.
1899 // The added raise serves a TRACKING pass for an unmet first pass whose
1900 // RETURNED curve still misses the pass's own target: its tracking (the
1901 // image against the projected foot, read natively on its own knot spans
1902 // and endpoints) over the tracking floor (floor x fraction). Reaching
1903 // the ceiling alone is exhaustion, not a miss, and the inherited
1904 // standoff never triggers it. Read only at a ceiling exit not yet
1905 // raised; an unreadable read does not raise.
1906 let unmet_tracking = track_projection
1907 && raise_unmet_tracking
1908 && exit == PcurveFitExit::SampleCeiling
1909 && !raised
1910 && native_probe_fractions(&[&*pcurve])
1911 .and_then(|probes| native_read(surface, curve, pcurve, [t0, t1], &probes))
1912 .is_ok_and(|read| read.tracking > tracking_floor);
1913 if takes_raise(exit, raised, residual, off_surface, floor, unmet_tracking) {
1914 raised = true;
1915 max_samples = PCURVE_RAISED_SAMPLES;
1916 continue;
1917 }
1918 return Ok(Refinement {
1919 exit,
1920 residual,
1921 off_surface,
1922 raised,
1923 clamped,
1924 });
1925 }
1926}
1927
1928/// The exact inverse of an affine carrier's parameterization.
1929///
1930/// An affine patch (2x2 net, degree 1 both ways, equal weights) maps `uv` to 3D
1931/// by an affine map, so the inverse — a 3D point to the `uv` of its orthogonal
1932/// projection — is affine too. An affine map commutes with the rational basis:
1933/// sending a curve's control points through it and keeping the weights
1934/// produces the EXACT image of that curve in parameter space, not a fit of it.
1935struct AffineInversion {
1936 origin: Vec3,
1937 du: Vec3,
1938 dv: Vec3,
1939 u0: f64,
1940 v0: f64,
1941 uu: f64,
1942 uv: f64,
1943 vv: f64,
1944 determinant: f64,
1945}
1946
1947impl AffineInversion {
1948 /// `None` when `surface` is not affine, or when its two directions are
1949 /// parallel enough that the inverse is not determined. The caller decides
1950 /// what that means: a refusal on the whole-curve lane, the sampled lane on
1951 /// the range lane.
1952 fn of(surface: &NurbsSurface) -> Result<Option<Self>, String> {
1953 if !surface.is_affine()? {
1954 return Ok(None);
1955 }
1956 let ([u0, _], [v0, _]) = surface_domains(surface)?;
1957 let (origin, du, dv) = surface.deriv1(u0, v0)?;
1958 let uu = du.dot(du);
1959 let uv = du.dot(dv);
1960 let vv = dv.dot(dv);
1961 let determinant = uu * vv - uv * uv;
1962 if determinant.abs() <= 1e-18 {
1963 return Ok(None);
1964 }
1965 Ok(Some(Self {
1966 origin,
1967 du,
1968 dv,
1969 u0,
1970 v0,
1971 uu,
1972 uv,
1973 vv,
1974 determinant,
1975 }))
1976 }
1977
1978 fn uv_of(&self, point: Vec3) -> [f64; 2] {
1979 let delta = point.sub(self.origin);
1980 let along_u = delta.dot(self.du);
1981 let along_v = delta.dot(self.dv);
1982 [
1983 self.u0 + (along_u * self.vv - along_v * self.uv) / self.determinant,
1984 self.v0 + (along_v * self.uu - along_u * self.uv) / self.determinant,
1985 ]
1986 }
1987
1988 /// The exact image of `curve` in this carrier's parameter space, with the
1989 /// curve's own degree, knots and weights.
1990 fn image_of(&self, curve: &NurbsCurve) -> Result<NurbsCurve, String> {
1991 let control_points = curve
1992 .control_points
1993 .iter()
1994 .map(|control| {
1995 let [u, v] = self.uv_of(control.point()?);
1996 Ok(Vec4::from_point(Vec3::new(u, v, 0.0), control.w))
1997 })
1998 .collect::<Result<Vec<_>, String>>()?;
1999 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
2000 }
2001}
2002
2003/// Build a parameter-space curve for a 3D curve lying on a surface, and say
2004/// what the fit achieved.
2005///
2006/// This mirrors the reference imprint implementation: affine patches map
2007/// homogeneous control points exactly; general patches use sampled inversion,
2008/// periodic seam unwrapping, and adaptive 3D residual refinement. The report
2009/// beside the curve carries the residual the RETURNED curve reaches and why
2010/// refinement stopped ([`PcurveFitReport`]); the curve itself is what
2011/// [`build_pcurve_on_surface`] has always returned.
2012pub fn fit_pcurve_on_surface(
2013 surface: &NurbsSurface,
2014 curve: &NurbsCurve,
2015) -> Result<PcurveFit, String> {
2016 let mode = std::env::var("BREP_PCURVE_EVAL_CACHE").unwrap_or_default();
2017 let bounds = std::env::var("BREP_PCURVE_REJECT_BOUNDS").unwrap_or_else(|_| "on".into());
2018 let stack = std::env::var("BREP_PCURVE_STACK_POINT").unwrap_or_else(|_| "on".into());
2019 if mode == "check" || bounds == "check" || stack == "check" {
2020 let old = fit_pcurve_on_surface_cached(surface, curve, false, false, false)?;
2021 if matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) {
2022 let candidate = fit_pcurve_on_surface_cached(surface, curve, mode == "check" || mode == "on" || mode == "1", bounds == "check" || bounds == "on" || bounds == "1", stack == "check" || stack == "on" || stack == "1");
2023 let identical = candidate.as_ref().is_ok_and(|new| {
2024 old.curve.degree == new.curve.degree
2025 && old.curve.knots.len() == new.curve.knots.len()
2026 && old.curve.knots.iter().zip(&new.curve.knots).all(|(a, b)| a.to_bits() == b.to_bits())
2027 && old.curve.control_points.len() == new.curve.control_points.len()
2028 && old.curve.control_points.iter().zip(&new.curve.control_points).all(|(a, b)| {
2029 [a.x.to_bits(), a.y.to_bits(), a.z.to_bits(), a.w.to_bits()]
2030 == [b.x.to_bits(), b.y.to_bits(), b.z.to_bits(), b.w.to_bits()]
2031 }) && old.report == new.report
2032 });
2033 if let Ok(path) = std::env::var("BREP_PCURVE_EVAL_CENSUS") {
2034 use std::io::Write;
2035 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
2036 let _ = writeln!(file, "pcurve.eval_census identical={identical} controls={} samples={}", old.curve.control_points.len(), old.report.samples);
2037 }
2038 }
2039 }
2040 return Ok(old);
2041 }
2042 fit_pcurve_on_surface_cached(surface, curve, mode == "1" || mode == "on", bounds == "1" || bounds == "on", stack == "1" || stack == "on")
2043}
2044
2045fn fit_pcurve_on_surface_cached(surface: &NurbsSurface, curve: &NurbsCurve, enabled: bool, bounds: bool, stack: bool) -> Result<PcurveFit, String> {
2046 let _stack = PcurveStackPointScope::open(stack && matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })));
2047 let _samples = PcurveSampleScope::open(surface, curve, enabled);
2048 let _bounds = PcurveRejectScope::open(bounds && matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })));
2049 let [t0, t1] = curve.domain()?;
2050 if surface.is_affine()? {
2051 let Some(inversion) = AffineInversion::of(surface)? else {
2052 return Err("build_pcurve_on_surface: singular affine parameterization".into());
2053 };
2054 let pcurve = inversion.image_of(curve)?;
2055 return Ok(PcurveFit {
2056 report: PcurveFitReport {
2057 residual: 0.0,
2058 floor: PCURVE_REFINEMENT_TOLERANCE,
2059 samples: pcurve.control_points.len(),
2060 exit: PcurveFitExit::Converged,
2061 off_surface: 0.0,
2062 raised: false,
2063 clamped_excursion: 0.0,
2064 clamped_distance: 0.0,
2065 },
2066 curve: pcurve,
2067 });
2068 }
2069
2070 let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
2071 let drop_tolerance = 1e-4 * scale;
2072 let endpoint_tolerance = 1e-3 * scale;
2073 let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
2074 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
2075 let invert =
2076 |fraction: f64| invert_checked(surface, section_curve_point(curve, t0 + (t1 - t0) * fraction)?);
2077
2078 let mut parameters = Vec::with_capacity(25);
2079 let mut raw_surface_parameters = Vec::with_capacity(25);
2080 // The edge curve's own distance from the surface at every station inverted,
2081 // accepted or not: the floor below which no pcurve can bring the residual.
2082 let mut off_surface = 0.0_f64;
2083 // The edge's own C0 knots must remain corners of its parameter image.
2084 // Plant those stations before inversion; refinement retains their joins.
2085 let corners = range_breaks(curve, t0, t1, true);
2086 let mut stations: Vec<f64> = (0..=24).map(|index| index as f64 / 24.0).collect();
2087 // Replace nearby uniform seeds, never the native corner itself. Keep
2088 // endpoints and other native corners even when their spans are short.
2089 stations.retain(|station| {
2090 *station == 0.0 || *station == 1.0 || corners.contains(station)
2091 || !corners.iter().any(|corner| (*station - corner).abs() <= SPAN_FLOOR)
2092 });
2093 for &corner in &corners {
2094 let at = stations.partition_point(|value| *value < corner);
2095 if stations.get(at) != Some(&corner) {
2096 stations.insert(at, corner);
2097 }
2098 }
2099 let joins = corners;
2100 for (index, &fraction) in stations.iter().enumerate() {
2101 let (parameter, distance) = invert(fraction)?;
2102 off_surface = off_surface.max(distance);
2103 if distance > drop_tolerance && index != 0 && index != stations.len() - 1 {
2104 continue;
2105 }
2106 if distance > endpoint_tolerance {
2107 if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
2108 let p3 = curve.evaluate(t0 + (t1 - t0) * fraction).ok();
2109 let p0 = curve.evaluate(t0).ok();
2110 let p1 = curve.evaluate(t1).ok();
2111 eprintln!(
2112 "PCURVE-FAIL idx={index} frac={fraction} dist={distance} endpt_tol={endpoint_tolerance} scale={scale}\n curve3D@frac={p3:?}\n curve3D@t0={p0:?} curve3D@t1={p1:?}\n surf_domain=u[{u0},{u1}] v[{v0},{v1}] invpar={parameter:?}\n surf@invpar={:?}",
2113 surface.evaluate(parameter[0], parameter[1]).ok()
2114 );
2115 }
2116 return Err(format!(
2117 "build_pcurve_on_surface: endpoint projection failed (distance={distance})"
2118 ));
2119 }
2120 parameters.push(fraction);
2121 raw_surface_parameters.push(parameter);
2122 }
2123
2124 let (mut pcurve, excursion, distance) =
2125 build_interpolant_joined(surface, &raw_surface_parameters, ¶meters, &joins)?;
2126 let refinement = refine_pcurve(
2127 surface,
2128 curve,
2129 [t0, t1],
2130 [u0, u1],
2131 [v0, v1],
2132 refinement_tolerance,
2133 &joins,
2134 &mut parameters,
2135 &mut raw_surface_parameters,
2136 &mut pcurve,
2137 off_surface,
2138 [excursion, distance],
2139 |fraction, _parameter, _on_curve| {
2140 let (surface_parameter, distance) = invert(fraction)?;
2141 Ok((
2142 (distance <= drop_tolerance).then_some(surface_parameter),
2143 distance,
2144 ))
2145 },
2146 )?;
2147 Ok(PcurveFit {
2148 curve: pcurve,
2149 report: PcurveFitReport {
2150 residual: refinement.residual,
2151 floor: refinement_tolerance,
2152 samples: parameters.len(),
2153 exit: refinement.exit,
2154 off_surface: refinement.off_surface,
2155 raised: refinement.raised,
2156 clamped_excursion: refinement.clamped[0],
2157 clamped_distance: refinement.clamped[1],
2158 },
2159 })
2160}
2161
2162/// [`fit_pcurve_on_surface`] for the callers that only need the curve. The
2163/// report is not dropped: it is tallied on the open [`PcurveFitScope`], if any,
2164/// so the operation that owns this fit can say in its diagnostics whether every
2165/// trim reached the floor.
2166pub fn build_pcurve_on_surface(
2167 surface: &NurbsSurface,
2168 curve: &NurbsCurve,
2169) -> Result<NurbsCurve, String> {
2170 let fit = fit_pcurve_on_surface(surface, curve)?;
2171 record_fit(&fit.report);
2172 Ok(fit.curve)
2173}
2174
2175/// Optional section-fit clock; records every early fallback without changing it.
2176struct SectionFitClock {
2177 started: Option<web_time::Instant>,
2178 global: std::time::Duration,
2179 seeded: bool,
2180}
2181impl SectionFitClock {
2182 fn new() -> Self {
2183 Self { started: std::env::var_os("BREP_PROFILE").is_some().then(web_time::Instant::now),
2184 global: std::time::Duration::ZERO, seeded: false }
2185 }
2186}
2187impl Drop for SectionFitClock {
2188 fn drop(&mut self) {
2189 if let Some(started) = self.started {
2190 eprintln!("pcurve.section_time global={:.3} seeded={:.3} result={}",
2191 self.global.as_secs_f64() * 1000.0,
2192 started.elapsed().saturating_sub(self.global).as_secs_f64() * 1000.0,
2193 if self.seeded { "seeded" } else { "global" });
2194 }
2195 }
2196}
2197
2198/// SECTION-PCURVE SEEDED MARCH (t222: cone × ABC 00000327, non-integral genus).
2199///
2200/// `build_pcurve_on_surface` inverts every sample by GLOBAL closest point. Where
2201/// the carrier surface is COMPRESSED / self-overlapping along the row the
2202/// section rides, the global search aliases interior samples onto a DISTANT
2203/// preimage sheet, folding the pcurve out past the trim boundary and re-crossing
2204/// it. The arrangement then splits the shared section at that phantom crossing,
2205/// while the mate face (whose carrier is not folded there) keeps the section
2206/// whole — so the two operands' fragments disagree and the section strands
2207/// one-use (non-integral genus). Ground truth for t222: 00000327 face 496's
2208/// v≈0.7875 row maps u∈[0.55,0.81] into a ~0.18mm neighbourhood of the section
2209/// endpoint A; the global pcurve for piece 9 folded out to u=0.806 and back,
2210/// re-crossing trim edge 349 at v≈0.784, 0.003 below the corner vertex A.
2211///
2212/// This marches the interior inversions with each seeded from the PREVIOUS
2213/// accepted parameters, so a marched sample
2214/// adopts the nearby branch instead of the momentarily-closest distant sheet.
2215/// The two ENDPOINT samples keep the deterministic global inversion (they are
2216/// the piece's shared junction vertices — same reasoning as
2217/// `repair_branch_jumps`). Additive + fail-soft: the marched chain is used ONLY
2218/// when it CLOSES onto the global far endpoint (its last interior sample is
2219/// parameter-adjacent to it, measured against the chain's own median step);
2220/// otherwise the plain global build is returned byte-for-byte. A single-preimage
2221/// carrier (the cone side of the same section) marches to the same samples the
2222/// global search already had. Use the analytic-aware seeded projector for both
2223/// the march and inserted stations: raw Newton stops at 1e-7 even on an exact
2224/// cylinder, introducing avoidable trim error. General folded carriers retain
2225/// their local seeded Newton path; analytic carriers take their closed form.
2226///
2227/// Escape hatch: `BREP_SECTION_PCURVE_MARCH=0`.
2228pub fn fit_pcurve_on_surface_marched(
2229 surface: &NurbsSurface,
2230 curve: &NurbsCurve,
2231) -> Result<PcurveFit, String> {
2232 let mut clock = SectionFitClock::new();
2233 let global = fit_pcurve_on_surface(surface, curve)?;
2234 if let Some(started) = clock.started { clock.global = started.elapsed(); }
2235 if std::env::var("BREP_SECTION_PCURVE_MARCH").as_deref() == Ok("0") || surface.is_affine()? {
2236 return Ok(global);
2237 }
2238 let [t0, t1] = curve.domain()?;
2239 // A closed (ring) section has no distinct endpoints to anchor the march;
2240 // leave it to the existing path (closed rings are handled elsewhere).
2241 if curve.evaluate(t0)?.sub(curve.evaluate(t1)?).length() <= 1e-6 {
2242 return Ok(global);
2243 }
2244 const SAMPLES: usize = 24;
2245 let fractions: Vec<f64> = (0..=SAMPLES).map(|i| i as f64 / SAMPLES as f64).collect();
2246 let edge: Vec<Vec3> = fractions
2247 .iter()
2248 .map(|fraction| curve.evaluate(t0 + (t1 - t0) * fraction))
2249 .collect::<Result<Vec<_>, _>>()?;
2250 let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
2251 let endpoint_tolerance = 1e-3 * scale;
2252 let (first, first_distance) = invert_checked(surface, edge[0])?;
2253 let (last, last_distance) = invert_checked(surface, edge[SAMPLES])?;
2254 if first_distance > endpoint_tolerance || last_distance > endpoint_tolerance {
2255 return Ok(global);
2256 }
2257 // The edge curve's distance from the surface at every station this chain
2258 // inverts — on the MARCHED branch, which is the one being fitted.
2259 let mut off_surface = first_distance.max(last_distance);
2260 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
2261 let u_span = (u1 - u0).abs().max(EPSILON);
2262 let v_span = (v1 - v0).abs().max(EPSILON);
2263 let (closed_u, closed_v) = surface_closedness(surface)?;
2264 let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
2265 let mut du = a[0] - b[0];
2266 if closed_u {
2267 while du > 0.5 * u_span {
2268 du -= u_span;
2269 }
2270 while du < -0.5 * u_span {
2271 du += u_span;
2272 }
2273 }
2274 let mut dv = a[1] - b[1];
2275 if closed_v {
2276 while dv > 0.5 * v_span {
2277 dv -= v_span;
2278 }
2279 while dv < -0.5 * v_span {
2280 dv += v_span;
2281 }
2282 }
2283 ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
2284 };
2285 // Forward seeded march over the interior samples.
2286 let mut raw = vec![first];
2287 let mut previous = first;
2288 for index in 1..SAMPLES {
2289 let seeded =
2290 crate::projection::project_point_to_surface_from_seed(surface, edge[index], previous)?;
2291 let (_, global_distance) = invert_checked(surface, edge[index])?;
2292 // The seeded footpoint must still sit on the surface — as tight as the
2293 // fit band or the global answer already had it. If it fell off (the
2294 // seed led Newton into a valley), abandon the march and keep global.
2295 if seeded.distance > 4.0 * global_distance + endpoint_tolerance {
2296 return Ok(global);
2297 }
2298 off_surface = off_surface.max(seeded.distance);
2299 previous = [seeded.u, seeded.v];
2300 raw.push(previous);
2301 }
2302 raw.push(last);
2303 // CLOSURE GATE: the last marched interior sample must be parameter-adjacent
2304 // to the global far endpoint — the march stayed on ONE branch the whole way.
2305 // Measure the endpoint gap against the chain's OWN median step so a genuine
2306 // long edge is not rejected while a sheet-hop (which leaves a big gap to the
2307 // endpoint) is.
2308 let mut steps: Vec<f64> = (1..raw.len())
2309 .map(|i| norm_step(raw[i], raw[i - 1]))
2310 .collect();
2311 let endpoint_gap = steps.pop().unwrap_or(0.0);
2312 steps.sort_by(f64::total_cmp);
2313 let median = steps
2314 .get(steps.len() / 2)
2315 .copied()
2316 .unwrap_or(0.0)
2317 .max(EPSILON);
2318 if endpoint_gap > (4.0 * median).max(0.05) {
2319 return Ok(global);
2320 }
2321 // Build the interpolant from the marched samples, then refine — re-projecting
2322 // each insert SEEDED from the interpolant (already on the marched branch), so
2323 // a mid-refinement global inversion cannot re-alias onto the far sheet.
2324 let mut parameters = fractions;
2325 let (mut pcurve, excursion, distance) = build_interpolant(surface, &raw, ¶meters)?;
2326 let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
2327 // Same classified exits and budget ladder as `fit_pcurve_on_surface`. A
2328 // seeded insert always lands, so this loop cannot stall.
2329 let refinement = refine_pcurve(
2330 surface,
2331 curve,
2332 [t0, t1],
2333 [u0, u1],
2334 [v0, v1],
2335 refinement_tolerance,
2336 &[],
2337 &mut parameters,
2338 &mut raw,
2339 &mut pcurve,
2340 off_surface,
2341 [excursion, distance],
2342 |_fraction, seed, on_curve| {
2343 let seeded = crate::projection::project_point_to_surface_from_seed(surface, on_curve, [seed.x, seed.y])?;
2344 Ok((Some([seeded.u, seeded.v]), seeded.distance))
2345 },
2346 )?;
2347 clock.seeded = true;
2348 Ok(PcurveFit {
2349 curve: pcurve,
2350 report: PcurveFitReport {
2351 residual: refinement.residual,
2352 floor: refinement_tolerance,
2353 samples: parameters.len(),
2354 exit: refinement.exit,
2355 off_surface: refinement.off_surface,
2356 raised: refinement.raised,
2357 clamped_excursion: refinement.clamped[0],
2358 clamped_distance: refinement.clamped[1],
2359 },
2360 })
2361}
2362
2363/// [`fit_pcurve_on_surface_marched`] for the callers that only need the curve;
2364/// the report is tallied on the open [`PcurveFitScope`] exactly as
2365/// [`build_pcurve_on_surface`] does.
2366pub fn build_pcurve_on_surface_marched(
2367 surface: &NurbsSurface,
2368 curve: &NurbsCurve,
2369) -> Result<NurbsCurve, String> {
2370 let fit = fit_pcurve_on_surface_marched(surface, curve)?;
2371 record_fit(&fit.report);
2372 Ok(fit.curve)
2373}
2374
2375/// Build a pcurve for a represented subrange of a larger edge curve.
2376///
2377/// On an affine carrier the answer is exact and is taken as such
2378/// ([`affine_pcurve_on_range`]). Otherwise this samples only the represented
2379/// interval, which is essential when off-interval control points do not lie on
2380/// the target carrier — the exact lane restricts the net by knot insertion
2381/// first, so those control points are removed rather than ignored.
2382pub fn fit_pcurve_on_surface_range(
2383 surface: &NurbsSurface,
2384 curve: &NurbsCurve,
2385 edge_start: f64,
2386 edge_end: f64,
2387 forward: bool,
2388 tolerance: f64,
2389) -> Result<PcurveFit, String> {
2390 if let Some(exact) =
2391 affine_pcurve_on_range(surface, curve, edge_start, edge_end, forward, tolerance)?
2392 {
2393 return Ok(exact);
2394 }
2395 fit_pcurve_on_surface_range_dense(
2396 surface, curve, edge_start, edge_end, forward, tolerance, 64, 3, 513,
2397 )
2398}
2399
2400/// [`fit_pcurve_on_surface_range`] for the callers that only need the curve:
2401/// the fit is ACCEPTED or REFUSED on its bar ([`accept_fit`]), never returned
2402/// off it.
2403pub fn build_pcurve_on_surface_range(
2404 surface: &NurbsSurface,
2405 curve: &NurbsCurve,
2406 edge_start: f64,
2407 edge_end: f64,
2408 forward: bool,
2409 tolerance: f64,
2410) -> Result<NurbsCurve, String> {
2411 accept_fit(fit_pcurve_on_surface_range(
2412 surface, curve, edge_start, edge_end, forward, tolerance,
2413 )?)
2414}
2415
2416/// `BREP_DEBUG_STATION_FIT`: trace every station fit round by round, and the
2417/// stations around the worst span of a fit off its bar. Read once per process.
2418fn station_fit_tracing() -> bool {
2419 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2420 *ON.get_or_init(|| std::env::var_os("BREP_DEBUG_STATION_FIT").is_some())
2421}
2422
2423/// Narrowest span, as a fraction of the parameter range, that
2424/// [`fit_pcurve_on_surface_stations`] subdivides. `refine_pcurve` floors at
2425/// 1e-3 from 25 base samples; this fitter starts from 64 and quarters, so a
2426/// floor of 1e-3 bound on its first pass and refused 67 of the 57-document
2427/// corpus's bodies (measured 2026-09-26). 1e-6 admits every halving a real
2428/// trim needs and stops the runaway that fits stations 4e-7 apart.
2429const SPAN_FLOOR: f64 = 1e-6;
2430
2431/// Rounds the raised pass of [`fit_pcurve_on_surface_stations`] may spend:
2432/// enough halvings for any span the sample cap can resolve.
2433const RAISED_ROUNDS: usize = 40;
2434
2435/// The refusal every station builder returns for a fit off its bar.
2436pub const PCURVE_OFF_BAR: &str = "pcurve fit refused: the trim misses its stations by";
2437
2438/// The one acceptance decision behind [`build_pcurve_on_surface_range`],
2439/// [`build_pcurve_on_surface_range_dense`] and
2440/// [`build_pcurve_on_surface_stations`]: a fit on its bar
2441/// ([`PcurveFitReport::on_bar`]) is the curve, a fit off it is a refusal that
2442/// names the residual, the bar, what was asked, how far the stations stand off
2443/// the carrier and why refinement stopped.
2444///
2445/// Until 2026-09-26 these builders returned whatever their last round built
2446/// without measuring it (the 2026-09-18 census: 4564 of 54556 imported pcurves
2447/// over the ask they were built to, 2072 of them at a station the refinement
2448/// itself had tested and given up on). Nothing here widens a band: the bar is
2449/// the caller's own ask plus what the input makes unreachable.
2450fn accept_fit(fit: PcurveFit) -> Result<NurbsCurve, String> {
2451 let report = fit.report;
2452 if report.on_bar() {
2453 return Ok(fit.curve);
2454 }
2455 Err(off_bar_message(&report))
2456}
2457
2458fn off_bar_message(report: &PcurveFitReport) -> String {
2459 format!(
2460 "{PCURVE_OFF_BAR} {:.3e} out of sample against a bar of {:.3e} (asked {:.1e}, the \
2461 stations stand {:.3e} off the carrier; {:?} at {} samples{}{})",
2462 report.residual,
2463 report.floor + report.off_surface,
2464 report.floor,
2465 report.off_surface,
2466 report.exit,
2467 report.samples,
2468 if report.raised { ", after the raise" } else { "" },
2469 if report.clamped_excursion > 0.0 {
2470 format!(
2471 "; clamped into the chart by {:.3e} in parameter, {:.3e} in 3D",
2472 report.clamped_excursion, report.clamped_distance
2473 )
2474 } else {
2475 String::new()
2476 },
2477 )
2478}
2479
2480/// The class slug of a pcurve fit refused off its bar
2481/// ([`crate::RefusalClass::NonConvergence`]): minted by
2482/// [`build_pcurve_on_surface_range_classed`] and read by
2483/// [`is_pcurve_off_bar`], so a dispatcher decides on the class, never on the
2484/// message text.
2485pub(crate) const PCURVE_OFF_BAR_WHAT: &str = "pcurve_fit_off_bar";
2486
2487/// Whether `refusal` is a pcurve fit that could not reach its bar.
2488pub(crate) fn is_pcurve_off_bar(refusal: &crate::KernelRefusal) -> bool {
2489 matches!(&refusal.class, crate::RefusalClass::NonConvergence { what } if what == PCURVE_OFF_BAR_WHAT)
2490}
2491
2492/// [`build_pcurve_on_surface_range`] with a typed refusal: a fit off its bar
2493/// is `NonConvergence { what: PCURVE_OFF_BAR_WHAT }` at `stage`, with the same
2494/// message; any other failure of the fit is `Internal` under `what`.
2495pub(crate) fn build_pcurve_on_surface_range_classed(
2496 surface: &NurbsSurface,
2497 curve: &NurbsCurve,
2498 edge_start: f64,
2499 edge_end: f64,
2500 forward: bool,
2501 tolerance: f64,
2502 stage: KernelStage,
2503 what: &'static str,
2504) -> Result<NurbsCurve, crate::KernelRefusal> {
2505 use crate::OrRefuse;
2506 let fit = fit_pcurve_on_surface_range(surface, curve, edge_start, edge_end, forward, tolerance)
2507 .or_refuse(stage, what)?;
2508 accept_fit_classed(fit, stage)
2509}
2510
2511/// [`accept_fit`] with the refusal typed: `NonConvergence { what:
2512/// PCURVE_OFF_BAR_WHAT }` at `stage`, the message unchanged.
2513pub(crate) fn accept_fit_classed(fit: PcurveFit, stage: KernelStage) -> Result<NurbsCurve, crate::KernelRefusal> {
2514 if fit.report.on_bar() {
2515 return Ok(fit.curve);
2516 }
2517 Err(crate::KernelRefusal {
2518 class: crate::RefusalClass::NonConvergence { what: PCURVE_OFF_BAR_WHAT.into() },
2519 stage,
2520 message: off_bar_message(&fit.report),
2521 })
2522}
2523
2524/// The EXACT pcurve for a coedge's subrange of `curve` on an affine carrier,
2525/// or `None` when there is no such answer and the sampled lane must run.
2526///
2527/// [`fit_pcurve_on_surface`] has always mapped a WHOLE curve's control net
2528/// exactly on an affine carrier. A coedge's trim is that same curve restricted
2529/// to `[edge_start, edge_end]` and oriented by `forward`, and both operations
2530/// are exact on a NURBS: [`NurbsCurve::split`] subdivides by knot insertion,
2531/// [`NurbsCurve::reversed`] mirrors the net, and rescaling the knots to
2532/// `[0, 1]` is affine in the edge's own parameter — which is precisely the
2533/// fraction convention the sampled lane uses, so the pcurve's correspondence
2534/// with the edge is the same one either lane produces.
2535///
2536/// Sampling where an exact answer exists leaves a real error: measured
2537/// 2026-09-17, a booleaned band rim's two planar trims sat 2.379e-4 and
2538/// 2.395e-4 off their edges, where this lane lands 1.4e-13 off.
2539///
2540/// The image is verified against the caller's own `tolerance` before it is
2541/// returned. The exact image of a curve that does NOT lie on this carrier is
2542/// its orthogonal projection, which is not that coedge's trim, so every such
2543/// case falls through to the sampled lane and keeps the refusals that lane
2544/// makes today.
2545fn affine_pcurve_on_range(
2546 surface: &NurbsSurface,
2547 curve: &NurbsCurve,
2548 edge_start: f64,
2549 edge_end: f64,
2550 forward: bool,
2551 tolerance: f64,
2552) -> Result<Option<PcurveFit>, String> {
2553 // Opt-out for A/B bisection of this lane against the sampled one, in the
2554 // shape of the two hatches beside it (`BREP_NO_PCURVE_REPAIR`,
2555 // `BREP_SECTION_PCURVE_MARCH`). It is what showed that the boolean's
2556 // planar residuals do NOT come through here: the same binary with this set
2557 // and unset read identically to every digit on 21_band_rim.
2558 if std::env::var("BREP_AFFINE_RANGE_PCURVE").as_deref() == Ok("0") {
2559 return Ok(None);
2560 }
2561 let Some(inversion) = AffineInversion::of(surface)? else {
2562 return Ok(None);
2563 };
2564 if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
2565 // The sampled lane owns this refusal; it is not this lane's to reword.
2566 return Ok(None);
2567 }
2568 let [t0, t1] = curve.domain()?;
2569 let start = edge_start.max(t0);
2570 let end = edge_end.min(t1);
2571 // A range narrower than the knot identity tolerance cannot be split onto
2572 // its own endpoints at all.
2573 if end - start <= 2.0 * KNOT_IDENTITY_TOL {
2574 return Ok(None);
2575 }
2576 let mut restricted = curve.clone();
2577 if end < t1 - KNOT_IDENTITY_TOL {
2578 restricted = restricted.split(end)?.0;
2579 }
2580 if start > t0 + KNOT_IDENTITY_TOL {
2581 restricted = restricted.split(start)?.1;
2582 }
2583 let mut pcurve = inversion.image_of(&restricted)?;
2584 if !forward {
2585 pcurve = pcurve.reversed()?;
2586 }
2587 let first = pcurve.knots[0];
2588 let last = pcurve.knots[pcurve.knots.len() - 1];
2589 if !(last - first > 0.0) {
2590 return Ok(None);
2591 }
2592 let knots = pcurve
2593 .knots
2594 .iter()
2595 .map(|knot| (knot - first) / (last - first))
2596 .collect::<Vec<_>>();
2597 let pcurve = NurbsCurve::new(pcurve.degree, knots, pcurve.control_points.clone())?;
2598
2599 // Verify against the range the CALLER asked for, not the one the
2600 // restriction produced: `split` snaps onto a nearby knot, so a snap that
2601 // moved the correspondence reads here as a deviation. Sampling at
2602 // fractions that are not the stations any lane interpolates through keeps
2603 // this a check on the whole trim rather than on its endpoints.
2604 let scale = 1.0 + curve.evaluate((start + end) / 2.0)?.length();
2605 let bar = tolerance.max(1e-11 * scale);
2606 let mut residual = 0.0_f64;
2607 for index in 0..=32 {
2608 let fraction = index as f64 / 32.0;
2609 let uv = pcurve.evaluate(fraction)?;
2610 // `evaluate` CLAMPS out-of-domain uv. A planar carrier's domain is a
2611 // bounding rectangle that a booleaned trim can leave, and an affine
2612 // patch's extension is the same plane, so read it unclamped.
2613 let on_carrier = surface.evaluate_extended(uv.x, uv.y)?;
2614 let edge_fraction = if forward { fraction } else { 1.0 - fraction };
2615 let on_edge = curve.evaluate(start + (end - start) * edge_fraction)?;
2616 residual = residual.max(on_carrier.sub(on_edge).length());
2617 if residual > bar {
2618 return Ok(None);
2619 }
2620 }
2621 let samples = pcurve.control_points.len();
2622 Ok(Some(PcurveFit {
2623 curve: pcurve,
2624 report: PcurveFitReport {
2625 residual,
2626 // The floor this lane VERIFIED against: the caller's ask, or the
2627 // floating-point floor of an exact map on a part this size. An
2628 // exact image is on its bar by construction; a caller asking for
2629 // less than the map's own precision is not refused for it.
2630 floor: bar,
2631 samples,
2632 exit: PcurveFitExit::Converged,
2633 off_surface: 0.0,
2634 raised: false,
2635 clamped_excursion: 0.0,
2636 clamped_distance: 0.0,
2637 },
2638 }))
2639}
2640
2641fn align_collapsed_endpoints(surface: &NurbsSurface, raw: &mut [[f64; 2]]) -> Result<(), String> {
2642 let anchor = surface.control_points[0][0].point()?;
2643 let mut extent = 0.0_f64;
2644 for control in surface.control_points.iter().flatten() {
2645 extent = extent.max(control.point()?.sub(anchor).length());
2646 }
2647 // This is a geometric identity check, independent of the fitting allowance
2648 // and of world position. Positive rational weights keep the whole iso-curve
2649 // inside the convex hull of its Euclidean controls. Cap the size coupling
2650 // at the linear tolerance so a large patch cannot erase a thin finite row.
2651 let pole_band = (1e-10 * (1.0 + extent)).min(LINEAR_TOLERANCE);
2652 for (endpoint, neighbor) in [(0, 1), (raw.len() - 1, raw.len() - 2)] {
2653 let point = surface.evaluate(raw[endpoint][0], raw[endpoint][1])?;
2654 for axis in 0..2 {
2655 let mut candidate = raw[endpoint];
2656 candidate[axis] = raw[neighbor][axis];
2657 if candidate == raw[endpoint]
2658 || surface
2659 .evaluate(candidate[0], candidate[1])?
2660 .sub(point)
2661 .length()
2662 > pole_band
2663 {
2664 continue;
2665 }
2666 let row = if axis == 0 {
2667 surface.iso_curve_v(raw[endpoint][1])?
2668 } else {
2669 surface.iso_curve_u(raw[endpoint][0])?
2670 };
2671 let mut collapsed = true;
2672 for control in &row.control_points {
2673 if control.point()?.sub(point).length() > pole_band {
2674 collapsed = false;
2675 break;
2676 }
2677 }
2678 if collapsed {
2679 raw[endpoint] = candidate;
2680 }
2681 }
2682 }
2683 Ok(())
2684}
2685
2686/// Re-seat any raw inversion sample that BRANCH-JUMPED — snapped to a distant
2687/// fold of a self-overlapping general carrier — back onto the branch traced by
2688/// its neighbours.
2689///
2690/// Each entry in `raw` is an INDEPENDENT global closest-point inversion of the
2691/// corresponding 3D edge sample. Every one is geometrically valid (on the
2692/// surface, at the edge), but they need not be CONTIGUOUS: where a rational
2693/// B-spline surface folds back over the small trimmed patch it carries, the
2694/// momentarily-closest fold flips from one sample to the next, so the raw
2695/// polygon zig-zags across the whole domain and self-crosses — and the region
2696/// its interpolant bounds no longer covers the true face (its surface flux can
2697/// exceed its own area). We anchor on the LONGEST run of mutually-continuous
2698/// samples — the fold the trimmed patch actually lies on, since a small face
2699/// lives on ONE fold and the jumped samples are the minority the global search
2700/// snapped elsewhere — then walk outward in both directions, re-seeding Newton
2701/// from the last good parameters and adopting the continuous footpoint whenever
2702/// it is geometrically as valid as the global one. Anchoring on consensus (not
2703/// blindly on sample 0, which can itself be an outlier that would drag the
2704/// whole edge onto the wrong branch and tear the loop open at its endpoint)
2705/// leaves continuous nonsingular fits unchanged. Certified collapsed endpoint
2706/// rows first adopt their neighbour's branch; a bad seed in the subsequent
2707/// jump repair cannot make a sample worse than the global search already had it.
2708fn repair_branch_jumps(
2709 surface: &NurbsSurface,
2710 edge_points: &[Vec3],
2711 raw: &mut [[f64; 2]],
2712 tolerance: f64,
2713) -> Result<(), String> {
2714 if raw.len() < 3 {
2715 return Ok(());
2716 }
2717 // Affine carriers invert linearly and exactly — no folds, nothing to chase.
2718 // Checked first so the common planar/affine face never touches the env.
2719 if surface.is_affine()? {
2720 return Ok(());
2721 }
2722 // Opt-out for A/B bisection of a STEP-import regression against this repair.
2723 if std::env::var("BREP_NO_PCURVE_REPAIR").is_ok() {
2724 return Ok(());
2725 }
2726 // A pole has no unique coordinate along its collapsed row. Continuing the
2727 // adjacent sample's branch is safe only when the ENTIRE row is collapsed;
2728 // coincident points across an ordinary seam or fold are not enough.
2729 align_collapsed_endpoints(surface, raw)?;
2730 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
2731 let u_span = (u1 - u0).abs().max(EPSILON);
2732 let v_span = (v1 - v0).abs().max(EPSILON);
2733 let (closed_u, closed_v) = surface_closedness(surface)?;
2734 // Normalised, seam-aware step between two parameter samples. Wrapping the
2735 // closed directions keeps a legitimate seam crossing SMALL, so the seam /
2736 // periodic-unwrap machinery elsewhere is never disturbed by this repair.
2737 let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
2738 let mut du = a[0] - b[0];
2739 if closed_u {
2740 while du > 0.5 * u_span {
2741 du -= u_span;
2742 }
2743 while du < -0.5 * u_span {
2744 du += u_span;
2745 }
2746 }
2747 let mut dv = a[1] - b[1];
2748 if closed_v {
2749 while dv > 0.5 * v_span {
2750 dv -= v_span;
2751 }
2752 while dv < -0.5 * v_span {
2753 dv += v_span;
2754 }
2755 }
2756 ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
2757 };
2758 // A jump crosses a large fraction of the WHOLE domain — orders above the
2759 // per-sample motion of any real (even domain-spanning) edge, which advances
2760 // ~1/N of its traversal between consecutive stations.
2761 const JUMP_THRESHOLD: f64 = 0.2;
2762 let count = raw.len();
2763
2764 // Locate the longest maximal run of consecutive continuous samples — the
2765 // branch to anchor on. If nothing jumps, this spans [0, count-1] and both
2766 // re-seat passes below are empty, leaving `raw` untouched.
2767 let (mut best_start, mut best_len, mut run_start) = (0usize, 1usize, 0usize);
2768 for index in 1..count {
2769 if norm_step(raw[index], raw[index - 1]) > JUMP_THRESHOLD {
2770 if index - run_start > best_len {
2771 best_len = index - run_start;
2772 best_start = run_start;
2773 }
2774 run_start = index;
2775 }
2776 }
2777 if count - run_start > best_len {
2778 best_len = count - run_start;
2779 best_start = run_start;
2780 }
2781 let (spine_lo, spine_hi) = (best_start, best_start + best_len - 1);
2782
2783 // Adopt the continuous footpoint only when it (a) still sits on the surface
2784 // — as tight as the fit band or the global answer — and (b) genuinely
2785 // closes the jump rather than trading it for another. Returns the parameter
2786 // to carry forward as the next seed (the repaired one, or the untouched
2787 // global when no repair applies).
2788 let reseat = |edge_point: Vec3,
2789 previous: [f64; 2],
2790 global: [f64; 2]|
2791 -> Result<[f64; 2], String> {
2792 let global_step = norm_step(global, previous);
2793 if global_step <= JUMP_THRESHOLD {
2794 return Ok(global);
2795 }
2796 let seeded =
2797 project_point_to_surface_seeded(surface, edge_point, previous[0], previous[1])?;
2798 let candidate = [seeded.u, seeded.v];
2799 let global_residual = surface
2800 .evaluate(global[0], global[1])?
2801 .sub(edge_point)
2802 .length();
2803 let on_surface = seeded.distance <= 4.0 * global_residual + tolerance.max(1e-12);
2804 if on_surface && norm_step(candidate, previous) < 0.5 * global_step {
2805 if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
2806 eprintln!(
2807 "pcurve repair: jump {global_step:.4} ({global:?}) -> {:.4} ({candidate:?}) res {:.2e}->{:.2e}",
2808 norm_step(candidate, previous),
2809 global_residual,
2810 seeded.distance
2811 );
2812 }
2813 Ok(candidate)
2814 } else {
2815 Ok(global)
2816 }
2817 };
2818
2819 // Walk forward off the spine's high end, then backward off its low end,
2820 // chaining each repaired sample as the next seed so continuity propagates.
2821 //
2822 // Apart from the certified collapsed rows above, the two ENDPOINT samples
2823 // (fraction 0 and 1) are NEVER moved: they are the
2824 // edge's shared loop vertices. The global inversion is deterministic, so the
2825 // two coedges meeting at a vertex land it at the SAME parameters even when
2826 // that vertex sits on a fold reachable from two branches — moving one side
2827 // to a different branch tears the loop open there (`synthesize_pole_edge`
2828 // then rejects the non-collapsed gap). A vertex's genuine fold transition
2829 // (an edge whose interior rides u≈0.08 but whose endpoint must meet its
2830 // neighbour at u≈0.95) is exactly this case and must be preserved, not
2831 // "continuity-repaired" back onto the interior branch.
2832 let mut previous = raw[spine_hi];
2833 for index in (spine_hi + 1)..count.saturating_sub(1) {
2834 raw[index] = reseat(edge_points[index], previous, raw[index])?;
2835 previous = raw[index];
2836 }
2837 let mut previous = raw[spine_lo];
2838 for index in (1..spine_lo).rev() {
2839 raw[index] = reseat(edge_points[index], previous, raw[index])?;
2840 previous = raw[index];
2841 }
2842 Ok(())
2843}
2844
2845/// Range fitter with explicit sampling knobs, reporting what the fit achieved.
2846/// [`fit_pcurve_on_surface_range`] keeps the long-standing (base 64, 3
2847/// refinement rounds, 513 cap) budget; a fit that binds at that cap with the
2848/// bar unmet grows once to [`PCURVE_RAISED_SAMPLES`]
2849/// ([`fit_pcurve_on_surface_stations`]).
2850///
2851/// NOTE (2026-09-03): this comment used to say "STEP import retries failed fits
2852/// with a denser budget". No caller passes these knobs any more — that retry
2853/// was removed and the sentence outlived it. The knobs are kept because they
2854/// are the honest way to ASK whether a fit had headroom left, which is what
2855/// `examples/pcurve_residual_probe.rs` uses them for: on every ABC corpus
2856/// coedge that failed `validate`'s pcurve check, budgets up to (512, 8, 8193)
2857/// with a 1000x tighter target reproduced the coarse fit's deviation to six
2858/// decimals, because that deviation is the edge-vs-surface closest-point
2859/// residual and a curve ON the surface cannot beat it.
2860#[allow(clippy::too_many_arguments)]
2861pub fn fit_pcurve_on_surface_range_dense(
2862 surface: &NurbsSurface,
2863 curve: &NurbsCurve,
2864 edge_start: f64,
2865 edge_end: f64,
2866 forward: bool,
2867 tolerance: f64,
2868 base_samples: usize,
2869 refinement_rounds: usize,
2870 parameter_cap: usize,
2871) -> Result<PcurveFit, String> {
2872 if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
2873 return Err("build_pcurve_on_surface_range: invalid edge interval".into());
2874 }
2875 let evaluate_edge = |fraction: f64| {
2876 let edge_fraction = if forward { fraction } else { 1.0 - fraction };
2877 curve.evaluate(edge_start + (edge_end - edge_start) * edge_fraction)
2878 };
2879 let breaks = range_breaks(curve, edge_start, edge_end, forward);
2880 let ask = tolerance.max(PCURVE_REFINEMENT_TOLERANCE);
2881 let fit = fit_station_pcurve(
2882 surface,
2883 &evaluate_edge,
2884 &breaks,
2885 &breaks,
2886 ask,
2887 base_samples,
2888 refinement_rounds,
2889 parameter_cap,
2890 SPAN_FLOOR,
2891 )?;
2892 end_span_challenger(surface, curve, &evaluate_edge, edge_start, edge_end, forward, &breaks, ask, base_samples, refinement_rounds, parameter_cap, fit)
2893}
2894
2895/// The edge curve's OWN distinct interior knots inside `[edge_start,
2896/// edge_end]`, mapped through the trim's range and sense to coedge
2897/// fractions, that fall inside `pcurve`'s FIRST or LAST span (between the
2898/// domain end and its nearest interior knot), sorted.
2899fn native_end_knot_fractions(curve: &NurbsCurve, edge_start: f64, edge_end: f64, forward: bool, pcurve: &NurbsCurve) -> Result<Vec<f64>, String> {
2900 let [q0, q1] = pcurve.domain()?;
2901 let mut interior: Vec<f64> = pcurve.knots.iter().copied().filter(|k| *k > q0 && *k < q1).collect();
2902 interior.dedup();
2903 let (Some(&first), Some(&last)) = (interior.first(), interior.last()) else { return Ok(Vec::new()) };
2904 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
2905 let span = edge_end - edge_start;
2906 let mut fractions: Vec<f64> = curve
2907 .knots
2908 .iter()
2909 .copied()
2910 .filter(|k| *k > low && *k < high)
2911 .map(|k| {
2912 let along = (k - edge_start) / span;
2913 if forward { along } else { 1.0 - along }
2914 })
2915 .filter(|f| f.is_finite() && ((*f > q0 && *f < first) || (*f > last && *f < q1)))
2916 .collect();
2917 fractions.sort_by(f64::total_cmp);
2918 fractions.dedup();
2919 Ok(fractions)
2920}
2921
2922/// The admission rule for a foot read against an edge point: finite distance,
2923/// finite point, finite (u, v) inside the native domain. Shared by
2924/// [`common_station_read`] and the station fitter's tracking mode.
2925fn foot_admitted(foot: &crate::SurfaceProjection, [u0, u1]: [f64; 2], [v0, v1]: [f64; 2]) -> bool {
2926 let finite = |p: Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
2927 foot.distance.is_finite() && finite(foot.point) && foot.u.is_finite() && foot.v.is_finite() && (u0..=u1).contains(&foot.u) && (v0..=v1).contains(&foot.v)
2928}
2929
2930/// The tracking mode's verification probes: the current pcurve's native probes
2931/// ([`native_probe_fractions`]: endpoints, quarters and 16 interior points of
2932/// each knot span), its EXACT knots (the final common read includes them, and
2933/// the native probes do not), and the caller's fixed fractions; finite, in
2934/// [0, 1], sorted, deduplicated.
2935fn verification_fractions(pcurve: &NurbsCurve, verify: &[f64]) -> Result<Vec<f64>, String> {
2936 let mut fractions = native_probe_fractions(&[pcurve])?;
2937 fractions.extend(pcurve.knots.iter().copied());
2938 fractions.extend(verify.iter().copied());
2939 fractions.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
2940 fractions.sort_by(f64::total_cmp);
2941 fractions.dedup();
2942 Ok(fractions)
2943}
2944
2945/// The off-carrier candidate admission, at the same probes and shared feet
2946/// (`[tracking, deviation, excess]` per curve, [`common_station_read_with_excess`]):
2947/// every reading finite, the candidate's tracking strictly lower, and EITHER
2948/// its maximum deviation no worse than the legacy's (the original rule) OR the
2949/// candidate meeting the ORIGINAL floors outright, tracking and excess both
2950/// within `PCURVE_REFINEMENT_TOLERANCE` (1e-7).
2951///
2952/// Why the second branch: for fits whose images follow the shared admitted
2953/// feet, the maximum deviation can be dominated by the edge's standoff.
2954/// Their measured maxima can then differ by rounding while tracking improves.
2955/// This does not assert that the admitted foot is a certified global nearest
2956/// point or that an arbitrary pcurve has the same maximum deviation.
2957/// next5 fixture 25 (BREP_DEBUG_STATION_FIT): every tracking candidate reached
2958/// the floor (9.52e-8 .. 9.94e-8 tracking) and was declined on a deviation
2959/// 1.2e-14 .. 6.7e-13 above the legacy's at 2.1e-5 .. 9.0e-4 standoffs, while
2960/// the legacy kept 5.9e-7 .. 8.5e-5 tracking. A candidate within the original
2961/// floors on both readings is what those controls require; it is not a
2962/// standoff allowance and no bar moves.
2963pub(crate) fn floor_admits(old: [f64; 3], new: [f64; 3]) -> bool {
2964 let floor = PCURVE_REFINEMENT_TOLERANCE;
2965 old.iter().chain(new.iter()).all(|v| v.is_finite())
2966 && new[0] < old[0]
2967 && (new[1] <= old[1] || (new[0] <= floor && new[2] <= floor))
2968}
2969
2970/// The common reader's foot: the NEAREST admitted ([`foot_admitted`]) of the
2971/// global projection, the projection seeded at the reference (legacy) uv and
2972/// the one seeded at the candidate (image) uv, in THAT order, so that between
2973/// admitted feet at equal distance the first in this order is taken
2974/// (`min_by` keeps the first minimum). [`common_station_read`] and the station
2975/// fitter's tracking mode both take their foot here, so an equal-distance tie
2976/// resolves to the same foot in both.
2977fn common_foot(
2978 global: Option<crate::SurfaceProjection>,
2979 reference: Option<crate::SurfaceProjection>,
2980 candidate: Option<crate::SurfaceProjection>,
2981 domain_u: [f64; 2],
2982 domain_v: [f64; 2],
2983) -> Option<crate::SurfaceProjection> {
2984 [global, reference, candidate]
2985 .into_iter()
2986 .flatten()
2987 .filter(|f| foot_admitted(f, domain_u, domain_v))
2988 .min_by(|a, b| a.distance.total_cmp(&b.distance))
2989}
2990
2991/// The probes a RANGE trim is read at by the common reader: `curves`'
2992/// native spans (endpoints, quarters and 16 interior points of each knot
2993/// span) and exact knots, plus the edge curve's own knots CLIPPED to
2994/// `[t0, t1]` (with the range ends) and the same 19 points on every clipped
2995/// span, mapped through the coedge's sense to fractions; finite, in [0, 1],
2996/// sorted, deduplicated.
2997pub(crate) fn range_use_probes(curves: &[&NurbsCurve], edge_curve: &NurbsCurve, t0: f64, t1: f64, forward: bool) -> Result<Vec<f64>, String> {
2998 let mut probes = native_probe_fractions(curves)?;
2999 for curve in curves {
3000 probes.extend(curve.knots.iter().copied());
3001 }
3002 let span = t1 - t0;
3003 if span.is_finite() && span != 0.0 {
3004 let (low, high) = (t0.min(t1), t0.max(t1));
3005 let mut knots: Vec<f64> = edge_curve.knots.iter().copied().filter(|k| *k >= low && *k <= high).collect();
3006 knots.push(low);
3007 knots.push(high);
3008 knots.sort_by(f64::total_cmp);
3009 knots.dedup();
3010 let locals: Vec<f64> = [0.25, 0.5, 0.75]
3011 .into_iter()
3012 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
3013 .collect();
3014 let mut ts = knots.clone();
3015 for pair in knots.windows(2).filter(|w| w[1] > w[0]) {
3016 for &local in &locals {
3017 ts.push(pair[0] + (pair[1] - pair[0]) * local);
3018 }
3019 }
3020 for t in ts {
3021 let along = (t - t0) / span;
3022 probes.push(if forward { along } else { 1.0 - along });
3023 }
3024 }
3025 probes.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
3026 probes.sort_by(f64::total_cmp);
3027 probes.dedup();
3028 Ok(probes)
3029}
3030
3031/// [`common_station_read`] for two RANGE trims of one coedge (`old`, `new`,
3032/// both on the coedge fraction domain [0, 1]) against the edge curve over
3033/// `[t0, t1]` in the coedge's sense, at [`range_use_probes`] of both: the same
3034/// probes and one shared admitted foot per probe, finite or an error. A trim
3035/// not on [0, 1] is an error.
3036// Unused by the import lane since the tracking trigger and keep were withdrawn
3037// for this merge; kept, with its controls, for their re-proposal.
3038#[cfg_attr(not(test), allow(dead_code))]
3039pub(crate) fn range_common_read(
3040 surface: &NurbsSurface,
3041 edge_curve: &NurbsCurve,
3042 t0: f64,
3043 t1: f64,
3044 forward: bool,
3045 old: &NurbsCurve,
3046 new: &NurbsCurve,
3047) -> Result<((f64, f64), (f64, f64)), String> {
3048 let (o, n) = range_common_read_with_excess(surface, edge_curve, t0, t1, forward, old, new)?;
3049 Ok(((o[0], o[1]), (n[0], n[1])))
3050}
3051
3052/// [`range_common_read`] with each trim's maximum pointwise excess over the
3053/// shared admitted foot's own distance: `[tracking, deviation, excess]` per
3054/// trim ([`common_station_read_with_excess`]).
3055// Unused by the import lane since the tracking trigger and keep were withdrawn
3056// for this merge; kept, with its controls, for their re-proposal.
3057#[cfg_attr(not(test), allow(dead_code))]
3058pub(crate) fn range_common_read_with_excess(
3059 surface: &NurbsSurface,
3060 edge_curve: &NurbsCurve,
3061 t0: f64,
3062 t1: f64,
3063 forward: bool,
3064 old: &NurbsCurve,
3065 new: &NurbsCurve,
3066) -> Result<([f64; 3], [f64; 3]), String> {
3067 for curve in [old, new] {
3068 if !matches!(curve.domain(), Ok([a, b]) if a == 0.0 && b == 1.0) {
3069 return Err("range common read: a trim not on [0, 1]".into());
3070 }
3071 }
3072 let probes = range_use_probes(&[old, new], edge_curve, t0, t1, forward)?;
3073 let evaluate = |fraction: f64| -> Result<Vec3, String> {
3074 let along = if forward { fraction } else { 1.0 - fraction };
3075 edge_curve.evaluate(t0 + (t1 - t0) * along)
3076 };
3077 common_station_read_with_excess(surface, &evaluate, old, new, &probes)
3078}
3079
3080/// The legacy and candidate pcurves read against the edge at the SAME probes
3081/// and against the SAME foot per probe: at each fraction, the edge point's
3082/// foot is the NEAREST admitted one among the global projection and the
3083/// projections seeded at the legacy and the candidate image's (u, v). A foot
3084/// is admitted with a finite distance and point and a finite (u, v) inside the
3085/// native domain. Both images are then measured against that one shared foot
3086/// (TRACKING) and against the edge point (same-parameter DEVIATION). The
3087/// shared foot is the nearest of these three, not a certified global nearest.
3088/// Any non-finite image, point or distance, or a probe with no admitted foot,
3089/// is an error (the caller keeps the legacy fit). Returns
3090/// ((legacy tracking, legacy deviation), (candidate tracking, candidate
3091/// deviation)), maxima over the probes.
3092pub(crate) fn common_station_read(
3093 surface: &NurbsSurface,
3094 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3095 legacy: &NurbsCurve,
3096 candidate: &NurbsCurve,
3097 probes: &[f64],
3098) -> Result<((f64, f64), (f64, f64)), String> {
3099 let (old, new) = common_station_read_with_excess(surface, evaluate_edge, legacy, candidate, probes)?;
3100 Ok(((old[0], old[1]), (new[0], new[1])))
3101}
3102
3103/// [`common_station_read`] that also returns, per curve, the maximum
3104/// pointwise EXCESS: the same-parameter deviation minus the shared admitted
3105/// foot's own distance at that probe (the part over the edge's standoff
3106/// there, which the every-use controls bar at the original 1e-7). Returns
3107/// `[tracking, deviation, excess]` for the legacy and the candidate.
3108pub(crate) fn common_station_read_with_excess(
3109 surface: &NurbsSurface,
3110 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3111 legacy: &NurbsCurve,
3112 candidate: &NurbsCurve,
3113 probes: &[f64],
3114) -> Result<([f64; 3], [f64; 3]), String> {
3115 let (domain_u, domain_v) = surface_domains(surface)?;
3116 let finite = |p: Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
3117 // [tracking, deviation, excess] per curve; the excess (deviation minus
3118 // the shared foot's distance, per probe) starts at -inf, so a read with no
3119 // probe is not finite and admits nothing.
3120 let mut read = ([0.0_f64, 0.0, f64::NEG_INFINITY], [0.0_f64, 0.0, f64::NEG_INFINITY]);
3121 for &fraction in probes {
3122 let point = evaluate_edge(fraction)?;
3123 let (old_uv, new_uv) = (legacy.evaluate(fraction)?, candidate.evaluate(fraction)?);
3124 let (old_image, new_image) = (surface.evaluate_extended(old_uv.x, old_uv.y)?, surface.evaluate_extended(new_uv.x, new_uv.y)?);
3125 if !(finite(point) && finite(old_image) && finite(new_image)) {
3126 return Err(format!("common station read: non-finite point at fraction {fraction}"));
3127 }
3128 // Reading ONE curve against itself (`legacy` and `candidate` the same
3129 // object, as a trigger's read is): the candidate-seeded projection is
3130 // the reference-seeded one on identical inputs, so it is computed once
3131 // (same result, same tie order).
3132 let reference_foot = memo_seeded_foot(surface, point, old_uv.x, old_uv.y);
3133 let candidate_foot = if std::ptr::eq(legacy, candidate) {
3134 reference_foot
3135 } else {
3136 memo_seeded_foot(surface, point, new_uv.x, new_uv.y)
3137 };
3138 let foot = common_foot(
3139 memo_global_foot(surface, point),
3140 reference_foot,
3141 candidate_foot,
3142 domain_u,
3143 domain_v,
3144 )
3145 .ok_or_else(|| format!("common station read: no admitted foot at fraction {fraction}"))?;
3146 let values = [
3147 old_image.sub(foot.point).length(),
3148 old_image.sub(point).length(),
3149 new_image.sub(foot.point).length(),
3150 new_image.sub(point).length(),
3151 ];
3152 if values.iter().any(|v| !v.is_finite()) {
3153 return Err(format!("common station read: non-finite distance at fraction {fraction}"));
3154 }
3155 if !foot.distance.is_finite() {
3156 return Err(format!("common station read: non-finite foot distance at fraction {fraction}"));
3157 }
3158 read.0[0] = read.0[0].max(values[0]);
3159 read.0[1] = read.0[1].max(values[1]);
3160 read.0[2] = read.0[2].max(values[1] - foot.distance);
3161 read.1[0] = read.1[0].max(values[2]);
3162 read.1[1] = read.1[1].max(values[3]);
3163 read.1[2] = read.1[2].max(values[3] - foot.distance);
3164 }
3165 Ok(read)
3166}
3167
3168/// The interior probe fractions of the edge curve's own FIRST and LAST native
3169/// knot spans inside `[edge_start, edge_end]` (quarter points and 16 interior
3170/// points of each), mapped through the trim's range and sense to coedge
3171/// fractions in [0, 1].
3172fn edge_end_span_fractions(curve: &NurbsCurve, edge_start: f64, edge_end: f64, forward: bool) -> Vec<f64> {
3173 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
3174 let mut knots: Vec<f64> = curve.knots.iter().copied().filter(|k| *k > low && *k < high).collect();
3175 knots.dedup();
3176 let spans = match (knots.first(), knots.last()) {
3177 (Some(&first), Some(&last)) => vec![(low, first), (last, high)],
3178 _ => vec![(low, high)],
3179 };
3180 let span = edge_end - edge_start;
3181 let mut out = Vec::new();
3182 for (a, b) in spans {
3183 if !(b > a) {
3184 continue;
3185 }
3186 for local in [0.25, 0.5, 0.75].into_iter().chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64)) {
3187 let along = (a + (b - a) * local - edge_start) / span;
3188 let fraction = if forward { along } else { 1.0 - along };
3189 if fraction.is_finite() && (0.0..=1.0).contains(&fraction) {
3190 out.push(fraction);
3191 }
3192 }
3193 }
3194 out.sort_by(f64::total_cmp);
3195 out.dedup();
3196 out
3197}
3198
3199/// At most this many of the legacy pcurve's worst spans are seeded.
3200const WORST_SPANS: usize = 4;
3201/// Interior stations seeded in each of them.
3202const WORST_SPAN_SEEDS: usize = 7;
3203
3204/// The seed fractions of [`end_span_challenger`]'s worst-span source: the
3205/// legacy pcurve's own spans (between its distinct knots) whose same-parameter
3206/// deviation, read at the quarter points and 16 interior points, and at any of
3207/// `extra_probes` (the edge's own end-span interior points mapped to coedge
3208/// fractions) that fall inside the span, exceeds
3209/// `bar`, worst first, at most [`WORST_SPANS`] of them, each seeded with
3210/// [`WORST_SPAN_SEEDS`] evenly spaced interior fractions. Fails closed on any
3211/// non-finite reading.
3212fn legacy_worst_span_seeds(
3213 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3214 surface: &NurbsSurface,
3215 pcurve: &NurbsCurve,
3216 bar: f64,
3217 extra_probes: &[f64],
3218) -> Result<Vec<f64>, String> {
3219 let [q0, q1] = pcurve.domain()?;
3220 let mut knots: Vec<f64> = pcurve.knots.iter().copied().filter(|k| *k >= q0 && *k <= q1).collect();
3221 knots.dedup();
3222 let locals: Vec<f64> = [0.25, 0.5, 0.75]
3223 .into_iter()
3224 .chain((1..=TRACKING_ACCEPTANCE_POINTS).map(|index| index as f64 / (TRACKING_ACCEPTANCE_POINTS + 1) as f64))
3225 .collect();
3226 let mut over: Vec<(f64, f64, f64)> = Vec::new();
3227 for span in knots.windows(2).filter(|w| w[1] > w[0]) {
3228 let mut worst = 0.0_f64;
3229 let inside = extra_probes.iter().copied().filter(|f| *f > span[0] && *f < span[1]);
3230 for fraction in locals.iter().map(|local| span[0] + (span[1] - span[0]) * local).chain(inside) {
3231 let uv = pcurve.evaluate(fraction)?;
3232 let deviation = surface.evaluate_extended(uv.x, uv.y)?.sub(evaluate_edge(fraction)?).length();
3233 if !deviation.is_finite() {
3234 return Err(format!("worst-span read: non-finite deviation at fraction {fraction}"));
3235 }
3236 worst = worst.max(deviation);
3237 }
3238 if worst > bar {
3239 over.push((worst, span[0], span[1]));
3240 }
3241 }
3242 over.sort_by(|a, b| b.0.total_cmp(&a.0).then(a.1.total_cmp(&b.1)));
3243 over.truncate(WORST_SPANS);
3244 Ok(over
3245 .into_iter()
3246 .flat_map(|(_, lo, hi)| (1..=WORST_SPAN_SEEDS).map(move |index| lo + (hi - lo) * index as f64 / (WORST_SPAN_SEEDS + 1) as f64))
3247 .collect())
3248}
3249
3250/// Why the range lane's [`end_span_challenger`] runs.
3251#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3252enum ChallengerTrigger {
3253 /// The legacy residual misses `ask + standoff` (the original trigger).
3254 Deviation,
3255 /// The legacy residual is within `ask + standoff`, the edge stands off its
3256 /// carrier by more than the ask, and the legacy fit's COMMON tracking
3257 /// (`common_station_read` at the fixed probes) misses the ask.
3258 Tracking,
3259}
3260
3261/// The challenger's trigger. The original rule reads the same-parameter
3262/// DEVIATION against `ask + standoff`; over a standoff `g` a tangential slip
3263/// `d` adds only about d²/(2g) to it, so a slip up to sqrt(ask² + 2·ask·g)
3264/// never triggers (about 1.3e-5 at g = 9e-4, the fixture-25 inherited edges).
3265/// When the deviation rule does not fire and the edge stands off its carrier
3266/// by more than the ask (the same gate `refine_pcurve` takes its
3267/// standoff-tracking lane on), the legacy fit's common tracking is read
3268/// (`tracking`, called only then): over the ask, the challenger runs on
3269/// tracking. An unread tracking (None or non-finite) never triggers.
3270fn challenger_trigger(residual: f64, off_surface: f64, ask: f64, tracking: impl FnOnce() -> Option<f64>) -> Option<ChallengerTrigger> {
3271 if residual > ask + off_surface {
3272 return Some(ChallengerTrigger::Deviation);
3273 }
3274 if !(off_surface > ask) {
3275 return None;
3276 }
3277 match tracking() {
3278 Some(value) if value.is_finite() && value > ask => Some(ChallengerTrigger::Tracking),
3279 _ => None,
3280 }
3281}
3282
3283/// The END-SPAN CHALLENGER of the range lane (2026-10-05, the four original
3284/// fitter rows of `abc_00000011/12` edges 7 and 243). On e90c30c94 their worst
3285/// excess (4.58e-6 and 5.14e-6) sat in the installed pcurve's LAST span, at the
3286/// edge's start, where the edge curve's own first knot span is only 4.5e-6
3287/// (e7) and 1.2e-4 (e243) wide while the fitter's end span was 1.4e-4 wide at
3288/// its cap. It runs when [`challenger_trigger`] fires: the legacy fit misses
3289/// its bar (residual over the ask plus its standoff), or, on an edge standing
3290/// off its carrier by more than the ask, its common tracking misses the ask.
3291/// It refits with the edge's own interior knots that
3292/// fall inside the legacy pcurve's first or last span as additional stations
3293/// (not joins; corners stay the legacy ones), through the same fitter with the
3294/// unchanged ask, SPAN_FLOOR, cap and rounds. The candidate replaces the legacy
3295/// fit only when both, read at COMMON probes (both pcurves' own knot spans,
3296/// knots and endpoints, plus the edge's own knots and knot spans mapped to
3297/// fractions) against one SHARED foot per probe ([`common_station_read`]),
3298/// read finite, the candidate's tracking is strictly lower and its
3299/// same-parameter deviation no worse. Otherwise, or on any error or unreadable read, the legacy fit
3300/// stands exactly.
3301#[allow(clippy::too_many_arguments)]
3302fn end_span_challenger(
3303 surface: &NurbsSurface,
3304 curve: &NurbsCurve,
3305 evaluate_edge: &dyn Fn(f64) -> Result<Vec3, String>,
3306 edge_start: f64,
3307 edge_end: f64,
3308 forward: bool,
3309 breaks: &[f64],
3310 ask: f64,
3311 base_samples: usize,
3312 refinement_rounds: usize,
3313 parameter_cap: usize,
3314 legacy: PcurveFit,
3315) -> Result<PcurveFit, String> {
3316 // On the bar by deviation: nothing is read (the original zero-work
3317 // path). The TRACKING trigger (538dd700c: an off-carrier edge on its
3318 // deviation bar challenged on its common tracking) is withdrawn from this
3319 // merge (Claude master's ruling, 2026-10-05): bisected as the first commit
3320 // that moved step_import's a_trim_off_its_bar_is_accepted_and_named, and
3321 // the four original fitter rows do not need it (they enter on the
3322 // deviation trigger).
3323 if !(legacy.report.residual > ask + legacy.report.off_surface) {
3324 return Ok(legacy);
3325 }
3326 // The trigger read, the candidate's tracking verification and the final
3327 // read project the same edge points again: memoized, same answers.
3328 let _feet = FootMemoScope::open(surface);
3329 let Ok(end_knots) = native_end_knot_fractions(curve, edge_start, edge_end, forward, &legacy.curve) else { return Ok(legacy) };
3330 // The legacy fit's own WORST native spans (2026-10-05, root's boundary
3331 // audit of the four rows): the e90 witnesses read a 4.58e-6 / 5.14e-6
3332 // pointwise excess at q 0.99996 inside the closed native domain, in the
3333 // legacy pcurve's last span, where no edge knot falls. Each span between the
3334 // legacy pcurve's own distinct knots is read at its 19 probes; the spans
3335 // whose same-parameter deviation exceeds the ask plus the legacy standoff,
3336 // worst first and at most WORST_SPANS of them, each get WORST_SPAN_SEEDS
3337 // evenly spaced interior stations. Unreadable reads decline.
3338 // The edge's OWN first and last native knot spans, read at their quarter
3339 // points and 16 interior points, mapped through the trim's range and sense
3340 // to coedge fractions (e7's first edge span [0, 4.496e-6] of a
3341 // 0.018661-long trim maps reversed to q in [0.99976, 1], overlapping the
3342 // legacy pcurve's last span [0.99986, 1]).
3343 let edge_end_probes = edge_end_span_fractions(curve, edge_start, edge_end, forward);
3344 // The FIXED part of the common probe set, known before the candidate is
3345 // fitted: the legacy pcurve's own spans, endpoints and knots, and the
3346 // edge's own end-span probes, knot-span quarters and knots (every probe
3347 // that could have TRIGGERED the challenger among them). The candidate fit
3348 // verifies against these as well as its own spans, so it stops on the
3349 // reader that judges it.
3350 let Ok(mut fixed) = native_probe_fractions(&[&legacy.curve]) else { return Ok(legacy) };
3351 fixed.extend(edge_end_probes.iter().copied());
3352 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
3353 let mut edge_knots: Vec<f64> = curve.knots.iter().copied().filter(|k| *k >= low && *k <= high).collect();
3354 edge_knots.push(low);
3355 edge_knots.push(high);
3356 edge_knots.sort_by(f64::total_cmp);
3357 edge_knots.dedup();
3358 let to_fraction = |t: f64| {
3359 let along = (t - edge_start) / (edge_end - edge_start);
3360 if forward { along } else { 1.0 - along }
3361 };
3362 for span in edge_knots.windows(2).filter(|w| w[1] > w[0]) {
3363 for local in [0.25, 0.5, 0.75] {
3364 fixed.push(to_fraction(span[0] + (span[1] - span[0]) * local));
3365 }
3366 }
3367 fixed.extend(edge_knots.iter().map(|&t| to_fraction(t)));
3368 fixed.extend(end_knots.iter().copied());
3369 fixed.extend(legacy.curve.knots.iter().copied());
3370 fixed.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
3371 fixed.sort_by(f64::total_cmp);
3372 fixed.dedup();
3373 let trigger = challenger_trigger(legacy.report.residual, legacy.report.off_surface, ask, || {
3374 common_station_read(surface, evaluate_edge, &legacy.curve, &legacy.curve, &fixed).ok().map(|((tracking, _), _)| tracking)
3375 });
3376 let Some(trigger) = trigger else { return Ok(legacy) };
3377 let Ok(worst_seeds) = legacy_worst_span_seeds(evaluate_edge, surface, &legacy.curve, ask + legacy.report.off_surface, &edge_end_probes) else { return Ok(legacy) };
3378 // The original guard holds for the deviation trigger only: a tracking
3379 // trigger needs no added station (the tracking mode and its verification
3380 // read the fixed probes).
3381 if trigger == ChallengerTrigger::Deviation && end_knots.is_empty() && worst_seeds.is_empty() {
3382 return Ok(legacy);
3383 }
3384 let mut stations: Vec<f64> = breaks.iter().copied().chain(end_knots.iter().copied()).chain(worst_seeds.iter().copied()).collect();
3385 stations.sort_by(f64::total_cmp);
3386 stations.dedup();
3387 // The fitter's cap is max(requested cap, initial station count): the added
3388 // stations must not raise it. Decline when the merged initial count
3389 // (base stations plus every break and end knot) could exceed the
3390 // requested cap.
3391 let initial = base_samples.max(1).checked_add(1).and_then(|count| count.checked_add(stations.len()));
3392 if initial.is_none_or(|count| count > parameter_cap) {
3393 return Ok(legacy);
3394 }
3395 let Ok(candidate) = fit_station_pcurve_mode(surface, evaluate_edge, &stations, breaks, ask, base_samples, refinement_rounds, parameter_cap, SPAN_FLOOR, true, Some(&legacy.curve), &fixed) else {
3396 return Ok(legacy);
3397 };
3398 if !(candidate.report.residual.is_finite() && candidate.report.off_surface.is_finite() && candidate.report.clamped_excursion.is_finite() && candidate.report.clamped_distance.is_finite()) {
3399 return Ok(legacy);
3400 }
3401 // Common probes: the fixed set plus the candidate's own knot spans,
3402 // endpoints and knots (the same set as both curves' native probes plus
3403 // the edge's, as before).
3404 let Ok(mut probes) = native_probe_fractions(&[&candidate.curve]) else { return Ok(legacy) };
3405 probes.extend(fixed.iter().copied());
3406 probes.extend(candidate.curve.knots.iter().copied());
3407 probes.retain(|f| f.is_finite() && (0.0..=1.0).contains(f));
3408 probes.sort_by(f64::total_cmp);
3409 probes.dedup();
3410 let readings = common_station_read_with_excess(surface, evaluate_edge, &legacy.curve, &candidate.curve, &probes);
3411 let accepted = matches!(&readings, Ok((old, new)) if floor_admits(*old, *new));
3412 if station_fit_tracing() {
3413 eprintln!(
3414 "STATION-FIT end-span challenger: trigger={trigger:?} end_knots={} worst_span_seeds={} edge_end_probes={} legacy={:?}/{:e} candidate={:?}/{:e} common={readings:?} decision={}",
3415 end_knots.len(), worst_seeds.len(), edge_end_probes.len(), legacy.report.exit, legacy.report.residual, candidate.report.exit, candidate.report.residual,
3416 if accepted { "candidate" } else { "legacy" }
3417 );
3418 }
3419 Ok(if accepted { candidate } else { legacy })
3420}
3421
3422
3423/// [`fit_pcurve_on_surface_range_dense`] for the callers that only need the
3424/// curve, accepted or refused on its bar ([`accept_fit`]).
3425#[allow(clippy::too_many_arguments)]
3426pub fn build_pcurve_on_surface_range_dense(
3427 surface: &NurbsSurface,
3428 curve: &NurbsCurve,
3429 edge_start: f64,
3430 edge_end: f64,
3431 forward: bool,
3432 tolerance: f64,
3433 base_samples: usize,
3434 refinement_rounds: usize,
3435 parameter_cap: usize,
3436) -> Result<NurbsCurve, String> {
3437 accept_fit(fit_pcurve_on_surface_range_dense(
3438 surface,
3439 curve,
3440 edge_start,
3441 edge_end,
3442 forward,
3443 tolerance,
3444 base_samples,
3445 refinement_rounds,
3446 parameter_cap,
3447 )?)
3448}
3449
3450/// The fractions of `[edge_start, edge_end]`, walked in the coedge's sense, at
3451/// which `curve` is not C2: interior knots of multiplicity at least its
3452/// degree. A rational-quadratic arc past a quarter turn is two segments joined
3453/// at a double knot, and an interpolant through uniform samples across that
3454/// joint cannot represent the second-derivative jump: on the 2026-09-18 import
3455/// census 1511 of the 2979 arc trims blamed on the fitter had their worst
3456/// miss at fraction 0.49–0.51, the mid double knot, exactly as the fillet's
3457/// corner patch had measured (`blend/track_fit.rs::curve_breaks`, which this
3458/// mirrors for a range).
3459pub(crate) fn range_breaks(
3460 curve: &NurbsCurve,
3461 edge_start: f64,
3462 edge_end: f64,
3463 forward: bool,
3464) -> Vec<f64> {
3465 let mut fractions = Vec::new();
3466 let span = edge_end - edge_start;
3467 if !(span.abs() > 0.0) {
3468 return fractions;
3469 }
3470 let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
3471 let mut index = 0;
3472 while index < curve.knots.len() {
3473 let value = curve.knots[index];
3474 let run = curve.knots[index..]
3475 .iter()
3476 .take_while(|knot| **knot == value)
3477 .count();
3478 if value > low && value < high && run >= curve.degree.max(1) {
3479 let along = (value - edge_start) / span;
3480 fractions.push(if forward { along } else { 1.0 - along });
3481 }
3482 index += run;
3483 }
3484 fractions.sort_by(f64::total_cmp);
3485 fractions
3486}
3487
3488/// The body of [`fit_pcurve_on_surface_range_dense`], parameterized by the
3489/// 3D station the pcurve must pass through at each coedge fraction instead of
3490/// by an edge curve and a range.
3491///
3492/// The station supplier is the whole difference between deriving a pcurve and
3493/// reading one. `fit_pcurve_on_surface_range_dense` supplies points off the
3494/// edge's own 3D curve, so the answer is "the nearest point of the carrier",
3495/// which is ambiguous exactly where a trim is interesting — a seam branch, a
3496/// near-tangential approach, a pole. The STEP importer's supplied-pcurve lane
3497/// supplies points off the VENDOR's stated trim instead, and the ambiguity is
3498/// gone because the vendor resolved it.
3499///
3500/// `stations(fraction)` must be continuous in `fraction` over `[0, 1]` and land
3501/// on (or very near) `surface`; `breaks` are the fractions at which it is not
3502/// C2 ([`range_breaks`]), each of which becomes a station so no kink sits
3503/// between two nodes. Everything else — inversion, branch repair, refinement
3504/// and the measurement — is shared with the derived lane by construction.
3505///
3506/// The SURFACE's knot-line crossings are deliberately not stations here,
3507/// although `blend/track_fit.rs` makes them so. Measured 2026-09-26 over the
3508/// 57-document import corpus: with each crossing bisected on the fraction and
3509/// projected seeded from the left station, 34 bodies refused (19 with a foot
3510/// that misses its global distance rejected) against 15 without the pass —
3511/// every extra refusal a fit stalled around a crossing station, on
3512/// `AssemblyExample-Assembly` and `abc_00000014` among others. The raised
3513/// pass resolves the O(h²) tail across a crossing on its own.
3514///
3515/// # What the fit is measured against, and when it stops
3516///
3517/// The stations' own standoff from the carrier is read FIRST, from the base
3518/// inversions, and the refinement target is the ask plus that standoff
3519/// ([`PcurveFitReport::on_bar`]): a station standing `g` off the surface
3520/// cannot be imaged closer than `g`, so chasing it to the ask alone spends the
3521/// whole budget for nothing (the 2026-09-18 census: 2052 imported pcurves at
3522/// the 513 cap, 1958 of them on a curve that is not on its carrier).
3523///
3524/// Every probe and the final measurement read the carrier through
3525/// `evaluate_extended`: a pcurve riding a periodic seam legitimately carries
3526/// parameters past the domain, and the clamped `evaluate` this loop used until
3527/// 2026-09-26 read every such station as a gross miss — an insert per round
3528/// until the cap, on a trim that was right.
3529///
3530/// The RETURNED curve is measured out of sample — at the quarter points of
3531/// every span between its nodes, where an interpolant is exact at the nodes
3532/// and extremal between — unless its last round probed every span and found
3533/// nothing to insert, in which case that round's maximum already is that
3534/// measurement. A fit that binds at `parameter_cap` or spends its rounds with
3535/// the bar still unmet grows ONCE to [`PCURVE_RAISED_SAMPLES`]
3536/// (deterministic). The report says which exit it took and what it reached;
3537/// the `build_*` wrappers refuse over the bar, this function never does.
3538#[allow(clippy::too_many_arguments)]
3539pub fn fit_pcurve_on_surface_stations(
3540 surface: &NurbsSurface,
3541 stations: &dyn Fn(f64) -> Result<Vec3, String>,
3542 breaks: &[f64],
3543 tolerance: f64,
3544 base_samples: usize,
3545 refinement_rounds: usize,
3546 parameter_cap: usize,
3547) -> Result<PcurveFit, String> {
3548 fit_station_pcurve(
3549 surface, stations, breaks, &[], tolerance.max(PCURVE_REFINEMENT_TOLERANCE),
3550 base_samples, refinement_rounds, parameter_cap, SPAN_FLOOR,
3551 )
3552}
3553
3554/// Precision construction for stations on recognized analytic carriers. Unlike
3555/// general projection, their inverse maps reach floating point accuracy. This
3556/// is an explicit construction contract, not a change to general fit policy.
3557pub(crate) fn analytic_station_pcurve(
3558 surface: &NurbsSurface,
3559 stations: &dyn Fn(f64) -> Result<Vec3, String>,
3560 tolerance: f64,
3561) -> Result<NurbsCurve, String> {
3562 if surface.analytic().is_none() {
3563 return Err("analytic pcurve requires an analytic carrier".into());
3564 }
3565 let mut breaks = Vec::new();
3566 for _ in 0..4 {
3567 let fit = fit_station_pcurve(surface, stations, &breaks, &[], tolerance, 64, 64, 8192, 1e-10)?;
3568 if fit.report.residual > tolerance || fit.report.off_surface > tolerance {
3569 return Err(format!(
3570 "analytic pcurve construction missed its contract: {:?}",
3571 fit.report
3572 ));
3573 }
3574 let mut failed = Vec::new();
3575 // Independent Gauss stations in every returned knot span, rather than
3576 // re-reading the interpolation nodes or the fitter's quarter stations.
3577 for pair in fit.curve.knots.windows(2) {
3578 if pair[1] <= pair[0] {
3579 continue;
3580 }
3581 for local in [
3582 0.06943184420297371,
3583 0.33000947820757187,
3584 0.6699905217924281,
3585 0.9305681557970262,
3586 ] {
3587 let t = pair[0] + (pair[1] - pair[0]) * local;
3588 let uv = fit.curve.evaluate(t)?;
3589 if surface
3590 .evaluate_extended(uv.x, uv.y)?
3591 .sub(stations(t)?)
3592 .length()
3593 > tolerance
3594 {
3595 failed.push(t);
3596 }
3597 }
3598 }
3599 if failed.is_empty() {
3600 return Ok(fit.curve);
3601 }
3602 if breaks.len() + failed.len() > 8192 {
3603 break;
3604 }
3605 breaks.extend(failed);
3606 breaks.sort_by(f64::total_cmp);
3607 breaks.dedup();
3608 }
3609 Err("analytic pcurve failed independent knot-span witnesses".into())
3610}
3611
3612#[allow(clippy::too_many_arguments)]
3613fn fit_station_pcurve(
3614 surface: &NurbsSurface,
3615 stations: &dyn Fn(f64) -> Result<Vec3, String>,
3616 breaks: &[f64],
3617 corners: &[f64],
3618 tolerance: f64,
3619 base_samples: usize,
3620 refinement_rounds: usize,
3621 parameter_cap: usize,
3622 span_floor: f64,
3623) -> Result<PcurveFit, String> {
3624 fit_station_pcurve_mode(surface, stations, breaks, corners, tolerance, base_samples, refinement_rounds, parameter_cap, span_floor, false, None, &[])
3625}
3626
3627/// [`fit_station_pcurve`] with an optional TRACKING mode, taken only by the
3628/// range lane's [`end_span_challenger`]; every other caller keeps the legacy
3629/// mode byte for byte. The legacy fitter probes, ranks and stops on the
3630/// image's same-parameter DEVIATION from the edge point. On an edge standing
3631/// off its carrier that deviation never falls under the ask, so every probe is
3632/// a candidate, the cap goes to the probes with the largest STANDOFF, and a
3633/// tangential slip of `d` adds only about d²/(2g) to a deviation over a
3634/// standoff `g`: the four original rows' challenger met its standoff (4.670e-6)
3635/// and kept a common tracking of 1.434e-6 (root's e24 run, 2026-10-05). In
3636/// tracking mode each probe reads its TRACKING, the image against the edge
3637/// point's foot (the nearer admitted one of the global projection and the
3638/// projection seeded at the image's uv: finite, inside the native domain, as
3639/// [`common_station_read`] admits). A probe is a candidate only over the ask,
3640/// the cap goes to the worst TRACKING first, and the one raise is taken when
3641/// the last round's tracking still misses the ask. The reported residual
3642/// stays the same-parameter deviation (standoff included). A probe with no
3643/// admitted foot is an error, which the challenger turns into the exact legacy
3644/// fit.
3645#[allow(clippy::too_many_arguments)]
3646fn fit_station_pcurve_mode(
3647 surface: &NurbsSurface,
3648 stations: &dyn Fn(f64) -> Result<Vec3, String>,
3649 breaks: &[f64],
3650 corners: &[f64],
3651 tolerance: f64,
3652 base_samples: usize,
3653 refinement_rounds: usize,
3654 parameter_cap: usize,
3655 span_floor: f64,
3656 track_projection: bool,
3657 reference: Option<&NurbsCurve>,
3658 verify: &[f64],
3659) -> Result<PcurveFit, String> {
3660 // Read only in tracking mode: the legacy mode makes no extra call.
3661 let domains = if track_projection { Some(surface_domains(surface)?) } else { None };
3662 // The tracking reading at one fraction, by the SAME foot rule as
3663 // [`common_station_read`], the reader the challenger accepts on: the
3664 // nearest admitted foot among the global projection and the projections
3665 // seeded at this image's uv and at the reference (legacy) pcurve's uv at
3666 // the same fraction. Returns the tracking and that foot (uv, distance),
3667 // which is where a tracking insert is placed.
3668 let tracking_of = |represented: Vec3, edge_point: Vec3, uv: Vec3, fraction: f64| -> Result<(f64, [f64; 2], f64), String> {
3669 let (domain_u, domain_v) = domains.ok_or_else(|| "station tracking: legacy mode".to_string())?;
3670 let finite = |p: Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
3671 let reference_seed = match reference {
3672 Some(curve) => curve.evaluate(fraction).ok().and_then(|seed| memo_seeded_foot(surface, edge_point, seed.x, seed.y)),
3673 None => None,
3674 };
3675 // The common reader's order (global, reference, candidate), so an
3676 // equal-distance tie takes the same foot here as there.
3677 let foot = common_foot(
3678 memo_global_foot(surface, edge_point),
3679 reference_seed,
3680 memo_seeded_foot(surface, edge_point, uv.x, uv.y),
3681 domain_u,
3682 domain_v,
3683 )
3684 .ok_or_else(|| "station tracking: no admitted foot".to_string())?;
3685 let tracking = represented.sub(foot.point).length();
3686 if !(finite(represented) && tracking.is_finite()) {
3687 return Err("station tracking: non-finite reading".into());
3688 }
3689 Ok((tracking, [foot.u, foot.v], foot.distance))
3690 };
3691 // The last round's tracking missed the ask (tracking mode only).
3692 let mut tracking_unmet = false;
3693 let evaluate_edge = stations;
3694 let base_samples = base_samples.max(1);
3695 // The general caller floors its ask: its projector places a foot
3696 // to about `LINEAR_TOLERANCE` (1e-7), so a caller asking for 1e-9 asks for
3697 // more than any station can deliver. Chased to such an ask the fit spends
3698 // its cap and reads its own noise — the direct-edit heals asked 1e-9 on
3699 // stations 9.9e-8 off their carrier and were refused by 8.5e-9
3700 // (2026-09-26). The floor is the ask's floor; the report says what was
3701 // reached against it.
3702
3703 let mut parameters = (0..=base_samples)
3704 .map(|index| index as f64 / base_samples as f64)
3705 .collect::<Vec<_>>();
3706 // A break within the span floor of a station IS that station: two nodes
3707 // closer than that make the interpolant swing between them, and over a
3708 // stretch that short the kink moves the fit by nothing a bar can read.
3709 // `abc_00000011` face #549's curve carries a triple knot 4e-7 before its
3710 // end; as a station it put the cubic 1.594e-2 off between it and the last
3711 // one (measured 2026-09-26), where the file's curve sits 1e-7 off.
3712 // A break that is a CORNER of the curve (`corners`, a subset of
3713 // `breaks`: the range lane passes its curve's knots of multiplicity >=
3714 // degree as both) is a C0 JOIN of the interpolant
3715 // (`build_interpolant_joined`): one cubic through the corner overshoots
3716 // on both sides whatever is inserted. A break that is only a station
3717 // (an analytic witness, a caller's own marker) joins nothing.
3718 let mut joins = Vec::with_capacity(corners.len());
3719 for &fraction in breaks {
3720 if !(fraction > 0.0 && fraction < 1.0) {
3721 continue;
3722 }
3723 let corner = corners.contains(&fraction);
3724 let at = parameters.partition_point(|parameter| *parameter < fraction);
3725 let near_left = at > 0 && fraction - parameters[at - 1] <= span_floor;
3726 let near_right = at < parameters.len() && parameters[at] - fraction <= span_floor;
3727 if near_left || near_right {
3728 // The break IS that station (a corner at a base station, e.g.
3729 // the midpoint of a two-piece curve at 32/64): no insert, but the
3730 // station is still the join.
3731 if corner {
3732 joins.push(if near_right { parameters[at] } else { parameters[at - 1] });
3733 }
3734 continue;
3735 }
3736 parameters.insert(at, fraction);
3737 if corner {
3738 joins.push(fraction);
3739 }
3740 }
3741 // The 3D edge point behind each raw inversion sample, kept parallel so the
3742 // continuity repair can re-seed a jumped station from its own footpoint.
3743 let mut edge_points = parameters
3744 .iter()
3745 .map(|fraction| evaluate_edge(*fraction))
3746 .collect::<Result<Vec<_>, _>>()?;
3747 // The stations' standoff from the carrier: the part of every residual no
3748 // pcurve can remove, known before a single probe is spent.
3749 let mut off_surface = 0.0_f64;
3750 let mut raw = Vec::with_capacity(edge_points.len());
3751 let mut gaps = Vec::with_capacity(edge_points.len());
3752 for point in &edge_points {
3753 let (parameter, gap) = invert_checked(surface, *point)?;
3754 off_surface = off_surface.max(gap);
3755 raw.push(parameter);
3756 gaps.push(gap);
3757 }
3758 repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
3759 let (mut pcurve, mut excursion, mut distance) =
3760 build_interpolant_joined(surface, &raw, ¶meters, &joins)?;
3761
3762 let mut cap = parameter_cap.max(parameters.len());
3763 let mut rounds = refinement_rounds;
3764 let mut raised = false;
3765 loop {
3766 let mut exit = PcurveFitExit::RoundBudget;
3767 let mut converged_max = None;
3768 // This pass's last round probed every span (none narrow, none
3769 // truncated) and every probe tracked within the ask (tracking mode).
3770 let mut tracking_met = false;
3771 for _ in 0..rounds {
3772 if parameters.len() >= cap {
3773 exit = PcurveFitExit::SampleCeiling;
3774 break;
3775 }
3776 // The target is the ASK, bare: a station standing off the carrier
3777 // is chased to the foot it projects to, as the fitter has always
3778 // done. A target of ask + standoff (one landing, 2026-09-26) let
3779 // such a fit stop at its base stations with the interpolant
3780 // wandering between them, folded a molex wall's mesh and put a
3781 // trim of abc_00000023 2.2e-5 off its own parameter; the bar
3782 // below still reads what it reached.
3783 let target = tolerance;
3784 let mut candidates = Vec::new();
3785 let mut round_max = 0.0_f64;
3786 let mut truncated = false;
3787 let mut skipped_narrow = false;
3788 let worst_first = worst_first();
3789 'spans: for index in 0..parameters.len() - 1 {
3790 // The loop's own resolution floor: a span narrower than this
3791 // is not subdivided. Without it the
3792 // raised pass inserted into a vanishing span at a trim's end
3793 // until the cap — `abc_00000011` face #549's last stations sat
3794 // 4e-7 apart with their feet clamped at the patch edge, and the
3795 // cubic through them swung 1.594e-2 off between them (measured
3796 // 2026-09-26). A miss that survives only inside such a span is
3797 // reported as [`PcurveFitExit::SpanFloor`].
3798 if parameters[index + 1] - parameters[index] < span_floor {
3799 skipped_narrow = true;
3800 continue;
3801 }
3802 for local in [0.25, 0.5, 0.75] {
3803 if !worst_first && parameters.len() + candidates.len() >= cap {
3804 truncated = true;
3805 break 'spans;
3806 }
3807 let fraction =
3808 parameters[index] + (parameters[index + 1] - parameters[index]) * local;
3809 let uv = pcurve.evaluate(fraction)?;
3810 let represented = surface.evaluate_extended(uv.x, uv.y)?;
3811 let edge_point = evaluate_edge(fraction)?;
3812 let deviation = represented.sub(edge_point).length();
3813 // The probe's reading: its deviation (legacy), or its
3814 // tracking against the admitted foot (tracking mode).
3815 let (reading, foot) = if track_projection {
3816 let (tracking, foot_uv, foot_gap) = tracking_of(represented, edge_point, uv, fraction)?;
3817 (tracking, Some((foot_uv, foot_gap)))
3818 } else {
3819 (deviation, None)
3820 };
3821 round_max = round_max.max(reading);
3822 if reading <= target {
3823 continue;
3824 }
3825 candidates.push((index, fraction, edge_point, uv, reading, foot));
3826 }
3827 }
3828 // Tracking mode's VERIFICATION, before it may stop: a round whose
3829 // quarter probes all track within the ask is read again at the
3830 // probes the challenger's acceptance reads, the current pcurve's
3831 // own knot spans (endpoints, quarters and 16 interior points: the
3832 // interpolant's knots are averages of three stations, not the
3833 // stations) and the caller's fixed fractions (the legacy pcurve's
3834 // spans and the edge's own). A probe over the ask becomes a
3835 // candidate at its own fraction, placed at its own foot. One
3836 // within the span floor of a station cannot be inserted: it is
3837 // counted, and the fit may not report its tracking as met. On e24
3838 // the four rows stopped on their quarter probes and read 1.144e-7 /
3839 // 1.590e-7 at the common probes (a4c84871 runtime, 2026-10-05).
3840 let mut verify_blocked = false;
3841 if track_projection && candidates.is_empty() && !truncated && !skipped_narrow {
3842 for fraction in verification_fractions(&pcurve, verify)? {
3843 let index = parameters.partition_point(|parameter| *parameter <= fraction).saturating_sub(1).min(parameters.len() - 2);
3844 let uv = pcurve.evaluate(fraction)?;
3845 let represented = surface.evaluate_extended(uv.x, uv.y)?;
3846 let edge_point = evaluate_edge(fraction)?;
3847 let (tracking, foot_uv, foot_gap) = tracking_of(represented, edge_point, uv, fraction)?;
3848 round_max = round_max.max(tracking);
3849 if tracking <= target {
3850 continue;
3851 }
3852 if fraction - parameters[index] <= span_floor || parameters[index + 1] - fraction <= span_floor {
3853 verify_blocked = true;
3854 continue;
3855 }
3856 // Two over-ask probes closer than the span floor in one
3857 // span: keep the worse, so no near-duplicate stations.
3858 if let Some(last) = candidates.last_mut() {
3859 if last.0 == index && fraction - last.1 <= span_floor {
3860 if tracking > last.4 {
3861 *last = (index, fraction, edge_point, uv, tracking, Some((foot_uv, foot_gap)));
3862 }
3863 continue;
3864 }
3865 }
3866 candidates.push((index, fraction, edge_point, uv, tracking, Some((foot_uv, foot_gap))));
3867 }
3868 }
3869 tracking_unmet = track_projection && round_max > target;
3870 // A round with more candidates than the cap has room for spends
3871 // that room on its WORST probes, kept in curve order, rather than
3872 // on the first spans in order: truncating in span order left the
3873 // late spans of an off-carrier ring at their base spacing and its
3874 // image 1.523e-6 off its feet; worst first reaches 5.84e-8 on the
3875 // same 513 samples. An even stride along the curve reached 2.87e-7
3876 // there but diluted the inserts a local miss needs and lost fits
3877 // over the corpus (a 1.3e-5 trim of `abc_00000011` fell back to its
3878 // 4.4e-4 size-ask fit), measured 2026-09-27.
3879 let room = cap.saturating_sub(parameters.len());
3880 if candidates.len() > room {
3881 truncated = true;
3882 let mut order: Vec<usize> = (0..candidates.len()).collect();
3883 order.sort_by(|&a, &b| candidates[b].4.total_cmp(&candidates[a].4).then(a.cmp(&b)));
3884 order.truncate(room);
3885 order.sort_unstable();
3886 candidates = order.into_iter().map(|index| candidates[index]).collect();
3887 }
3888 let mut inserts = Vec::with_capacity(candidates.len());
3889 for (index, fraction, edge_point, uv, _, foot) in candidates {
3890 // A tracking candidate is placed at the very foot it was read
3891 // against, so the node's image IS that foot; the legacy
3892 // inversion is unchanged.
3893 let (parameter, gap) = match foot {
3894 Some(placed) => placed,
3895 None => invert_near(surface, edge_point, [uv.x, uv.y], gaps[index].max(gaps[index + 1]))?,
3896 };
3897 inserts.push((index + 1, fraction, edge_point, parameter, gap));
3898 }
3899 if station_fit_tracing() {
3900 eprintln!(
3901 "STATION-FIT round: samples={} target={target:.3e} round_max={round_max:.3e} inserts={} truncated={truncated}{}",
3902 parameters.len(),
3903 inserts.len(),
3904 // Tracking mode's round_max is a TRACKING reading.
3905 if track_projection { " mode=tracking" } else { "" }
3906 );
3907 }
3908 if inserts.is_empty() {
3909 exit = if truncated {
3910 PcurveFitExit::SampleCeiling
3911 } else if skipped_narrow {
3912 // Every probed span is within the target; the narrow ones
3913 // were not probed, and the sweep below measures them.
3914 PcurveFitExit::SpanFloor
3915 } else if track_projection {
3916 // Every probe TRACKS within the ask, but round_max is a
3917 // tracking reading, not the deviation the report carries.
3918 // The exit is Stalled (no probe can be improved; the
3919 // residual measured below is the edge's own standoff),
3920 // and it becomes Converged below only if that measured
3921 // deviation itself meets the floor. A verification probe
3922 // over the ask that could not be inserted leaves the
3923 // tracking UNMET.
3924 tracking_met = !verify_blocked;
3925 PcurveFitExit::Stalled
3926 } else {
3927 converged_max = Some(round_max);
3928 PcurveFitExit::Converged
3929 };
3930 break;
3931 }
3932 for (index, fraction, edge_point, parameter, gap) in inserts.into_iter().rev() {
3933 parameters.insert(index, fraction);
3934 edge_points.insert(index, edge_point);
3935 raw.insert(index, parameter);
3936 gaps.insert(index, gap);
3937 off_surface = off_surface.max(gap);
3938 }
3939 // Inserts are independent global inversions too — re-run the repair
3940 // so a fold-flip introduced mid-refinement cannot poison the next
3941 // round's deviation interpolant.
3942 repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
3943 let rebuilt = build_interpolant_joined(surface, &raw, ¶meters, &joins)?;
3944 pcurve = rebuilt.0;
3945 excursion = rebuilt.1;
3946 distance = rebuilt.2;
3947 if truncated {
3948 exit = PcurveFitExit::SampleCeiling;
3949 break;
3950 }
3951 }
3952 let residual = match converged_max {
3953 Some(measured) => measured,
3954 None => {
3955 station_residual(surface, evaluate_edge, &pcurve, ¶meters, tolerance, &mut off_surface)?
3956 }
3957 };
3958 if station_fit_tracing() {
3959 eprintln!(
3960 "STATION-FIT pass raised={raised} exit={exit:?} samples={} residual={residual:.3e} off_surface={off_surface:.3e}",
3961 parameters.len()
3962 );
3963 if residual > tolerance + off_surface && (exit == PcurveFitExit::Converged || raised) {
3964 // Where the miss sits, and the uv track around it: the trace a
3965 // refusal needs to be attributed to a fold, a seam or the file.
3966 let (mut worst, mut at) = (0.0f64, 0usize);
3967 for (index, span) in parameters.windows(2).enumerate() {
3968 let fraction = 0.5 * (span[0] + span[1]);
3969 let uv = pcurve.evaluate(fraction)?;
3970 let deviation = surface.evaluate_extended(uv.x, uv.y)?.sub(evaluate_edge(fraction)?).length();
3971 if deviation > worst {
3972 worst = deviation;
3973 at = index;
3974 }
3975 }
3976 let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
3977 let (closed_u, closed_v) = surface_closedness(surface)?;
3978 eprintln!(
3979 "STATION-FIT OFF BAR: worst {worst:.3e} between stations {at} and {}; surface degree {}x{} net {}x{} domain u[{u0},{u1}] v[{v0},{v1}] closed ({closed_u},{closed_v}) analytic {:?}",
3980 at + 1,
3981 surface.degree_u,
3982 surface.degree_v,
3983 surface.control_points.len(),
3984 surface.control_points[0].len(),
3985 surface.analytic().map(|_| "yes").unwrap_or("no")
3986 );
3987 {
3988 let fraction = 0.5 * (parameters[at] + parameters[at + 1]);
3989 let uv = pcurve.evaluate(fraction)?;
3990 let image = surface.evaluate_extended(uv.x, uv.y)?;
3991 let station = evaluate_edge(fraction)?;
3992 let foot = project_point_to_surface(surface, station)?;
3993 let seeded = project_point_to_surface_seeded(surface, station, raw[at][0], raw[at][1])?;
3994 eprintln!(
3995 " at the worst midpoint: deviation {:.4e}, station gap {:.4e} (seeded {:.4e}), image-to-foot {:.4e} (to seeded foot {:.4e}), foot uv ({:.7}, {:.7}) seeded ({:.7}, {:.7}) image uv ({:.7}, {:.7})",
3996 image.sub(station).length(), foot.distance, seeded.distance, image.sub(foot.point).length(), image.sub(seeded.point).length(), foot.u, foot.v, seeded.u, seeded.v, uv.x, uv.y
3997 );
3998 }
3999 let lo = at.saturating_sub(3);
4000 let hi = (at + 4).min(parameters.len() - 1);
4001 for index in lo..=hi {
4002 eprintln!(
4003 " station {index}: fraction {:.6} uv ({:.6}, {:.6}) point ({:.4}, {:.4}, {:.4})",
4004 parameters[index], raw[index][0], raw[index][1], edge_points[index].x, edge_points[index].y, edge_points[index].z
4005 );
4006 }
4007 }
4008 }
4009 // Legacy only: a SpanFloor exit whose measured DEVIATION is on the
4010 // bar is promoted. In tracking mode the deviation sits at the standoff
4011 // whatever the unprobed narrow spans' TRACKING is, so SpanFloor stays
4012 // SpanFloor (unread); a tracking-mode fit is Converged only when every
4013 // span was probed within the ask AND the measured deviation meets the
4014 // floor (the exit's documented meaning, never the standoff bar).
4015 if !track_projection && exit == PcurveFitExit::SpanFloor && residual <= tolerance + off_surface {
4016 exit = PcurveFitExit::Converged;
4017 }
4018 if tracking_met && residual <= tolerance {
4019 exit = PcurveFitExit::Converged;
4020 }
4021 if exit != PcurveFitExit::Converged
4022 && !raised
4023 && (residual > tolerance + off_surface || tracking_unmet)
4024 && cap < PCURVE_RAISED_SAMPLES
4025 {
4026 raised = true;
4027 cap = PCURVE_RAISED_SAMPLES;
4028 // The raised pass is bounded by SAMPLES, not rounds: a kink
4029 // converges O(h²), a round per halving, and the three rounds of
4030 // the first pass are what left the 2026-09-18 census's fits over
4031 // their ask with budget to spare (RoundBudget at 158–400 of 2000
4032 // samples, measured 2026-09-26). Forty rounds cover every halving
4033 // the cap admits; a fit that is not converging stalls out first.
4034 rounds = RAISED_ROUNDS;
4035 continue;
4036 }
4037 return Ok(PcurveFit {
4038 curve: pcurve,
4039 report: PcurveFitReport {
4040 residual,
4041 floor: tolerance,
4042 samples: parameters.len(),
4043 exit,
4044 off_surface,
4045 raised,
4046 clamped_excursion: excursion,
4047 clamped_distance: distance,
4048 },
4049 });
4050 }
4051}
4052
4053/// [`fit_pcurve_on_surface_stations`] for the callers that only need the
4054/// curve, accepted or refused on its bar ([`accept_fit`]).
4055pub fn build_pcurve_on_surface_stations(
4056 surface: &NurbsSurface,
4057 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4058 breaks: &[f64],
4059 tolerance: f64,
4060 base_samples: usize,
4061 refinement_rounds: usize,
4062 parameter_cap: usize,
4063) -> Result<NurbsCurve, String> {
4064 accept_fit(fit_pcurve_on_surface_stations(
4065 surface,
4066 stations,
4067 breaks,
4068 tolerance,
4069 base_samples,
4070 refinement_rounds,
4071 parameter_cap,
4072 )?)
4073}
4074
4075/// The largest 3D distance between `pcurve`'s image and its stations at the
4076/// quarter points of every span of `parameters` — out of sample by
4077/// construction, since the interpolant is exact at its nodes. Unclamped
4078/// evaluations; a station read over the running bar is inverted so that
4079/// `off_surface` covers the standoff AT the stations the residual is taken at,
4080/// not only at the nodes (a curve lifted off its plane by a bump stands
4081/// further off between two nodes than at either).
4082fn station_residual(
4083 surface: &NurbsSurface,
4084 stations: &dyn Fn(f64) -> Result<Vec3, String>,
4085 pcurve: &NurbsCurve,
4086 parameters: &[f64],
4087 tolerance: f64,
4088 off_surface: &mut f64,
4089) -> Result<f64, String> {
4090 let mut worst = 0.0_f64;
4091 let mut read = |fraction: f64| -> Result<(), String> {
4092 let uv = pcurve.evaluate(fraction)?;
4093 let represented = surface.evaluate_extended(uv.x, uv.y)?;
4094 let station = stations(fraction)?;
4095 let deviation = represented.sub(station).length();
4096 worst = worst.max(deviation);
4097 if deviation > tolerance + *off_surface {
4098 *off_surface = off_surface.max(invert_checked(surface, station)?.1);
4099 }
4100 Ok(())
4101 };
4102 for span in parameters.windows(2) {
4103 for local in [0.25, 0.5, 0.75] {
4104 read(span[0] + (span[1] - span[0]) * local)?;
4105 }
4106 }
4107 Ok(worst)
4108}
4109