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