Skip to main content

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
71fn seeded_inserts() -> bool {
72    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
73    *ON.get_or_init(|| std::env::var_os("BREP_PCURVE_SEEDED_INSERTS").is_none_or(|value| value != "0"))
74}
75
76/// [`invert_checked`] for a station inserted between two whose feet are known:
77/// Newton from the pcurve's own `(u, v)` at that fraction, which already lies
78/// between the neighbours' feet on their branch, and the global search only
79/// when that local foot stands off further than the neighbours do (plus the
80/// refinement floor) — a local minimum the curve does not sit at. The global
81/// search was 30 s of a 54 s `abc_00000011` import (732k calls, 41 µs each,
82/// measured 2026-09-26). Planes and analytic carriers keep their closed forms.
83/// `BREP_PCURVE_SEEDED_INSERTS=0` restores the global search at every insert,
84/// for a same-binary A/B.
85fn invert_near(
86    surface: &NurbsSurface,
87    point: Vec3,
88    seed: [f64; 2],
89    neighbour_gap: f64,
90) -> Result<([f64; 2], f64), String> {
91    if !seeded_inserts() || surface.is_affine()? || surface.analytic().is_some() {
92        return invert_checked(surface, point);
93    }
94    let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
95    if seeded.distance <= neighbour_gap + PCURVE_REFINEMENT_TOLERANCE {
96        return Ok(([seeded.u, seeded.v], seeded.distance));
97    }
98    invert_checked(surface, point)
99}
100
101fn unwrap_periodic(values: &mut [f64], minimum: f64, maximum: f64) {
102    let period = maximum - minimum;
103    for index in 1..values.len() {
104        while values[index] - values[index - 1] > period / 2.0 {
105            values[index] -= period;
106        }
107        while values[index] - values[index - 1] < -period / 2.0 {
108            values[index] += period;
109        }
110    }
111    if values.len() > 1 {
112        while values[0] - values[1] > period / 2.0 {
113            values[0] -= period;
114        }
115        while values[0] - values[1] < -period / 2.0 {
116            values[0] += period;
117        }
118    }
119    // Recenter the whole (now continuous) sequence into the domain by the
120    // whole-period shift that leaves the LEAST parameter outside [minimum,
121    // maximum]. A single middle/mean sample is NOT representative of a curve
122    // that bulges out to touch — or straddle — the periodic seam: when that
123    // sample is a boundary-kissing point (the at-seam projection unwraps a hair
124    // past the boundary), a single-sample test misfires and shoves the entire
125    // curve a full period out of range (ABC helmet 00000011/12 side channels:
126    // a touching edge's parameter run snapped a whole period off its true
127    // interior, tearing the loop open in parameter space).
128    if values.len() > 1 {
129        let excursion = |shift: f64| -> f64 {
130            values
131                .iter()
132                .map(|value| {
133                    let v = value + shift;
134                    (minimum - v).max(0.0) + (v - maximum).max(0.0)
135                })
136                .sum::<f64>()
137        };
138        let mean = values.iter().copied().sum::<f64>() / values.len() as f64;
139        let center = 0.5 * (minimum + maximum);
140        let base_k = ((center - mean) / period).round() as i64;
141        let mut best_shift = 0.0;
142        let mut best_excursion = f64::INFINITY;
143        for k in (base_k - 1)..=(base_k + 1) {
144            let shift = k as f64 * period;
145            let value = excursion(shift);
146            if value < best_excursion {
147                best_excursion = value;
148                best_shift = shift;
149            }
150        }
151        if best_shift != 0.0 {
152            for value in values.iter_mut() {
153                *value += best_shift;
154            }
155        }
156    }
157}
158
159fn build_interpolant(
160    surface: &NurbsSurface,
161    raw_parameters: &[[f64; 2]],
162    curve_parameters: &[f64],
163) -> Result<NurbsCurve, String> {
164    let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
165    let (closed_u, closed_v) = surface_closedness(surface)?;
166    let mut parameters = raw_parameters.to_vec();
167    // A cone's apex has no longitude either: every u there is the same point,
168    // and the projector answers one arbitrarily (0, which unwraps to 1). A trim
169    // that ENDS at the apex then turns sideways in its last span —
170    // `abc 00008080` face #163 edge 476, a generatrix at u = 0.80255 whose
171    // apex station read u = 1.0 (2026-09-27). An apex station at either end of
172    // the run takes the u of its nearest regular station, so a generatrix's
173    // pcurve is the straight u = const it is. Interior apex crossings are left
174    // to the branch logic below. `BREP_PCURVE_CONE_APEX_U=0` switches this off,
175    // for a same-binary A/B.
176    if cone_apex_u() && matches!(surface.analytic(), Some(AnalyticSurface::RuledRevolution { .. })) && parameters.len() >= 2 {
177        let speeds = parameters
178            .iter()
179            .map(|parameter| surface.deriv1(parameter[0], parameter[1]).map(|(_, su, _)| su.length()))
180            .collect::<Result<Vec<_>, _>>()?;
181        let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
182        let singular = |index: usize| speeds[index] <= reference * 1e-2;
183        if let (Some(first), Some(last)) = (
184            (0..parameters.len()).find(|&index| !singular(index)),
185            (0..parameters.len()).rfind(|&index| !singular(index)),
186        ) {
187            for index in 0..first {
188                parameters[index][0] = parameters[first][0];
189            }
190            for index in last + 1..parameters.len() {
191                parameters[index][0] = parameters[last][0];
192            }
193        }
194    }
195    // Longitude is undefined at a sphere pole: the surface's u tangent
196    // vanishes there, and closest-point inversion is free to return any u.
197    // Letting that arbitrary value participate in periodic unwrapping can move
198    // the first real meridian by a whole period (e.g. 0.75 -> -0.25). Analytic
199    // carriers are subsequently clamped, collapsing that meridian onto u=0.
200    //
201    // Keep this deliberately sphere- and singularity-gated. Split the u values
202    // into maximal non-pole runs, unwrap each run independently, and leave the
203    // pole samples untouched. The existing interpolant degree stays unchanged;
204    // lowering it here would also change endpoint-tangent decisions made later
205    // while stitching the face loop.
206    let sphere_u_singular = if matches!(surface.analytic(), Some(AnalyticSurface::Sphere { .. })) {
207        let speeds = parameters
208            .iter()
209            .map(|parameter| {
210                surface
211                    .deriv1(parameter[0], parameter[1])
212                    .map(|(_, su, _)| su.length())
213            })
214            .collect::<Result<Vec<_>, _>>()?;
215        let reference = speeds.iter().copied().fold(0.0_f64, f64::max);
216        speeds
217            .into_iter()
218            // Uniform edge stations need not hit the pole exactly (ABC 5360
219            // bottoms out at v=5.5e-4). In this small conditioning band,
220            // longitude is already numerically arbitrary and must not connect
221            // the two meridian runs across the pole.
222            .map(|speed| speed <= reference * 1e-2)
223            .collect::<Vec<_>>()
224    } else {
225        vec![false; parameters.len()]
226    };
227    // Do not perturb ordinary pole-touching trims. The special path is needed
228    // only when a pole band is bracketed by nonsingular samples and its raw
229    // longitude changes by at least half a period: a true through-pole branch
230    // reset whose two meridian runs must be unwrapped independently.
231    // Prefix/suffix pole samples on normal sphere caps retain the legacy
232    // interpolation byte-for-byte. Include the exactly-half-period case so
233    // floating-point noise cannot decide which entire run is lifted (ABC 5603).
234    let has_interior_pole_crossing = sphere_u_singular
235        .iter()
236        .position(|singular| !singular)
237        .zip(sphere_u_singular.iter().rposition(|singular| !singular))
238        .is_some_and(|(first, last)| sphere_u_singular[first..=last].iter().any(|value| *value));
239    let period = (u1 - u0).abs();
240    let interior_pole_branch_reset = closed_u
241        && has_interior_pole_crossing
242        && parameters.windows(2).zip(sphere_u_singular.windows(2)).any(
243            |(parameter_pair, singular_pair)| {
244                (singular_pair[0] || singular_pair[1])
245                    && (parameter_pair[1][0] - parameter_pair[0][0]).abs()
246                        >= 0.5 * period - 1e-12 * period.max(1.0)
247            },
248        );
249    if closed_u {
250        let mut values = parameters.iter().map(|value| value[0]).collect::<Vec<_>>();
251        if interior_pole_branch_reset {
252            let mut start = 0;
253            while start < values.len() {
254                while start < values.len() && sphere_u_singular[start] {
255                    start += 1;
256                }
257                let mut end = start;
258                while end < values.len() && !sphere_u_singular[end] {
259                    end += 1;
260                }
261                unwrap_periodic(&mut values[start..end], u0, u1);
262                start = end;
263            }
264        } else {
265            unwrap_periodic(&mut values, u0, u1);
266        }
267        for (parameter, value) in parameters.iter_mut().zip(values) {
268            parameter[0] = value;
269        }
270    }
271    if closed_v {
272        let mut values = parameters.iter().map(|value| value[1]).collect::<Vec<_>>();
273        unwrap_periodic(&mut values, v0, v1);
274        for (parameter, value) in parameters.iter_mut().zip(values) {
275            parameter[1] = value;
276        }
277    }
278    // A closed direction WRAPS: an edge that straddles the seam has no single
279    // in-domain parameter run, so keep the unwrapped (possibly slightly
280    // out-of-domain) values and let the periodic evaluator wrap them — clamping
281    // them onto the seam boundary would collapse the straddling geometry (and,
282    // via the interpolant, spike neighbouring stations). This applies to
283    // analytic carriers too: a refitted cylinder can have stepped rims that
284    // cross its seam. evaluate_extended wraps every closed direction.
285    // Open directions must stay in-domain; their extension is a ruling.
286    let points = parameters
287        .into_iter()
288        .map(|parameter| {
289            let u = if closed_u {
290                parameter[0]
291            } else {
292                parameter[0].clamp(u0, u1)
293            };
294            let v = if closed_v {
295                parameter[1]
296            } else {
297                parameter[1].clamp(v0, v1)
298            };
299            Vec3::new(u, v, 0.0)
300        })
301        .collect::<Vec<_>>();
302    interpolate_curve(&points, 3usize.min(points.len() - 1), curve_parameters)
303}
304
305/// The 3D residual a pcurve's image must reach before `build_pcurve_on_surface`
306/// accepts it, and the sample ceiling it refines up to.
307///
308/// A trimmed face's boundary is only ever as accurate as these two numbers, and
309/// every mass property is an integral over that boundary — so this is the floor
310/// under area, volume and centroid for every curved trim in the kernel.
311///
312/// These were `min(1e-3, 1e-4 * scale)` and 160, where `scale` is
313/// `1 + ||curve midpoint||` — the DISTANCE FROM THE WORLD ORIGIN of the curve's
314/// midpoint, not the part's size. That expression pinned to an absolute 1e-3
315/// once the midpoint sat more than about nine units from the origin, so on an
316/// ordinary part the bar was 1e-3 and tracked nothing.
317///
318/// Measured on `offset-shell-box-bore-dished-face`, whose dished wall is a
319/// closed-form identity at 2468.431782123 (three independent routes agree). The
320/// floor is one of TWO errors there; the other is the refitted sphere carrier
321/// that `sphere_offset_surface` now builds in closed form, and before both moved
322/// they partly CANCELLED — which is why either alone reads as a regression:
323///
324/// ```text
325/// floor     Box_O.S2            abs err     relative   gate total  worst case
326/// 1e-3   2468.442270355      +1.049e-02     4.25e-06        (old)
327/// 1e-6   2468.431785429      +3.306e-06     1.34e-09       181.4 s     31.2 s
328/// 1e-7   2468.431782170      +4.746e-08     1.92e-11       218.5 s     32.1 s
329/// 1e-9   2468.431782103      -2.037e-08    -8.25e-12       305.5 s     46.8 s
330/// ```
331///
332/// 1e-7 is chosen: 70x the accuracy of 1e-6 for +20% gate time, where 1e-9 costs
333/// a further +40% for a 2.3x residual no oracle in this corpus could resolve.
334/// All four cells are one session on one binary, so the times are comparable to
335/// each other. Every case carrying a closed-form oracle moves TOWARD it and none
336/// away (`PushFaceTest` 339x, `PushFaceTest2` 275x); no case changes class at
337/// any floor.
338///
339/// A flat ABSOLUTE is what was measured, not what was designed. This codebase
340/// otherwise keys identity bands to part size (`KernelTolerances::heal_band`,
341/// the `1e-11*(1+scale)` blend family), and on a 3000 mm part 1e-7 is 3e-11
342/// relative — refinement will exhaust `MAX_PCURVE_SAMPLES` and return whatever
343/// it reached. Until 2026-09-12 it did so SILENTLY; now the fit's
344/// [`PcurveFitReport`] carries the residual actually achieved and a
345/// [`PcurveFitExit::SampleCeiling`] exit, and the owning operation's
346/// diagnostics count it (`pcurve.unmet_floor`). That is far better than the
347/// 1e-3 it replaces but it is not a size-aware bar; deriving one from the
348/// surface's own extent is separate, unmeasured work. Being absolute does remove this gate's dependence on
349/// distance from the origin as a side effect — a real translation-VARIANCE
350/// defect, the same anti-pattern `tolerance::merge_scale` records being removed
351/// elsewhere. `drop_tolerance` and `endpoint_tolerance` below still carry it.
352pub const PCURVE_REFINEMENT_TOLERANCE: f64 = 1e-7;
353
354/// Sample ceiling for a fit's FIRST refinement pass. 160 could not reach
355/// [`PCURVE_REFINEMENT_TOLERANCE`] on a curved trim of any length; 600 is the
356/// value every cell of the table above was measured at.
357///
358/// It BINDS. On `boolean_fuzz_corpus/25_cube_pierce_solid` the fitter stopped
359/// here 1.65e-3 of volume short of the converged answer (2000 and 4000 samples
360/// agree exactly). Raising it globally to 2000 was costed and rejected: +23%
361/// case-gate time, concentrated in three offset/fillet cases, and one case
362/// (`inbox-20260910-offset-shell-collapsed-fillet`) driven 26x outside its
363/// volume band. So it is not raised here: a fit that stops at this ceiling
364/// with a residual still worse than its edge curve's own distance from the
365/// surface re-enters refinement with [`PCURVE_RAISED_SAMPLES`]
366/// ([`refine_pcurve`]); one whose residual is already that gap stops here and
367/// says so ([`PcurveFitReport::off_surface`]), because no sample count can
368/// close it.
369const MAX_PCURVE_SAMPLES: usize = 600;
370
371/// Sample ceiling for the ONE raised pass a fit may take past
372/// [`MAX_PCURVE_SAMPLES`]. 2000 is the ceiling the convergence sweep measured
373/// on `25_cube_pierce_solid`: its 2000 and 4000 cells agree to every printed
374/// digit because refinement there stops itself near 1540 samples, so the cap
375/// is not what binds.
376const PCURVE_RAISED_SAMPLES: usize = 2000;
377
378/// What a pcurve fit ACHIEVED, returned beside the curve so a ceiling exit can
379/// never be mistaken for a met floor.
380///
381/// [`fit_pcurve_on_surface`] asks for [`PCURVE_REFINEMENT_TOLERANCE`] and stops
382/// at [`MAX_PCURVE_SAMPLES`] — or, where that ceiling bound and the floor is
383/// reachable, at [`PCURVE_RAISED_SAMPLES`]. Until 2026-09-12 a trim that ran
384/// out of samples came back exactly like one that met the floor; on
385/// `boolean_fuzz_corpus/25_cube_pierce_solid` that silence hid a 1.65e-3 volume
386/// error. Now every fit says which it was: `residual` is measured on the curve
387/// that is actually returned and `exit` names why refinement stopped.
388///
389/// This is a MEASUREMENT taken at a construction, not a band.
390#[derive(Clone, Copy, Debug, PartialEq)]
391pub struct PcurveFitReport {
392    /// Largest 3D distance between the returned pcurve's image on the surface
393    /// and the edge curve. On a [`PcurveFitExit::Converged`] exit whose final
394    /// round probed every span this is that round's maximum — the refinement's
395    /// own acceptance test. On every other exit, including a converged round
396    /// that skipped spans narrower than 1e-3 of the parameter range, it is a
397    /// sweep over EVERY span of the returned curve.
398    pub residual: f64,
399    /// The residual the refinement was asked to reach.
400    pub floor: f64,
401    /// Interpolation samples in the returned curve.
402    pub samples: usize,
403    /// Why refinement stopped.
404    pub exit: PcurveFitExit,
405    /// Largest distance from the edge curve to the surface at every station
406    /// the fit inverted — the part of `residual` no pcurve can remove, since a
407    /// pcurve's image lies ON the surface. Zero for an affine carrier. A value
408    /// above `floor` says the floor is unreachable for this input; a ceiling
409    /// exit is retried only while `residual` exceeds it by more than the floor,
410    /// i.e. while the returned curve is worse than its input.
411    pub off_surface: f64,
412    /// Whether the fit re-entered refinement with [`PCURVE_RAISED_SAMPLES`]
413    /// after [`MAX_PCURVE_SAMPLES`] bound with the floor unmet.
414    pub raised: bool,
415}
416
417impl PcurveFitReport {
418    /// Whether the returned curve reaches the floor it was asked for.
419    pub fn met_floor(&self) -> bool {
420        self.residual <= self.floor
421    }
422
423    /// Whether the returned curve is on the ACCEPTANCE BAR: the floor it was
424    /// asked for, plus the stations' own standoff from the carrier.
425    ///
426    /// A pcurve's image lies on the carrier, so at a station standing `g` off
427    /// it no image can come closer than `g`; a fit whose residual is within
428    /// the floor of `off_surface` is as good as the input allows, and one over
429    /// it is worse than its input — the same condition [`refine_pcurve`] uses
430    /// to decide that a raise can still buy accuracy. This is the bar every
431    /// station builder refuses over ([`build_pcurve_on_surface_stations`]).
432    /// It is blind to sideways error quadratically (a station tracked `d`
433    /// aside of the projection of a point `g` off the surface reads only
434    /// `d²/(2g)` further away), which is stated rather than hidden: on the
435    /// import corpus the stations that stand off their carriers are the
436    /// FILE's curves, and the bar there is the file's own number.
437    pub fn on_bar(&self) -> bool {
438        self.residual <= self.floor + self.off_surface
439    }
440}
441
442/// Why a pcurve refinement stopped. Only [`PcurveFitExit::Converged`] means the
443/// floor was met; every other variant is a BUDGET exit, and the report's
444/// residual was measured on the returned curve rather than assumed.
445#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
446pub enum PcurveFitExit {
447    /// A probe round found every station within the floor. An affine carrier
448    /// maps exactly and reports this with a zero residual.
449    Converged,
450    /// The sample ceiling bound before the floor was met: [`MAX_PCURVE_SAMPLES`]
451    /// when the raise was declined (the residual is already explained by
452    /// [`PcurveFitReport::off_surface`]), [`PCURVE_RAISED_SAMPLES`] when it was
453    /// taken and bound too.
454    SampleCeiling,
455    /// Every refinement round was spent and the last one still inserted
456    /// samples, so the curve it built was never re-checked by the loop.
457    RoundBudget,
458    /// A round found stations over the floor but could improve none of them:
459    /// the edge point could not be inverted inside the drop band, or (in the
460    /// raised pass) the pcurve already tracks the surface's nearest point to
461    /// within the floor. Either way the edge curve itself sits off the surface
462    /// there and no pcurve can close that gap; the residual reports how far.
463    Stalled,
464    /// The last probe round found every span it PROBED within the floor, but
465    /// skipped spans narrower than the loop's own resolution floor — 1e-3 of
466    /// the parameter range in [`refine_pcurve`], [`SPAN_FLOOR`] (1e-6) in
467    /// [`fit_pcurve_on_surface_stations`] — and a sweep over those found the
468    /// floor unmet.
469    /// Until 2026-09-12 this exit was reported as `Converged` with the probed
470    /// spans' maximum: on `25_cube_pierce_solid` a 1539-sample fit with 1537
471    /// narrow spans read 7e-8 that way while every span read 1.9e-4.
472    SpanFloor,
473}
474
475/// A fitted pcurve with what the fit achieved.
476#[derive(Clone, Debug)]
477pub struct PcurveFit {
478    pub curve: NurbsCurve,
479    pub report: PcurveFitReport,
480}
481
482/// Per-operation tally of pcurve fits and how many missed their floor.
483///
484/// [`build_pcurve_on_surface`] and its marched twin are called from some forty
485/// sites, most of which have no diagnostics record to write into. Rather than
486/// thread one through every layer, an operation that returns
487/// [`KernelDiagnostics`] opens a [`PcurveFitScope`]; every fit made on that
488/// thread while the scope is open is tallied here, and closing the scope hands
489/// the tally back for [`PcurveFitLedger::report_into`]. This is the shape the
490/// boolean's `CONFORMANCE_REPAIRS` counter already uses. With no scope open,
491/// recording is a no-op.
492///
493/// Scopes nest: CLOSING a child folds it into its parent, so an offset shell
494/// sees the fits of the booleans it ran. A scope that is DROPPED without being
495/// closed is discarded, not folded — a boolean attempt that refused (and may be
496/// retried under perturbation) must not leave its fits on the caller's tally.
497///
498/// Thread-local by construction: a fit made on a rayon worker (the `parallel`
499/// feature's STEP body import) is not seen by a scope on the calling thread. No
500/// diagnostics-returning operation builds pcurves off-thread today.
501#[derive(Clone, Copy, Debug, Default, PartialEq)]
502pub struct PcurveFitLedger {
503    /// Fits recorded, exact affine ones included.
504    pub fits: u64,
505    /// Fits whose returned curve did not reach its floor.
506    pub unmet: u64,
507    /// Unmet fits that stopped at a sample ceiling.
508    pub sample_ceiling: u64,
509    /// Unmet fits that spent every refinement round.
510    pub round_budget: u64,
511    /// Unmet fits that could insert nothing.
512    pub stalled: u64,
513    /// Unmet fits whose remaining over-floor spans were all below the loop's
514    /// 1e-3 span floor.
515    pub span_floor: u64,
516    /// Largest residual among the unmet fits; zero when there are none.
517    pub worst_unmet_residual: f64,
518    /// Fits that re-entered refinement with [`PCURVE_RAISED_SAMPLES`], met or
519    /// not.
520    pub raised: u64,
521    /// Unmet ceiling exits that were NOT raised because the residual is
522    /// already within the floor of the edge curve's own gap from the surface.
523    pub raise_declined: u64,
524    /// Largest [`PcurveFitReport::off_surface`] over every fit.
525    pub worst_off_surface: f64,
526}
527
528impl PcurveFitLedger {
529    fn record(&mut self, report: &PcurveFitReport) {
530        self.fits += 1;
531        if report.raised {
532            self.raised += 1;
533        }
534        self.worst_off_surface = self.worst_off_surface.max(report.off_surface);
535        if report.met_floor() {
536            return;
537        }
538        self.unmet += 1;
539        match report.exit {
540            PcurveFitExit::Converged => {}
541            PcurveFitExit::SampleCeiling => {
542                self.sample_ceiling += 1;
543                if !report.raised {
544                    self.raise_declined += 1;
545                }
546            }
547            PcurveFitExit::RoundBudget => self.round_budget += 1,
548            PcurveFitExit::Stalled => self.stalled += 1,
549            PcurveFitExit::SpanFloor => self.span_floor += 1,
550        }
551        self.worst_unmet_residual = self.worst_unmet_residual.max(report.residual);
552    }
553
554    fn fold(&mut self, child: &PcurveFitLedger) {
555        self.fits += child.fits;
556        self.unmet += child.unmet;
557        self.sample_ceiling += child.sample_ceiling;
558        self.round_budget += child.round_budget;
559        self.stalled += child.stalled;
560        self.span_floor += child.span_floor;
561        self.worst_unmet_residual = self.worst_unmet_residual.max(child.worst_unmet_residual);
562        self.raised += child.raised;
563        self.raise_declined += child.raise_declined;
564        self.worst_off_surface = self.worst_off_surface.max(child.worst_off_surface);
565    }
566
567    /// Write the tally into an operation's diagnostics: the `pcurve.*`
568    /// counters and measurements always, plus one `pcurve.unmet_floor` event at
569    /// [`DiagnosticSeverity::Degraded`] when any fit missed its floor. Degraded,
570    /// not Error: the result is still the kernel's best construction and stays
571    /// shippable, but it no longer claims the accuracy its floor states.
572    pub fn report_into(&self, diagnostics: &mut KernelDiagnostics) {
573        diagnostics.count_n("pcurve.fits", self.fits);
574        diagnostics.count_n("pcurve.unmet_floor", self.unmet);
575        diagnostics.count_n("pcurve.exit.sample_ceiling", self.sample_ceiling);
576        diagnostics.count_n("pcurve.exit.round_budget", self.round_budget);
577        diagnostics.count_n("pcurve.exit.stalled", self.stalled);
578        diagnostics.count_n("pcurve.exit.span_floor", self.span_floor);
579        diagnostics.count_n("pcurve.budget_raised", self.raised);
580        diagnostics.count_n("pcurve.budget_raise_declined", self.raise_declined);
581        diagnostics.measure_max("pcurve.worst_unmet_residual", self.worst_unmet_residual);
582        diagnostics.measure_max("pcurve.worst_off_surface", self.worst_off_surface);
583        if self.unmet > 0 {
584            diagnostics.event(
585                DiagnosticSeverity::Degraded,
586                KernelStage::Refine,
587                "pcurve.unmet_floor",
588                format!(
589                    "{} of {} pcurve fits did not reach the {:.0e} floor \
590                     (sample ceiling {}, round budget {}, stalled {}, span floor {}); \
591                     worst residual {:.3e}; budget raised on {}, declined on {}; \
592                     edge curves sit off their surfaces by up to {:.3e}",
593                    self.unmet,
594                    self.fits,
595                    PCURVE_REFINEMENT_TOLERANCE,
596                    self.sample_ceiling,
597                    self.round_budget,
598                    self.stalled,
599                    self.span_floor,
600                    self.worst_unmet_residual,
601                    self.raised,
602                    self.raise_declined,
603                    self.worst_off_surface,
604                ),
605            );
606        }
607    }
608}
609
610thread_local! {
611    /// The open [`PcurveFitScope`]s on this thread, innermost last.
612    static PCURVE_FIT_SCOPES: std::cell::RefCell<Vec<PcurveFitLedger>> =
613        const { std::cell::RefCell::new(Vec::new()) };
614}
615
616/// An open tally of the pcurve fits made on this thread — see
617/// [`PcurveFitLedger`]. Open it where an operation starts, [`close`] it where
618/// the operation returns its diagnostics; a `?` that unwinds past it drops the
619/// tally.
620///
621/// [`close`]: PcurveFitScope::close
622pub struct PcurveFitScope {
623    /// Not `Send`: the scope must close on the thread that opened it.
624    _thread_bound: std::marker::PhantomData<*const ()>,
625}
626
627impl PcurveFitScope {
628    pub fn open() -> Self {
629        PCURVE_FIT_SCOPES.with(|scopes| scopes.borrow_mut().push(PcurveFitLedger::default()));
630        Self {
631            _thread_bound: std::marker::PhantomData,
632        }
633    }
634
635    /// Take the tally, folding it into the enclosing scope if there is one.
636    pub fn close(self) -> PcurveFitLedger {
637        let ledger = PCURVE_FIT_SCOPES.with(|scopes| {
638            let mut scopes = scopes.borrow_mut();
639            let ledger = scopes.pop().unwrap_or_default();
640            if let Some(parent) = scopes.last_mut() {
641                parent.fold(&ledger);
642            }
643            ledger
644        });
645        std::mem::forget(self);
646        ledger
647    }
648}
649
650impl Drop for PcurveFitScope {
651    fn drop(&mut self) {
652        PCURVE_FIT_SCOPES.with(|scopes| {
653            scopes.borrow_mut().pop();
654        });
655    }
656}
657
658fn record_fit(report: &PcurveFitReport) {
659    // Per-fit trace for the convergence probes (`trim_floor_sweep_probe` and
660    // its offset-shell sibling): one line per fit with the whole report, the
661    // way `BREP_DEBUG_PCURVE` traces an endpoint failure.
662    if std::env::var_os("BREP_DEBUG_PCURVE_FIT").is_some() {
663        eprintln!("PCURVE-FIT {report:?}");
664    }
665    PCURVE_FIT_SCOPES.with(|scopes| {
666        if let Some(open) = scopes.borrow_mut().last_mut() {
667            open.record(report);
668        }
669    });
670}
671
672/// Largest 3D distance between `pcurve`'s image and `curve` at the
673/// quarter-points of every span of `parameters` — the stations refinement
674/// probes, over EVERY span. Evaluations only; nothing is inverted. The sweep
675/// measures, it does not decide.
676fn probe_residual(
677    surface: &NurbsSurface,
678    curve: &NurbsCurve,
679    pcurve: &NurbsCurve,
680    parameters: &[f64],
681    [t0, t1]: [f64; 2],
682    [u0, u1]: [f64; 2],
683    [v0, v1]: [f64; 2],
684) -> Result<f64, String> {
685    let mut worst = 0.0_f64;
686    for span in parameters.windows(2) {
687        for local in [0.25, 0.5, 0.75] {
688            let fraction = span[0] + (span[1] - span[0]) * local;
689            let parameter = pcurve.evaluate(fraction)?;
690            let on_surface =
691                surface.evaluate(parameter.x.clamp(u0, u1), parameter.y.clamp(v0, v1))?;
692            let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction)?;
693            worst = worst.max(on_surface.sub(on_curve).length());
694        }
695    }
696    Ok(worst)
697}
698
699/// What one call to [`refine_pcurve`] achieved, beside the curve it left in
700/// place.
701struct Refinement {
702    exit: PcurveFitExit,
703    residual: f64,
704    off_surface: f64,
705    raised: bool,
706}
707
708/// Adaptive 3D-residual refinement of an interpolant, with the PER-TRIM sample
709/// budget. Shared by [`fit_pcurve_on_surface`] (global inversion) and
710/// [`fit_pcurve_on_surface_marched`] (seeded projection); `insert` is the one
711/// step that differs — given a probe station's fraction, the current pcurve's
712/// parameter there and the edge point, it returns the surface parameter to
713/// interpolate through (or `None` to decline the station) and the distance
714/// from the edge point to the surface at that station, its GAP.
715///
716/// **First pass.** Four rounds at [`MAX_PCURVE_SAMPLES`], each probing the
717/// quarter-points of every span wider than 1e-3 of the parameter range and
718/// inserting the stations over `floor`. Byte-for-byte the pre-2026-09-12
719/// loop; every exit is classified ([`PcurveFitExit`]) and the residual is
720/// measured on the RETURNED curve — over every span, unless the final round
721/// probed every span, in which case that round's maximum already is.
722///
723/// **The raise.** A [`PcurveFitExit::SampleCeiling`] exit re-enters the four
724/// rounds ONCE with [`PCURVE_RAISED_SAMPLES`], continuing from the samples it
725/// has, when the residual exceeds `off_surface` by more than the floor — the
726/// returned curve is worse than the edge curve's own distance from the
727/// surface, so more samples CAN improve it. Measured on `25_cube_pierce_solid`
728/// (2026-09-12): the edge curves there sit off their surfaces by up to 1.9e-4,
729/// four of the eight ceiling exits already read their gap to four digits, and
730/// raising those spent 900 inversions each for no change in the swept
731/// residual. The raised pass therefore inserts only where the pcurve's surface
732/// point is further than the floor from the PROJECTION of the edge point — its
733/// tracking error, which the inversion just computed: it stops when the pcurve
734/// tracks the surface's nearest point to the floor, which is the best any
735/// pcurve can do, and reports [`PcurveFitExit::Stalled`] with the deviation it
736/// actually reached when that is still over the floor. The limit of that pass
737/// as the floor tightens is the projected edge curve itself, so a volume built
738/// on it can be shown to converge; the deviation test alone cannot say that on
739/// off-surface input. Grow-once, capped, deterministic.
740#[allow(clippy::too_many_arguments)]
741fn refine_pcurve(
742    surface: &NurbsSurface,
743    curve: &NurbsCurve,
744    domain: [f64; 2],
745    u_domain: [f64; 2],
746    v_domain: [f64; 2],
747    floor: f64,
748    parameters: &mut Vec<f64>,
749    raw: &mut Vec<[f64; 2]>,
750    pcurve: &mut NurbsCurve,
751    off_surface: f64,
752    mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
753) -> Result<Refinement, String> {
754    let mut best = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
755        floor, parameters, raw, pcurve, off_surface, false, 1.0, &mut insert)?;
756    if best.off_surface > floor { return Ok(best); }
757    // Each target starts from the same baseline station set: retaining the
758    // coarser target's inserts can exhaust the cap before the finer target
759    // places its own error-directed stations. Keep the best converged rung.
760    let baseline_parameters = parameters.clone();
761    let baseline_raw = raw.clone();
762    let baseline_curve = pcurve.clone();
763    let baseline_off_surface = best.off_surface;
764    for fraction in [0.1, 0.01] {
765        if best.residual <= floor * fraction { continue; }
766        let mut refined_parameters = baseline_parameters.clone();
767        let mut refined_raw = baseline_raw.clone();
768        let mut refined_curve = baseline_curve.clone();
769        let candidate = refine_pcurve_pass(surface, curve, domain, u_domain, v_domain,
770            floor, &mut refined_parameters, &mut refined_raw, &mut refined_curve,
771            baseline_off_surface, true, fraction, &mut insert);
772        match candidate {
773            Ok(candidate) if candidate.exit == PcurveFitExit::Converged
774                && candidate.residual <= floor && candidate.residual < best.residual => {
775                *parameters = refined_parameters;
776                *raw = refined_raw;
777                *pcurve = refined_curve;
778                best = candidate;
779            }
780            _ => break,
781        }
782    }
783    Ok(best)
784}
785
786fn refine_pcurve_pass(
787    surface: &NurbsSurface,
788    curve: &NurbsCurve,
789    [t0, t1]: [f64; 2],
790    [u0, u1]: [f64; 2],
791    [v0, v1]: [f64; 2],
792    floor: f64,
793    parameters: &mut Vec<f64>,
794    raw: &mut Vec<[f64; 2]>,
795    pcurve: &mut NurbsCurve,
796    mut off_surface: f64,
797    track_projection: bool,
798    tracking_fraction: f64,
799    mut insert: impl FnMut(f64, Vec3, Vec3) -> Result<(Option<[f64; 2]>, f64), String>,
800) -> Result<Refinement, String> {
801    let tracking_floor = if track_projection { floor * tracking_fraction } else { floor };
802    let mut max_samples = MAX_PCURVE_SAMPLES;
803    let mut raised = false;
804    loop {
805        // Every way out of this loop is CLASSIFIED (`PcurveFitExit`); a budget
806        // exit returns the curve it built, but says so. Falling off the end
807        // after a fourth round of inserts is the round-budget exit.
808        let mut exit = PcurveFitExit::RoundBudget;
809        let mut residual = None;
810        for _ in 0..4 {
811            if parameters.len() >= max_samples {
812                exit = PcurveFitExit::SampleCeiling;
813                break;
814            }
815            let mut inserts = Vec::new();
816            let mut round_max = 0.0_f64;
817            let mut truncated = false;
818            let mut skipped_narrow = false;
819            for index in 0..parameters.len() - 1 {
820                if parameters.len() + inserts.len() >= max_samples {
821                    truncated = true;
822                    break;
823                }
824                let start = parameters[index];
825                let end = parameters[index + 1];
826                if end - start < if track_projection { SPAN_FLOOR } else { 1e-3 } {
827                    skipped_narrow = true;
828                    continue;
829                }
830                for local_fraction in [0.25, 0.5, 0.75] {
831                    if parameters.len() + inserts.len() >= max_samples {
832                        truncated = true;
833                        break;
834                    }
835                    let fraction = start + (end - start) * local_fraction;
836                    let parameter = pcurve.evaluate(fraction)?;
837                    let on_surface =
838                        surface.evaluate(parameter.x.clamp(u0, u1), parameter.y.clamp(v0, v1))?;
839                    let on_curve = curve.evaluate(t0 + (t1 - t0) * fraction)?;
840                    let deviation = on_surface.sub(on_curve).length();
841                    round_max = round_max.max(deviation);
842                    if deviation <= tracking_floor {
843                        continue;
844                    }
845                    let (surface_parameter, gap) = insert(fraction, parameter, on_curve)?;
846                    off_surface = off_surface.max(gap);
847                    if let Some(surface_parameter) = surface_parameter {
848                        // The raised pass stops where the pcurve TRACKS the
849                        // edge curve's projection to the floor — the best any
850                        // pcurve can do at this station. Not `deviation - gap`:
851                        // a surface point at tangential distance d from the
852                        // projection of a point g off the surface is only
853                        // d^2/(2g) further from it, so the deviation test is
854                        // quadratically blind to sideways error on off-surface
855                        // input (6e-6 of slop at g = 1.9e-4, floor 1e-7).
856                        if raised || track_projection {
857                            let projected =
858                                surface.evaluate(surface_parameter[0], surface_parameter[1])?;
859                            if on_surface.sub(projected).length() <= tracking_floor {
860                                continue;
861                            }
862                        }
863                        inserts.push((index + 1, fraction, surface_parameter));
864                    }
865                }
866            }
867            if inserts.is_empty() {
868                // Nothing to insert: every probed station is within the floor
869                // (converged — unless a narrow span was skipped, in which case
870                // the sweep below decides), or the ones over it could not be
871                // improved (stalled — the edge curve is off the surface there).
872                exit = if truncated {
873                    PcurveFitExit::SampleCeiling
874                } else if round_max > floor {
875                    PcurveFitExit::Stalled
876                } else if skipped_narrow {
877                    PcurveFitExit::SpanFloor
878                } else {
879                    residual = Some(round_max);
880                    PcurveFitExit::Converged
881                };
882                break;
883            }
884            for (at, fraction, surface_parameter) in inserts.into_iter().rev() {
885                parameters.insert(at, fraction);
886                raw.insert(at, surface_parameter);
887            }
888            *pcurve = build_interpolant(surface, raw, parameters)?;
889            if truncated {
890                exit = PcurveFitExit::SampleCeiling;
891                break;
892            }
893        }
894        let residual = match residual {
895            Some(measured) => measured,
896            // A budget exit never re-checked the curve it is returning, and a
897            // converged round that skipped narrow spans never checked those:
898            // measure every span.
899            None => probe_residual(
900                surface,
901                curve,
902                pcurve,
903                parameters,
904                [t0, t1],
905                [u0, u1],
906                [v0, v1],
907            )?,
908        };
909        if exit == PcurveFitExit::SpanFloor && residual <= floor {
910            exit = PcurveFitExit::Converged;
911        }
912        // The per-trim budget decision. Deterministic, grow-once, capped (I3):
913        // the ceiling bound and the returned curve is worse than the edge
914        // curve's own gap from the surface by more than the floor, so samples
915        // can still buy accuracy. A curve already at its gap stops here —
916        // reported, not retried.
917        if exit == PcurveFitExit::SampleCeiling && !raised && residual > off_surface + floor {
918            raised = true;
919            max_samples = PCURVE_RAISED_SAMPLES;
920            continue;
921        }
922        return Ok(Refinement {
923            exit,
924            residual,
925            off_surface,
926            raised,
927        });
928    }
929}
930
931/// The exact inverse of an affine carrier's parameterization.
932///
933/// An affine patch (2x2 net, degree 1 both ways, equal weights) maps `uv` to 3D
934/// by an affine map, so the inverse — a 3D point to the `uv` of its orthogonal
935/// projection — is affine too. An affine map commutes with the rational basis:
936/// sending a curve's control points through it and keeping the weights
937/// produces the EXACT image of that curve in parameter space, not a fit of it.
938struct AffineInversion {
939    origin: Vec3,
940    du: Vec3,
941    dv: Vec3,
942    u0: f64,
943    v0: f64,
944    uu: f64,
945    uv: f64,
946    vv: f64,
947    determinant: f64,
948}
949
950impl AffineInversion {
951    /// `None` when `surface` is not affine, or when its two directions are
952    /// parallel enough that the inverse is not determined. The caller decides
953    /// what that means: a refusal on the whole-curve lane, the sampled lane on
954    /// the range lane.
955    fn of(surface: &NurbsSurface) -> Result<Option<Self>, String> {
956        if !surface.is_affine()? {
957            return Ok(None);
958        }
959        let ([u0, _], [v0, _]) = surface_domains(surface)?;
960        let (origin, du, dv) = surface.deriv1(u0, v0)?;
961        let uu = du.dot(du);
962        let uv = du.dot(dv);
963        let vv = dv.dot(dv);
964        let determinant = uu * vv - uv * uv;
965        if determinant.abs() <= 1e-18 {
966            return Ok(None);
967        }
968        Ok(Some(Self {
969            origin,
970            du,
971            dv,
972            u0,
973            v0,
974            uu,
975            uv,
976            vv,
977            determinant,
978        }))
979    }
980
981    fn uv_of(&self, point: Vec3) -> [f64; 2] {
982        let delta = point.sub(self.origin);
983        let along_u = delta.dot(self.du);
984        let along_v = delta.dot(self.dv);
985        [
986            self.u0 + (along_u * self.vv - along_v * self.uv) / self.determinant,
987            self.v0 + (along_v * self.uu - along_u * self.uv) / self.determinant,
988        ]
989    }
990
991    /// The exact image of `curve` in this carrier's parameter space, with the
992    /// curve's own degree, knots and weights.
993    fn image_of(&self, curve: &NurbsCurve) -> Result<NurbsCurve, String> {
994        let control_points = curve
995            .control_points
996            .iter()
997            .map(|control| {
998                let [u, v] = self.uv_of(control.point()?);
999                Ok(Vec4::from_point(Vec3::new(u, v, 0.0), control.w))
1000            })
1001            .collect::<Result<Vec<_>, String>>()?;
1002        NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
1003    }
1004}
1005
1006/// Build a parameter-space curve for a 3D curve lying on a surface, and say
1007/// what the fit achieved.
1008///
1009/// This mirrors the reference imprint implementation: affine patches map
1010/// homogeneous control points exactly; general patches use sampled inversion,
1011/// periodic seam unwrapping, and adaptive 3D residual refinement. The report
1012/// beside the curve carries the residual the RETURNED curve reaches and why
1013/// refinement stopped ([`PcurveFitReport`]); the curve itself is what
1014/// [`build_pcurve_on_surface`] has always returned.
1015pub fn fit_pcurve_on_surface(
1016    surface: &NurbsSurface,
1017    curve: &NurbsCurve,
1018) -> Result<PcurveFit, String> {
1019    let [t0, t1] = curve.domain()?;
1020    if surface.is_affine()? {
1021        let Some(inversion) = AffineInversion::of(surface)? else {
1022            return Err("build_pcurve_on_surface: singular affine parameterization".into());
1023        };
1024        let pcurve = inversion.image_of(curve)?;
1025        return Ok(PcurveFit {
1026            report: PcurveFitReport {
1027                residual: 0.0,
1028                floor: PCURVE_REFINEMENT_TOLERANCE,
1029                samples: pcurve.control_points.len(),
1030                exit: PcurveFitExit::Converged,
1031                off_surface: 0.0,
1032                raised: false,
1033            },
1034            curve: pcurve,
1035        });
1036    }
1037
1038    let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
1039    let drop_tolerance = 1e-4 * scale;
1040    let endpoint_tolerance = 1e-3 * scale;
1041    let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
1042    let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
1043    let invert =
1044        |fraction: f64| invert_checked(surface, curve.evaluate(t0 + (t1 - t0) * fraction)?);
1045
1046    let mut parameters = Vec::with_capacity(25);
1047    let mut raw_surface_parameters = Vec::with_capacity(25);
1048    // The edge curve's own distance from the surface at every station inverted,
1049    // accepted or not: the floor below which no pcurve can bring the residual.
1050    let mut off_surface = 0.0_f64;
1051    for index in 0..=24 {
1052        let fraction = index as f64 / 24.0;
1053        let (parameter, distance) = invert(fraction)?;
1054        off_surface = off_surface.max(distance);
1055        if distance > drop_tolerance && index != 0 && index != 24 {
1056            continue;
1057        }
1058        if distance > endpoint_tolerance {
1059            if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
1060                let p3 = curve.evaluate(t0 + (t1 - t0) * fraction).ok();
1061                let p0 = curve.evaluate(t0).ok();
1062                let p1 = curve.evaluate(t1).ok();
1063                eprintln!(
1064                    "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={:?}",
1065                    surface.evaluate(parameter[0], parameter[1]).ok()
1066                );
1067            }
1068            return Err(format!(
1069                "build_pcurve_on_surface: endpoint projection failed (distance={distance})"
1070            ));
1071        }
1072        parameters.push(fraction);
1073        raw_surface_parameters.push(parameter);
1074    }
1075
1076    let mut pcurve = build_interpolant(surface, &raw_surface_parameters, &parameters)?;
1077    let refinement = refine_pcurve(
1078        surface,
1079        curve,
1080        [t0, t1],
1081        [u0, u1],
1082        [v0, v1],
1083        refinement_tolerance,
1084        &mut parameters,
1085        &mut raw_surface_parameters,
1086        &mut pcurve,
1087        off_surface,
1088        |fraction, _parameter, _on_curve| {
1089            let (surface_parameter, distance) = invert(fraction)?;
1090            Ok((
1091                (distance <= drop_tolerance).then_some(surface_parameter),
1092                distance,
1093            ))
1094        },
1095    )?;
1096    Ok(PcurveFit {
1097        curve: pcurve,
1098        report: PcurveFitReport {
1099            residual: refinement.residual,
1100            floor: refinement_tolerance,
1101            samples: parameters.len(),
1102            exit: refinement.exit,
1103            off_surface: refinement.off_surface,
1104            raised: refinement.raised,
1105        },
1106    })
1107}
1108
1109/// [`fit_pcurve_on_surface`] for the callers that only need the curve. The
1110/// report is not dropped: it is tallied on the open [`PcurveFitScope`], if any,
1111/// so the operation that owns this fit can say in its diagnostics whether every
1112/// trim reached the floor.
1113pub fn build_pcurve_on_surface(
1114    surface: &NurbsSurface,
1115    curve: &NurbsCurve,
1116) -> Result<NurbsCurve, String> {
1117    let fit = fit_pcurve_on_surface(surface, curve)?;
1118    record_fit(&fit.report);
1119    Ok(fit.curve)
1120}
1121
1122/// SECTION-PCURVE SEEDED MARCH (t222: cone × ABC 00000327, non-integral genus).
1123///
1124/// `build_pcurve_on_surface` inverts every sample by GLOBAL closest point. Where
1125/// the carrier surface is COMPRESSED / self-overlapping along the row the
1126/// section rides, the global search aliases interior samples onto a DISTANT
1127/// preimage sheet, folding the pcurve out past the trim boundary and re-crossing
1128/// it. The arrangement then splits the shared section at that phantom crossing,
1129/// while the mate face (whose carrier is not folded there) keeps the section
1130/// whole — so the two operands' fragments disagree and the section strands
1131/// one-use (non-integral genus). Ground truth for t222: 00000327 face 496's
1132/// v≈0.7875 row maps u∈[0.55,0.81] into a ~0.18mm neighbourhood of the section
1133/// endpoint A; the global pcurve for piece 9 folded out to u=0.806 and back,
1134/// re-crossing trim edge 349 at v≈0.784, 0.003 below the corner vertex A.
1135///
1136/// This marches the interior inversions with each seeded from the PREVIOUS
1137/// accepted parameters, so a marched sample
1138/// adopts the nearby branch instead of the momentarily-closest distant sheet.
1139/// The two ENDPOINT samples keep the deterministic global inversion (they are
1140/// the piece's shared junction vertices — same reasoning as
1141/// `repair_branch_jumps`). Additive + fail-soft: the marched chain is used ONLY
1142/// when it CLOSES onto the global far endpoint (its last interior sample is
1143/// parameter-adjacent to it, measured against the chain's own median step);
1144/// otherwise the plain global build is returned byte-for-byte. A single-preimage
1145/// carrier (the cone side of the same section) marches to the same samples the
1146/// global search already had. Use the analytic-aware seeded projector for both
1147/// the march and inserted stations: raw Newton stops at 1e-7 even on an exact
1148/// cylinder, introducing avoidable trim error. General folded carriers retain
1149/// their local seeded Newton path; analytic carriers take their closed form.
1150///
1151/// Escape hatch: `BREP_SECTION_PCURVE_MARCH=0`.
1152pub fn fit_pcurve_on_surface_marched(
1153    surface: &NurbsSurface,
1154    curve: &NurbsCurve,
1155) -> Result<PcurveFit, String> {
1156    let global = fit_pcurve_on_surface(surface, curve)?;
1157    if std::env::var("BREP_SECTION_PCURVE_MARCH").as_deref() == Ok("0") || surface.is_affine()? {
1158        return Ok(global);
1159    }
1160    let [t0, t1] = curve.domain()?;
1161    // A closed (ring) section has no distinct endpoints to anchor the march;
1162    // leave it to the existing path (closed rings are handled elsewhere).
1163    if curve.evaluate(t0)?.sub(curve.evaluate(t1)?).length() <= 1e-6 {
1164        return Ok(global);
1165    }
1166    const SAMPLES: usize = 24;
1167    let fractions: Vec<f64> = (0..=SAMPLES).map(|i| i as f64 / SAMPLES as f64).collect();
1168    let edge: Vec<Vec3> = fractions
1169        .iter()
1170        .map(|fraction| curve.evaluate(t0 + (t1 - t0) * fraction))
1171        .collect::<Result<Vec<_>, _>>()?;
1172    let scale = 1.0 + curve.evaluate((t0 + t1) / 2.0)?.length();
1173    let endpoint_tolerance = 1e-3 * scale;
1174    let (first, first_distance) = invert_checked(surface, edge[0])?;
1175    let (last, last_distance) = invert_checked(surface, edge[SAMPLES])?;
1176    if first_distance > endpoint_tolerance || last_distance > endpoint_tolerance {
1177        return Ok(global);
1178    }
1179    // The edge curve's distance from the surface at every station this chain
1180    // inverts — on the MARCHED branch, which is the one being fitted.
1181    let mut off_surface = first_distance.max(last_distance);
1182    let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
1183    let u_span = (u1 - u0).abs().max(EPSILON);
1184    let v_span = (v1 - v0).abs().max(EPSILON);
1185    let (closed_u, closed_v) = surface_closedness(surface)?;
1186    let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
1187        let mut du = a[0] - b[0];
1188        if closed_u {
1189            while du > 0.5 * u_span {
1190                du -= u_span;
1191            }
1192            while du < -0.5 * u_span {
1193                du += u_span;
1194            }
1195        }
1196        let mut dv = a[1] - b[1];
1197        if closed_v {
1198            while dv > 0.5 * v_span {
1199                dv -= v_span;
1200            }
1201            while dv < -0.5 * v_span {
1202                dv += v_span;
1203            }
1204        }
1205        ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
1206    };
1207    // Forward seeded march over the interior samples.
1208    let mut raw = vec![first];
1209    let mut previous = first;
1210    for index in 1..SAMPLES {
1211        let seeded =
1212            crate::projection::project_point_to_surface_from_seed(surface, edge[index], previous)?;
1213        let (_, global_distance) = invert_checked(surface, edge[index])?;
1214        // The seeded footpoint must still sit on the surface — as tight as the
1215        // fit band or the global answer already had it. If it fell off (the
1216        // seed led Newton into a valley), abandon the march and keep global.
1217        if seeded.distance > 4.0 * global_distance + endpoint_tolerance {
1218            return Ok(global);
1219        }
1220        off_surface = off_surface.max(seeded.distance);
1221        previous = [seeded.u, seeded.v];
1222        raw.push(previous);
1223    }
1224    raw.push(last);
1225    // CLOSURE GATE: the last marched interior sample must be parameter-adjacent
1226    // to the global far endpoint — the march stayed on ONE branch the whole way.
1227    // Measure the endpoint gap against the chain's OWN median step so a genuine
1228    // long edge is not rejected while a sheet-hop (which leaves a big gap to the
1229    // endpoint) is.
1230    let mut steps: Vec<f64> = (1..raw.len())
1231        .map(|i| norm_step(raw[i], raw[i - 1]))
1232        .collect();
1233    let endpoint_gap = steps.pop().unwrap_or(0.0);
1234    steps.sort_by(f64::total_cmp);
1235    let median = steps
1236        .get(steps.len() / 2)
1237        .copied()
1238        .unwrap_or(0.0)
1239        .max(EPSILON);
1240    if endpoint_gap > (4.0 * median).max(0.05) {
1241        return Ok(global);
1242    }
1243    // Build the interpolant from the marched samples, then refine — re-projecting
1244    // each insert SEEDED from the interpolant (already on the marched branch), so
1245    // a mid-refinement global inversion cannot re-alias onto the far sheet.
1246    let mut parameters = fractions;
1247    let mut pcurve = build_interpolant(surface, &raw, &parameters)?;
1248    let refinement_tolerance = PCURVE_REFINEMENT_TOLERANCE;
1249    // Same classified exits and budget ladder as `fit_pcurve_on_surface`. A
1250    // seeded insert always lands, so this loop cannot stall.
1251    let refinement = refine_pcurve(
1252        surface,
1253        curve,
1254        [t0, t1],
1255        [u0, u1],
1256        [v0, v1],
1257        refinement_tolerance,
1258        &mut parameters,
1259        &mut raw,
1260        &mut pcurve,
1261        off_surface,
1262        |_fraction, seed, on_curve| {
1263            let seeded = crate::projection::project_point_to_surface_from_seed(surface, on_curve, [seed.x, seed.y])?;
1264            Ok((Some([seeded.u, seeded.v]), seeded.distance))
1265        },
1266    )?;
1267    Ok(PcurveFit {
1268        curve: pcurve,
1269        report: PcurveFitReport {
1270            residual: refinement.residual,
1271            floor: refinement_tolerance,
1272            samples: parameters.len(),
1273            exit: refinement.exit,
1274            off_surface: refinement.off_surface,
1275            raised: refinement.raised,
1276        },
1277    })
1278}
1279
1280/// [`fit_pcurve_on_surface_marched`] for the callers that only need the curve;
1281/// the report is tallied on the open [`PcurveFitScope`] exactly as
1282/// [`build_pcurve_on_surface`] does.
1283pub fn build_pcurve_on_surface_marched(
1284    surface: &NurbsSurface,
1285    curve: &NurbsCurve,
1286) -> Result<NurbsCurve, String> {
1287    let fit = fit_pcurve_on_surface_marched(surface, curve)?;
1288    record_fit(&fit.report);
1289    Ok(fit.curve)
1290}
1291
1292/// Build a pcurve for a represented subrange of a larger edge curve.
1293///
1294/// On an affine carrier the answer is exact and is taken as such
1295/// ([`affine_pcurve_on_range`]). Otherwise this samples only the represented
1296/// interval, which is essential when off-interval control points do not lie on
1297/// the target carrier — the exact lane restricts the net by knot insertion
1298/// first, so those control points are removed rather than ignored.
1299pub fn fit_pcurve_on_surface_range(
1300    surface: &NurbsSurface,
1301    curve: &NurbsCurve,
1302    edge_start: f64,
1303    edge_end: f64,
1304    forward: bool,
1305    tolerance: f64,
1306) -> Result<PcurveFit, String> {
1307    if let Some(exact) =
1308        affine_pcurve_on_range(surface, curve, edge_start, edge_end, forward, tolerance)?
1309    {
1310        return Ok(exact);
1311    }
1312    fit_pcurve_on_surface_range_dense(
1313        surface, curve, edge_start, edge_end, forward, tolerance, 64, 3, 513,
1314    )
1315}
1316
1317/// [`fit_pcurve_on_surface_range`] for the callers that only need the curve:
1318/// the fit is ACCEPTED or REFUSED on its bar ([`accept_fit`]), never returned
1319/// off it.
1320pub fn build_pcurve_on_surface_range(
1321    surface: &NurbsSurface,
1322    curve: &NurbsCurve,
1323    edge_start: f64,
1324    edge_end: f64,
1325    forward: bool,
1326    tolerance: f64,
1327) -> Result<NurbsCurve, String> {
1328    accept_fit(fit_pcurve_on_surface_range(
1329        surface, curve, edge_start, edge_end, forward, tolerance,
1330    )?)
1331}
1332
1333/// `BREP_DEBUG_STATION_FIT`: trace every station fit round by round, and the
1334/// stations around the worst span of a fit off its bar. Read once per process.
1335fn station_fit_tracing() -> bool {
1336    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1337    *ON.get_or_init(|| std::env::var_os("BREP_DEBUG_STATION_FIT").is_some())
1338}
1339
1340/// Narrowest span, as a fraction of the parameter range, that
1341/// [`fit_pcurve_on_surface_stations`] subdivides. `refine_pcurve` floors at
1342/// 1e-3 from 25 base samples; this fitter starts from 64 and quarters, so a
1343/// floor of 1e-3 bound on its first pass and refused 67 of the 57-document
1344/// corpus's bodies (measured 2026-09-26). 1e-6 admits every halving a real
1345/// trim needs and stops the runaway that fits stations 4e-7 apart.
1346const SPAN_FLOOR: f64 = 1e-6;
1347
1348/// Rounds the raised pass of [`fit_pcurve_on_surface_stations`] may spend:
1349/// enough halvings for any span the sample cap can resolve.
1350const RAISED_ROUNDS: usize = 40;
1351
1352/// The refusal every station builder returns for a fit off its bar.
1353pub const PCURVE_OFF_BAR: &str = "pcurve fit refused: the trim misses its stations by";
1354
1355/// The one acceptance decision behind [`build_pcurve_on_surface_range`],
1356/// [`build_pcurve_on_surface_range_dense`] and
1357/// [`build_pcurve_on_surface_stations`]: a fit on its bar
1358/// ([`PcurveFitReport::on_bar`]) is the curve, a fit off it is a refusal that
1359/// names the residual, the bar, what was asked, how far the stations stand off
1360/// the carrier and why refinement stopped.
1361///
1362/// Until 2026-09-26 these builders returned whatever their last round built
1363/// without measuring it (the 2026-09-18 census: 4564 of 54556 imported pcurves
1364/// over the ask they were built to, 2072 of them at a station the refinement
1365/// itself had tested and given up on). Nothing here widens a band: the bar is
1366/// the caller's own ask plus what the input makes unreachable.
1367fn accept_fit(fit: PcurveFit) -> Result<NurbsCurve, String> {
1368    let report = fit.report;
1369    if report.on_bar() {
1370        return Ok(fit.curve);
1371    }
1372    Err(off_bar_message(&report))
1373}
1374
1375fn off_bar_message(report: &PcurveFitReport) -> String {
1376    format!(
1377        "{PCURVE_OFF_BAR} {:.3e} out of sample against a bar of {:.3e} (asked {:.1e}, the \
1378         stations stand {:.3e} off the carrier; {:?} at {} samples{})",
1379        report.residual,
1380        report.floor + report.off_surface,
1381        report.floor,
1382        report.off_surface,
1383        report.exit,
1384        report.samples,
1385        if report.raised { ", after the raise" } else { "" },
1386    )
1387}
1388
1389/// The class slug of a pcurve fit refused off its bar
1390/// ([`crate::RefusalClass::NonConvergence`]): minted by
1391/// [`build_pcurve_on_surface_range_classed`] and read by
1392/// [`is_pcurve_off_bar`], so a dispatcher decides on the class, never on the
1393/// message text.
1394pub(crate) const PCURVE_OFF_BAR_WHAT: &str = "pcurve_fit_off_bar";
1395
1396/// Whether `refusal` is a pcurve fit that could not reach its bar.
1397pub(crate) fn is_pcurve_off_bar(refusal: &crate::KernelRefusal) -> bool {
1398    matches!(&refusal.class, crate::RefusalClass::NonConvergence { what } if what == PCURVE_OFF_BAR_WHAT)
1399}
1400
1401/// [`build_pcurve_on_surface_range`] with a typed refusal: a fit off its bar
1402/// is `NonConvergence { what: PCURVE_OFF_BAR_WHAT }` at `stage`, with the same
1403/// message; any other failure of the fit is `Internal` under `what`.
1404pub(crate) fn build_pcurve_on_surface_range_classed(
1405    surface: &NurbsSurface,
1406    curve: &NurbsCurve,
1407    edge_start: f64,
1408    edge_end: f64,
1409    forward: bool,
1410    tolerance: f64,
1411    stage: KernelStage,
1412    what: &'static str,
1413) -> Result<NurbsCurve, crate::KernelRefusal> {
1414    use crate::OrRefuse;
1415    let fit = fit_pcurve_on_surface_range(surface, curve, edge_start, edge_end, forward, tolerance)
1416        .or_refuse(stage, what)?;
1417    accept_fit_classed(fit, stage)
1418}
1419
1420/// [`accept_fit`] with the refusal typed: `NonConvergence { what:
1421/// PCURVE_OFF_BAR_WHAT }` at `stage`, the message unchanged.
1422pub(crate) fn accept_fit_classed(fit: PcurveFit, stage: KernelStage) -> Result<NurbsCurve, crate::KernelRefusal> {
1423    if fit.report.on_bar() {
1424        return Ok(fit.curve);
1425    }
1426    Err(crate::KernelRefusal {
1427        class: crate::RefusalClass::NonConvergence { what: PCURVE_OFF_BAR_WHAT.into() },
1428        stage,
1429        message: off_bar_message(&fit.report),
1430    })
1431}
1432
1433/// The EXACT pcurve for a coedge's subrange of `curve` on an affine carrier,
1434/// or `None` when there is no such answer and the sampled lane must run.
1435///
1436/// [`fit_pcurve_on_surface`] has always mapped a WHOLE curve's control net
1437/// exactly on an affine carrier. A coedge's trim is that same curve restricted
1438/// to `[edge_start, edge_end]` and oriented by `forward`, and both operations
1439/// are exact on a NURBS: [`NurbsCurve::split`] subdivides by knot insertion,
1440/// [`NurbsCurve::reversed`] mirrors the net, and rescaling the knots to
1441/// `[0, 1]` is affine in the edge's own parameter — which is precisely the
1442/// fraction convention the sampled lane uses, so the pcurve's correspondence
1443/// with the edge is the same one either lane produces.
1444///
1445/// Sampling where an exact answer exists leaves a real error: measured
1446/// 2026-09-17, a booleaned band rim's two planar trims sat 2.379e-4 and
1447/// 2.395e-4 off their edges, where this lane lands 1.4e-13 off.
1448///
1449/// The image is verified against the caller's own `tolerance` before it is
1450/// returned. The exact image of a curve that does NOT lie on this carrier is
1451/// its orthogonal projection, which is not that coedge's trim, so every such
1452/// case falls through to the sampled lane and keeps the refusals that lane
1453/// makes today.
1454fn affine_pcurve_on_range(
1455    surface: &NurbsSurface,
1456    curve: &NurbsCurve,
1457    edge_start: f64,
1458    edge_end: f64,
1459    forward: bool,
1460    tolerance: f64,
1461) -> Result<Option<PcurveFit>, String> {
1462    // Opt-out for A/B bisection of this lane against the sampled one, in the
1463    // shape of the two hatches beside it (`BREP_NO_PCURVE_REPAIR`,
1464    // `BREP_SECTION_PCURVE_MARCH`). It is what showed that the boolean's
1465    // planar residuals do NOT come through here: the same binary with this set
1466    // and unset read identically to every digit on 21_band_rim.
1467    if std::env::var("BREP_AFFINE_RANGE_PCURVE").as_deref() == Ok("0") {
1468        return Ok(None);
1469    }
1470    let Some(inversion) = AffineInversion::of(surface)? else {
1471        return Ok(None);
1472    };
1473    if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
1474        // The sampled lane owns this refusal; it is not this lane's to reword.
1475        return Ok(None);
1476    }
1477    let [t0, t1] = curve.domain()?;
1478    let start = edge_start.max(t0);
1479    let end = edge_end.min(t1);
1480    // A range narrower than the knot identity tolerance cannot be split onto
1481    // its own endpoints at all.
1482    if end - start <= 2.0 * KNOT_IDENTITY_TOL {
1483        return Ok(None);
1484    }
1485    let mut restricted = curve.clone();
1486    if end < t1 - KNOT_IDENTITY_TOL {
1487        restricted = restricted.split(end)?.0;
1488    }
1489    if start > t0 + KNOT_IDENTITY_TOL {
1490        restricted = restricted.split(start)?.1;
1491    }
1492    let mut pcurve = inversion.image_of(&restricted)?;
1493    if !forward {
1494        pcurve = pcurve.reversed()?;
1495    }
1496    let first = pcurve.knots[0];
1497    let last = pcurve.knots[pcurve.knots.len() - 1];
1498    if !(last - first > 0.0) {
1499        return Ok(None);
1500    }
1501    let knots = pcurve
1502        .knots
1503        .iter()
1504        .map(|knot| (knot - first) / (last - first))
1505        .collect::<Vec<_>>();
1506    let pcurve = NurbsCurve::new(pcurve.degree, knots, pcurve.control_points.clone())?;
1507
1508    // Verify against the range the CALLER asked for, not the one the
1509    // restriction produced: `split` snaps onto a nearby knot, so a snap that
1510    // moved the correspondence reads here as a deviation. Sampling at
1511    // fractions that are not the stations any lane interpolates through keeps
1512    // this a check on the whole trim rather than on its endpoints.
1513    let scale = 1.0 + curve.evaluate((start + end) / 2.0)?.length();
1514    let bar = tolerance.max(1e-11 * scale);
1515    let mut residual = 0.0_f64;
1516    for index in 0..=32 {
1517        let fraction = index as f64 / 32.0;
1518        let uv = pcurve.evaluate(fraction)?;
1519        // `evaluate` CLAMPS out-of-domain uv. A planar carrier's domain is a
1520        // bounding rectangle that a booleaned trim can leave, and an affine
1521        // patch's extension is the same plane, so read it unclamped.
1522        let on_carrier = surface.evaluate_extended(uv.x, uv.y)?;
1523        let edge_fraction = if forward { fraction } else { 1.0 - fraction };
1524        let on_edge = curve.evaluate(start + (end - start) * edge_fraction)?;
1525        residual = residual.max(on_carrier.sub(on_edge).length());
1526        if residual > bar {
1527            return Ok(None);
1528        }
1529    }
1530    let samples = pcurve.control_points.len();
1531    Ok(Some(PcurveFit {
1532        curve: pcurve,
1533        report: PcurveFitReport {
1534            residual,
1535            // The floor this lane VERIFIED against: the caller's ask, or the
1536            // floating-point floor of an exact map on a part this size. An
1537            // exact image is on its bar by construction; a caller asking for
1538            // less than the map's own precision is not refused for it.
1539            floor: bar,
1540            samples,
1541            exit: PcurveFitExit::Converged,
1542            off_surface: 0.0,
1543            raised: false,
1544        },
1545    }))
1546}
1547
1548fn align_collapsed_endpoints(surface: &NurbsSurface, raw: &mut [[f64; 2]]) -> Result<(), String> {
1549    let anchor = surface.control_points[0][0].point()?;
1550    let mut extent = 0.0_f64;
1551    for control in surface.control_points.iter().flatten() {
1552        extent = extent.max(control.point()?.sub(anchor).length());
1553    }
1554    // This is a geometric identity check, independent of the fitting allowance
1555    // and of world position. Positive rational weights keep the whole iso-curve
1556    // inside the convex hull of its Euclidean controls. Cap the size coupling
1557    // at the linear tolerance so a large patch cannot erase a thin finite row.
1558    let pole_band = (1e-10 * (1.0 + extent)).min(LINEAR_TOLERANCE);
1559    for (endpoint, neighbor) in [(0, 1), (raw.len() - 1, raw.len() - 2)] {
1560        let point = surface.evaluate(raw[endpoint][0], raw[endpoint][1])?;
1561        for axis in 0..2 {
1562            let mut candidate = raw[endpoint];
1563            candidate[axis] = raw[neighbor][axis];
1564            if candidate == raw[endpoint]
1565                || surface
1566                    .evaluate(candidate[0], candidate[1])?
1567                    .sub(point)
1568                    .length()
1569                    > pole_band
1570            {
1571                continue;
1572            }
1573            let row = if axis == 0 {
1574                surface.iso_curve_v(raw[endpoint][1])?
1575            } else {
1576                surface.iso_curve_u(raw[endpoint][0])?
1577            };
1578            let mut collapsed = true;
1579            for control in &row.control_points {
1580                if control.point()?.sub(point).length() > pole_band {
1581                    collapsed = false;
1582                    break;
1583                }
1584            }
1585            if collapsed {
1586                raw[endpoint] = candidate;
1587            }
1588        }
1589    }
1590    Ok(())
1591}
1592
1593/// Re-seat any raw inversion sample that BRANCH-JUMPED — snapped to a distant
1594/// fold of a self-overlapping general carrier — back onto the branch traced by
1595/// its neighbours.
1596///
1597/// Each entry in `raw` is an INDEPENDENT global closest-point inversion of the
1598/// corresponding 3D edge sample. Every one is geometrically valid (on the
1599/// surface, at the edge), but they need not be CONTIGUOUS: where a rational
1600/// B-spline surface folds back over the small trimmed patch it carries, the
1601/// momentarily-closest fold flips from one sample to the next, so the raw
1602/// polygon zig-zags across the whole domain and self-crosses — and the region
1603/// its interpolant bounds no longer covers the true face (its surface flux can
1604/// exceed its own area). We anchor on the LONGEST run of mutually-continuous
1605/// samples — the fold the trimmed patch actually lies on, since a small face
1606/// lives on ONE fold and the jumped samples are the minority the global search
1607/// snapped elsewhere — then walk outward in both directions, re-seeding Newton
1608/// from the last good parameters and adopting the continuous footpoint whenever
1609/// it is geometrically as valid as the global one. Anchoring on consensus (not
1610/// blindly on sample 0, which can itself be an outlier that would drag the
1611/// whole edge onto the wrong branch and tear the loop open at its endpoint)
1612/// leaves continuous nonsingular fits unchanged. Certified collapsed endpoint
1613/// rows first adopt their neighbour's branch; a bad seed in the subsequent
1614/// jump repair cannot make a sample worse than the global search already had it.
1615fn repair_branch_jumps(
1616    surface: &NurbsSurface,
1617    edge_points: &[Vec3],
1618    raw: &mut [[f64; 2]],
1619    tolerance: f64,
1620) -> Result<(), String> {
1621    if raw.len() < 3 {
1622        return Ok(());
1623    }
1624    // Affine carriers invert linearly and exactly — no folds, nothing to chase.
1625    // Checked first so the common planar/affine face never touches the env.
1626    if surface.is_affine()? {
1627        return Ok(());
1628    }
1629    // Opt-out for A/B bisection of a STEP-import regression against this repair.
1630    if std::env::var("BREP_NO_PCURVE_REPAIR").is_ok() {
1631        return Ok(());
1632    }
1633    // A pole has no unique coordinate along its collapsed row. Continuing the
1634    // adjacent sample's branch is safe only when the ENTIRE row is collapsed;
1635    // coincident points across an ordinary seam or fold are not enough.
1636    align_collapsed_endpoints(surface, raw)?;
1637    let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
1638    let u_span = (u1 - u0).abs().max(EPSILON);
1639    let v_span = (v1 - v0).abs().max(EPSILON);
1640    let (closed_u, closed_v) = surface_closedness(surface)?;
1641    // Normalised, seam-aware step between two parameter samples. Wrapping the
1642    // closed directions keeps a legitimate seam crossing SMALL, so the seam /
1643    // periodic-unwrap machinery elsewhere is never disturbed by this repair.
1644    let norm_step = |a: [f64; 2], b: [f64; 2]| -> f64 {
1645        let mut du = a[0] - b[0];
1646        if closed_u {
1647            while du > 0.5 * u_span {
1648                du -= u_span;
1649            }
1650            while du < -0.5 * u_span {
1651                du += u_span;
1652            }
1653        }
1654        let mut dv = a[1] - b[1];
1655        if closed_v {
1656            while dv > 0.5 * v_span {
1657                dv -= v_span;
1658            }
1659            while dv < -0.5 * v_span {
1660                dv += v_span;
1661            }
1662        }
1663        ((du / u_span).powi(2) + (dv / v_span).powi(2)).sqrt()
1664    };
1665    // A jump crosses a large fraction of the WHOLE domain — orders above the
1666    // per-sample motion of any real (even domain-spanning) edge, which advances
1667    // ~1/N of its traversal between consecutive stations.
1668    const JUMP_THRESHOLD: f64 = 0.2;
1669    let count = raw.len();
1670
1671    // Locate the longest maximal run of consecutive continuous samples — the
1672    // branch to anchor on. If nothing jumps, this spans [0, count-1] and both
1673    // re-seat passes below are empty, leaving `raw` untouched.
1674    let (mut best_start, mut best_len, mut run_start) = (0usize, 1usize, 0usize);
1675    for index in 1..count {
1676        if norm_step(raw[index], raw[index - 1]) > JUMP_THRESHOLD {
1677            if index - run_start > best_len {
1678                best_len = index - run_start;
1679                best_start = run_start;
1680            }
1681            run_start = index;
1682        }
1683    }
1684    if count - run_start > best_len {
1685        best_len = count - run_start;
1686        best_start = run_start;
1687    }
1688    let (spine_lo, spine_hi) = (best_start, best_start + best_len - 1);
1689
1690    // Adopt the continuous footpoint only when it (a) still sits on the surface
1691    // — as tight as the fit band or the global answer — and (b) genuinely
1692    // closes the jump rather than trading it for another. Returns the parameter
1693    // to carry forward as the next seed (the repaired one, or the untouched
1694    // global when no repair applies).
1695    let reseat = |edge_point: Vec3,
1696                  previous: [f64; 2],
1697                  global: [f64; 2]|
1698     -> Result<[f64; 2], String> {
1699        let global_step = norm_step(global, previous);
1700        if global_step <= JUMP_THRESHOLD {
1701            return Ok(global);
1702        }
1703        let seeded =
1704            project_point_to_surface_seeded(surface, edge_point, previous[0], previous[1])?;
1705        let candidate = [seeded.u, seeded.v];
1706        let global_residual = surface
1707            .evaluate(global[0], global[1])?
1708            .sub(edge_point)
1709            .length();
1710        let on_surface = seeded.distance <= 4.0 * global_residual + tolerance.max(1e-12);
1711        if on_surface && norm_step(candidate, previous) < 0.5 * global_step {
1712            if std::env::var("BREP_DEBUG_PCURVE").is_ok() {
1713                eprintln!(
1714                    "pcurve repair: jump {global_step:.4} ({global:?}) -> {:.4} ({candidate:?}) res {:.2e}->{:.2e}",
1715                    norm_step(candidate, previous),
1716                    global_residual,
1717                    seeded.distance
1718                );
1719            }
1720            Ok(candidate)
1721        } else {
1722            Ok(global)
1723        }
1724    };
1725
1726    // Walk forward off the spine's high end, then backward off its low end,
1727    // chaining each repaired sample as the next seed so continuity propagates.
1728    //
1729    // Apart from the certified collapsed rows above, the two ENDPOINT samples
1730    // (fraction 0 and 1) are NEVER moved: they are the
1731    // edge's shared loop vertices. The global inversion is deterministic, so the
1732    // two coedges meeting at a vertex land it at the SAME parameters even when
1733    // that vertex sits on a fold reachable from two branches — moving one side
1734    // to a different branch tears the loop open there (`synthesize_pole_edge`
1735    // then rejects the non-collapsed gap). A vertex's genuine fold transition
1736    // (an edge whose interior rides u≈0.08 but whose endpoint must meet its
1737    // neighbour at u≈0.95) is exactly this case and must be preserved, not
1738    // "continuity-repaired" back onto the interior branch.
1739    let mut previous = raw[spine_hi];
1740    for index in (spine_hi + 1)..count.saturating_sub(1) {
1741        raw[index] = reseat(edge_points[index], previous, raw[index])?;
1742        previous = raw[index];
1743    }
1744    let mut previous = raw[spine_lo];
1745    for index in (1..spine_lo).rev() {
1746        raw[index] = reseat(edge_points[index], previous, raw[index])?;
1747        previous = raw[index];
1748    }
1749    Ok(())
1750}
1751
1752/// Range fitter with explicit sampling knobs, reporting what the fit achieved.
1753/// [`fit_pcurve_on_surface_range`] keeps the long-standing (base 64, 3
1754/// refinement rounds, 513 cap) budget; a fit that binds at that cap with the
1755/// bar unmet grows once to [`PCURVE_RAISED_SAMPLES`]
1756/// ([`fit_pcurve_on_surface_stations`]).
1757///
1758/// NOTE (2026-09-03): this comment used to say "STEP import retries failed fits
1759/// with a denser budget". No caller passes these knobs any more — that retry
1760/// was removed and the sentence outlived it. The knobs are kept because they
1761/// are the honest way to ASK whether a fit had headroom left, which is what
1762/// `examples/pcurve_residual_probe.rs` uses them for: on every ABC corpus
1763/// coedge that failed `validate`'s pcurve check, budgets up to (512, 8, 8193)
1764/// with a 1000x tighter target reproduced the coarse fit's deviation to six
1765/// decimals, because that deviation is the edge-vs-surface closest-point
1766/// residual and a curve ON the surface cannot beat it.
1767#[allow(clippy::too_many_arguments)]
1768pub fn fit_pcurve_on_surface_range_dense(
1769    surface: &NurbsSurface,
1770    curve: &NurbsCurve,
1771    edge_start: f64,
1772    edge_end: f64,
1773    forward: bool,
1774    tolerance: f64,
1775    base_samples: usize,
1776    refinement_rounds: usize,
1777    parameter_cap: usize,
1778) -> Result<PcurveFit, String> {
1779    if !(edge_start.is_finite() && edge_end.is_finite() && edge_start < edge_end) {
1780        return Err("build_pcurve_on_surface_range: invalid edge interval".into());
1781    }
1782    let evaluate_edge = |fraction: f64| {
1783        let edge_fraction = if forward { fraction } else { 1.0 - fraction };
1784        curve.evaluate(edge_start + (edge_end - edge_start) * edge_fraction)
1785    };
1786    let breaks = range_breaks(curve, edge_start, edge_end, forward);
1787    fit_pcurve_on_surface_stations(
1788        surface,
1789        &evaluate_edge,
1790        &breaks,
1791        tolerance,
1792        base_samples,
1793        refinement_rounds,
1794        parameter_cap,
1795    )
1796}
1797
1798/// [`fit_pcurve_on_surface_range_dense`] for the callers that only need the
1799/// curve, accepted or refused on its bar ([`accept_fit`]).
1800#[allow(clippy::too_many_arguments)]
1801pub fn build_pcurve_on_surface_range_dense(
1802    surface: &NurbsSurface,
1803    curve: &NurbsCurve,
1804    edge_start: f64,
1805    edge_end: f64,
1806    forward: bool,
1807    tolerance: f64,
1808    base_samples: usize,
1809    refinement_rounds: usize,
1810    parameter_cap: usize,
1811) -> Result<NurbsCurve, String> {
1812    accept_fit(fit_pcurve_on_surface_range_dense(
1813        surface,
1814        curve,
1815        edge_start,
1816        edge_end,
1817        forward,
1818        tolerance,
1819        base_samples,
1820        refinement_rounds,
1821        parameter_cap,
1822    )?)
1823}
1824
1825/// The fractions of `[edge_start, edge_end]`, walked in the coedge's sense, at
1826/// which `curve` is not C2: interior knots of multiplicity at least its
1827/// degree. A rational-quadratic arc past a quarter turn is two segments joined
1828/// at a double knot, and an interpolant through uniform samples across that
1829/// joint cannot represent the second-derivative jump: on the 2026-09-18 import
1830/// census 1511 of the 2979 arc trims blamed on the fitter had their worst
1831/// miss at fraction 0.49–0.51, the mid double knot, exactly as the fillet's
1832/// corner patch had measured (`blend/track_fit.rs::curve_breaks`, which this
1833/// mirrors for a range).
1834pub(crate) fn range_breaks(
1835    curve: &NurbsCurve,
1836    edge_start: f64,
1837    edge_end: f64,
1838    forward: bool,
1839) -> Vec<f64> {
1840    let mut fractions = Vec::new();
1841    let span = edge_end - edge_start;
1842    if !(span.abs() > 0.0) {
1843        return fractions;
1844    }
1845    let (low, high) = (edge_start.min(edge_end), edge_start.max(edge_end));
1846    let mut index = 0;
1847    while index < curve.knots.len() {
1848        let value = curve.knots[index];
1849        let run = curve.knots[index..]
1850            .iter()
1851            .take_while(|knot| **knot == value)
1852            .count();
1853        if value > low && value < high && run >= curve.degree.max(1) {
1854            let along = (value - edge_start) / span;
1855            fractions.push(if forward { along } else { 1.0 - along });
1856        }
1857        index += run;
1858    }
1859    fractions.sort_by(f64::total_cmp);
1860    fractions
1861}
1862
1863/// The body of [`fit_pcurve_on_surface_range_dense`], parameterized by the
1864/// 3D station the pcurve must pass through at each coedge fraction instead of
1865/// by an edge curve and a range.
1866///
1867/// The station supplier is the whole difference between deriving a pcurve and
1868/// reading one. `fit_pcurve_on_surface_range_dense` supplies points off the
1869/// edge's own 3D curve, so the answer is "the nearest point of the carrier",
1870/// which is ambiguous exactly where a trim is interesting — a seam branch, a
1871/// near-tangential approach, a pole. The STEP importer's supplied-pcurve lane
1872/// supplies points off the VENDOR's stated trim instead, and the ambiguity is
1873/// gone because the vendor resolved it.
1874///
1875/// `stations(fraction)` must be continuous in `fraction` over `[0, 1]` and land
1876/// on (or very near) `surface`; `breaks` are the fractions at which it is not
1877/// C2 ([`range_breaks`]), each of which becomes a station so no kink sits
1878/// between two nodes. Everything else — inversion, branch repair, refinement
1879/// and the measurement — is shared with the derived lane by construction.
1880///
1881/// The SURFACE's knot-line crossings are deliberately not stations here,
1882/// although `blend/track_fit.rs` makes them so. Measured 2026-09-26 over the
1883/// 57-document import corpus: with each crossing bisected on the fraction and
1884/// projected seeded from the left station, 34 bodies refused (19 with a foot
1885/// that misses its global distance rejected) against 15 without the pass —
1886/// every extra refusal a fit stalled around a crossing station, on
1887/// `AssemblyExample-Assembly` and `abc_00000014` among others. The raised
1888/// pass resolves the O(h²) tail across a crossing on its own.
1889///
1890/// # What the fit is measured against, and when it stops
1891///
1892/// The stations' own standoff from the carrier is read FIRST, from the base
1893/// inversions, and the refinement target is the ask plus that standoff
1894/// ([`PcurveFitReport::on_bar`]): a station standing `g` off the surface
1895/// cannot be imaged closer than `g`, so chasing it to the ask alone spends the
1896/// whole budget for nothing (the 2026-09-18 census: 2052 imported pcurves at
1897/// the 513 cap, 1958 of them on a curve that is not on its carrier).
1898///
1899/// Every probe and the final measurement read the carrier through
1900/// `evaluate_extended`: a pcurve riding a periodic seam legitimately carries
1901/// parameters past the domain, and the clamped `evaluate` this loop used until
1902/// 2026-09-26 read every such station as a gross miss — an insert per round
1903/// until the cap, on a trim that was right.
1904///
1905/// The RETURNED curve is measured out of sample — at the quarter points of
1906/// every span between its nodes, where an interpolant is exact at the nodes
1907/// and extremal between — unless its last round probed every span and found
1908/// nothing to insert, in which case that round's maximum already is that
1909/// measurement. A fit that binds at `parameter_cap` or spends its rounds with
1910/// the bar still unmet grows ONCE to [`PCURVE_RAISED_SAMPLES`]
1911/// (deterministic). The report says which exit it took and what it reached;
1912/// the `build_*` wrappers refuse over the bar, this function never does.
1913#[allow(clippy::too_many_arguments)]
1914pub fn fit_pcurve_on_surface_stations(
1915    surface: &NurbsSurface,
1916    stations: &dyn Fn(f64) -> Result<Vec3, String>,
1917    breaks: &[f64],
1918    tolerance: f64,
1919    base_samples: usize,
1920    refinement_rounds: usize,
1921    parameter_cap: usize,
1922) -> Result<PcurveFit, String> {
1923    let evaluate_edge = stations;
1924    let base_samples = base_samples.max(1);
1925    // No fit reaches below the refinement floor: the projector places a foot
1926    // to about `LINEAR_TOLERANCE` (1e-7), so a caller asking for 1e-9 asks for
1927    // more than any station can deliver. Chased to such an ask the fit spends
1928    // its cap and reads its own noise — the direct-edit heals asked 1e-9 on
1929    // stations 9.9e-8 off their carrier and were refused by 8.5e-9
1930    // (2026-09-26). The floor is the ask's floor; the report says what was
1931    // reached against it.
1932    let tolerance = tolerance.max(PCURVE_REFINEMENT_TOLERANCE);
1933    let mut parameters = (0..=base_samples)
1934        .map(|index| index as f64 / base_samples as f64)
1935        .collect::<Vec<_>>();
1936    // A break within the span floor of a station IS that station: two nodes
1937    // closer than that make the interpolant swing between them, and over a
1938    // stretch that short the kink moves the fit by nothing a bar can read.
1939    // `abc_00000011` face #549's curve carries a triple knot 4e-7 before its
1940    // end; as a station it put the cubic 1.594e-2 off between it and the last
1941    // one (measured 2026-09-26), where the file's curve sits 1e-7 off.
1942    for &fraction in breaks {
1943        if !(fraction > 0.0 && fraction < 1.0) {
1944            continue;
1945        }
1946        let at = parameters.partition_point(|parameter| *parameter < fraction);
1947        let near_left = at > 0 && fraction - parameters[at - 1] <= SPAN_FLOOR;
1948        let near_right = at < parameters.len() && parameters[at] - fraction <= SPAN_FLOOR;
1949        if near_left || near_right {
1950            continue;
1951        }
1952        parameters.insert(at, fraction);
1953    }
1954    // The 3D edge point behind each raw inversion sample, kept parallel so the
1955    // continuity repair can re-seed a jumped station from its own footpoint.
1956    let mut edge_points = parameters
1957        .iter()
1958        .map(|fraction| evaluate_edge(*fraction))
1959        .collect::<Result<Vec<_>, _>>()?;
1960    // The stations' standoff from the carrier: the part of every residual no
1961    // pcurve can remove, known before a single probe is spent.
1962    let mut off_surface = 0.0_f64;
1963    let mut raw = Vec::with_capacity(edge_points.len());
1964    let mut gaps = Vec::with_capacity(edge_points.len());
1965    for point in &edge_points {
1966        let (parameter, gap) = invert_checked(surface, *point)?;
1967        off_surface = off_surface.max(gap);
1968        raw.push(parameter);
1969        gaps.push(gap);
1970    }
1971    repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
1972    let mut pcurve = build_interpolant(surface, &raw, &parameters)?;
1973
1974    let mut cap = parameter_cap.max(parameters.len());
1975    let mut rounds = refinement_rounds;
1976    let mut raised = false;
1977    loop {
1978        let mut exit = PcurveFitExit::RoundBudget;
1979        let mut converged_max = None;
1980        for _ in 0..rounds {
1981            if parameters.len() >= cap {
1982                exit = PcurveFitExit::SampleCeiling;
1983                break;
1984            }
1985            // The target is the ASK, bare: a station standing off the carrier
1986            // is chased to the foot it projects to, as the fitter has always
1987            // done. A target of ask + standoff (one landing, 2026-09-26) let
1988            // such a fit stop at its base stations with the interpolant
1989            // wandering between them, folded a molex wall's mesh and put a
1990            // trim of abc_00000023 2.2e-5 off its own parameter; the bar
1991            // below still reads what it reached.
1992            let target = tolerance;
1993            let mut candidates = Vec::new();
1994            let mut round_max = 0.0_f64;
1995            let mut truncated = false;
1996            let mut skipped_narrow = false;
1997            let worst_first = worst_first();
1998            'spans: for index in 0..parameters.len() - 1 {
1999                // The loop's own resolution floor: a span narrower than this
2000                // is not subdivided. Without it the
2001                // raised pass inserted into a vanishing span at a trim's end
2002                // until the cap — `abc_00000011` face #549's last stations sat
2003                // 4e-7 apart with their feet clamped at the patch edge, and the
2004                // cubic through them swung 1.594e-2 off between them (measured
2005                // 2026-09-26). A miss that survives only inside such a span is
2006                // reported as [`PcurveFitExit::SpanFloor`].
2007                if parameters[index + 1] - parameters[index] < SPAN_FLOOR {
2008                    skipped_narrow = true;
2009                    continue;
2010                }
2011                for local in [0.25, 0.5, 0.75] {
2012                    if !worst_first && parameters.len() + candidates.len() >= cap {
2013                        truncated = true;
2014                        break 'spans;
2015                    }
2016                    let fraction =
2017                        parameters[index] + (parameters[index + 1] - parameters[index]) * local;
2018                    let uv = pcurve.evaluate(fraction)?;
2019                    let represented = surface.evaluate_extended(uv.x, uv.y)?;
2020                    let edge_point = evaluate_edge(fraction)?;
2021                    let deviation = represented.sub(edge_point).length();
2022                    round_max = round_max.max(deviation);
2023                    if deviation <= target {
2024                        continue;
2025                    }
2026                    candidates.push((index, fraction, edge_point, uv, deviation));
2027                }
2028            }
2029            // A round with more candidates than the cap has room for spends
2030            // that room on its WORST probes, kept in curve order, rather than
2031            // on the first spans in order: truncating in span order left the
2032            // late spans of an off-carrier ring at their base spacing and its
2033            // image 1.523e-6 off its feet; worst first reaches 5.84e-8 on the
2034            // same 513 samples. An even stride along the curve reached 2.87e-7
2035            // there but diluted the inserts a local miss needs and lost fits
2036            // over the corpus (a 1.3e-5 trim of `abc_00000011` fell back to its
2037            // 4.4e-4 size-ask fit), measured 2026-09-27.
2038            let room = cap.saturating_sub(parameters.len());
2039            if candidates.len() > room {
2040                truncated = true;
2041                let mut order: Vec<usize> = (0..candidates.len()).collect();
2042                order.sort_by(|&a, &b| candidates[b].4.total_cmp(&candidates[a].4).then(a.cmp(&b)));
2043                order.truncate(room);
2044                order.sort_unstable();
2045                candidates = order.into_iter().map(|index| candidates[index]).collect();
2046            }
2047            let mut inserts = Vec::with_capacity(candidates.len());
2048            for (index, fraction, edge_point, uv, _) in candidates {
2049                let (parameter, gap) = invert_near(
2050                    surface,
2051                    edge_point,
2052                    [uv.x, uv.y],
2053                    gaps[index].max(gaps[index + 1]),
2054                )?;
2055                inserts.push((index + 1, fraction, edge_point, parameter, gap));
2056            }
2057            if station_fit_tracing() {
2058                eprintln!(
2059                    "STATION-FIT round: samples={} target={target:.3e} round_max={round_max:.3e} inserts={} truncated={truncated}",
2060                    parameters.len(),
2061                    inserts.len()
2062                );
2063            }
2064            if inserts.is_empty() {
2065                exit = if truncated {
2066                    PcurveFitExit::SampleCeiling
2067                } else if skipped_narrow {
2068                    // Every probed span is within the target; the narrow ones
2069                    // were not probed, and the sweep below measures them.
2070                    PcurveFitExit::SpanFloor
2071                } else {
2072                    converged_max = Some(round_max);
2073                    PcurveFitExit::Converged
2074                };
2075                break;
2076            }
2077            for (index, fraction, edge_point, parameter, gap) in inserts.into_iter().rev() {
2078                parameters.insert(index, fraction);
2079                edge_points.insert(index, edge_point);
2080                raw.insert(index, parameter);
2081                gaps.insert(index, gap);
2082                off_surface = off_surface.max(gap);
2083            }
2084            // Inserts are independent global inversions too — re-run the repair
2085            // so a fold-flip introduced mid-refinement cannot poison the next
2086            // round's deviation interpolant.
2087            repair_branch_jumps(surface, &edge_points, &mut raw, tolerance)?;
2088            pcurve = build_interpolant(surface, &raw, &parameters)?;
2089            if truncated {
2090                exit = PcurveFitExit::SampleCeiling;
2091                break;
2092            }
2093        }
2094        let residual = match converged_max {
2095            Some(measured) => measured,
2096            None => {
2097                station_residual(surface, evaluate_edge, &pcurve, &parameters, tolerance, &mut off_surface)?
2098            }
2099        };
2100        if station_fit_tracing() {
2101            eprintln!(
2102                "STATION-FIT pass raised={raised} exit={exit:?} samples={} residual={residual:.3e} off_surface={off_surface:.3e}",
2103                parameters.len()
2104            );
2105            if residual > tolerance + off_surface && (exit == PcurveFitExit::Converged || raised) {
2106                // Where the miss sits, and the uv track around it: the trace a
2107                // refusal needs to be attributed to a fold, a seam or the file.
2108                let (mut worst, mut at) = (0.0f64, 0usize);
2109                for (index, span) in parameters.windows(2).enumerate() {
2110                    let fraction = 0.5 * (span[0] + span[1]);
2111                    let uv = pcurve.evaluate(fraction)?;
2112                    let deviation = surface.evaluate_extended(uv.x, uv.y)?.sub(evaluate_edge(fraction)?).length();
2113                    if deviation > worst {
2114                        worst = deviation;
2115                        at = index;
2116                    }
2117                }
2118                let ([u0, u1], [v0, v1]) = surface_domains(surface)?;
2119                let (closed_u, closed_v) = surface_closedness(surface)?;
2120                eprintln!(
2121                    "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 {:?}",
2122                    at + 1,
2123                    surface.degree_u,
2124                    surface.degree_v,
2125                    surface.control_points.len(),
2126                    surface.control_points[0].len(),
2127                    surface.analytic().map(|_| "yes").unwrap_or("no")
2128                );
2129                {
2130                    let fraction = 0.5 * (parameters[at] + parameters[at + 1]);
2131                    let uv = pcurve.evaluate(fraction)?;
2132                    let image = surface.evaluate_extended(uv.x, uv.y)?;
2133                    let station = evaluate_edge(fraction)?;
2134                    let foot = project_point_to_surface(surface, station)?;
2135                    let seeded = project_point_to_surface_seeded(surface, station, raw[at][0], raw[at][1])?;
2136                    eprintln!(
2137                        "    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})",
2138                        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
2139                    );
2140                }
2141                let lo = at.saturating_sub(3);
2142                let hi = (at + 4).min(parameters.len() - 1);
2143                for index in lo..=hi {
2144                    eprintln!(
2145                        "    station {index}: fraction {:.6} uv ({:.6}, {:.6}) point ({:.4}, {:.4}, {:.4})",
2146                        parameters[index], raw[index][0], raw[index][1], edge_points[index].x, edge_points[index].y, edge_points[index].z
2147                    );
2148                }
2149            }
2150        }
2151        if exit == PcurveFitExit::SpanFloor && residual <= tolerance + off_surface {
2152            exit = PcurveFitExit::Converged;
2153        }
2154        if exit != PcurveFitExit::Converged
2155            && !raised
2156            && residual > tolerance + off_surface
2157            && cap < PCURVE_RAISED_SAMPLES
2158        {
2159            raised = true;
2160            cap = PCURVE_RAISED_SAMPLES;
2161            // The raised pass is bounded by SAMPLES, not rounds: a kink
2162            // converges O(h²), a round per halving, and the three rounds of
2163            // the first pass are what left the 2026-09-18 census's fits over
2164            // their ask with budget to spare (RoundBudget at 158–400 of 2000
2165            // samples, measured 2026-09-26). Forty rounds cover every halving
2166            // the cap admits; a fit that is not converging stalls out first.
2167            rounds = RAISED_ROUNDS;
2168            continue;
2169        }
2170        return Ok(PcurveFit {
2171            curve: pcurve,
2172            report: PcurveFitReport {
2173                residual,
2174                floor: tolerance,
2175                samples: parameters.len(),
2176                exit,
2177                off_surface,
2178                raised,
2179            },
2180        });
2181    }
2182}
2183
2184/// [`fit_pcurve_on_surface_stations`] for the callers that only need the
2185/// curve, accepted or refused on its bar ([`accept_fit`]).
2186pub fn build_pcurve_on_surface_stations(
2187    surface: &NurbsSurface,
2188    stations: &dyn Fn(f64) -> Result<Vec3, String>,
2189    breaks: &[f64],
2190    tolerance: f64,
2191    base_samples: usize,
2192    refinement_rounds: usize,
2193    parameter_cap: usize,
2194) -> Result<NurbsCurve, String> {
2195    accept_fit(fit_pcurve_on_surface_stations(
2196        surface,
2197        stations,
2198        breaks,
2199        tolerance,
2200        base_samples,
2201        refinement_rounds,
2202        parameter_cap,
2203    )?)
2204}
2205
2206/// The largest 3D distance between `pcurve`'s image and its stations at the
2207/// quarter points of every span of `parameters` — out of sample by
2208/// construction, since the interpolant is exact at its nodes. Unclamped
2209/// evaluations; a station read over the running bar is inverted so that
2210/// `off_surface` covers the standoff AT the stations the residual is taken at,
2211/// not only at the nodes (a curve lifted off its plane by a bump stands
2212/// further off between two nodes than at either).
2213fn station_residual(
2214    surface: &NurbsSurface,
2215    stations: &dyn Fn(f64) -> Result<Vec3, String>,
2216    pcurve: &NurbsCurve,
2217    parameters: &[f64],
2218    tolerance: f64,
2219    off_surface: &mut f64,
2220) -> Result<f64, String> {
2221    let mut worst = 0.0_f64;
2222    for span in parameters.windows(2) {
2223        for local in [0.25, 0.5, 0.75] {
2224            let fraction = span[0] + (span[1] - span[0]) * local;
2225            let uv = pcurve.evaluate(fraction)?;
2226            let represented = surface.evaluate_extended(uv.x, uv.y)?;
2227            let station = stations(fraction)?;
2228            let deviation = represented.sub(station).length();
2229            worst = worst.max(deviation);
2230            if deviation > tolerance + *off_surface {
2231                *off_surface = off_surface.max(invert_checked(surface, station)?.1);
2232            }
2233        }
2234    }
2235    Ok(worst)
2236}
2237