Skip to main content

brep_kernel/geometry/
projection.rs

1use crate::curve::interior_knots;
2use crate::{NurbsCurve, NurbsSurface, Vec3, Vec4};
3use serde::Serialize;
4
5const EPSILON: f64 = crate::tolerance::FP_FLOOR;
6const LINEAR_TOLERANCE: f64 = crate::tolerance::DEFAULT_MODEL;
7const MAX_NEWTON_ITERATIONS: usize = 50;
8
9#[derive(Clone, Copy, Debug, Serialize)]
10pub struct CurveProjection {
11    pub u: f64,
12    pub point: Vec3,
13    pub distance: f64,
14}
15
16#[derive(Clone, Copy, Debug, Serialize)]
17pub struct SurfaceProjection {
18    pub u: f64,
19    pub v: f64,
20    pub point: Vec3,
21    pub distance: f64,
22}
23
24pub(crate) fn fit_parameter(value: f64, minimum: f64, maximum: f64, closed: bool) -> f64 {
25    if closed {
26        let period = maximum - minimum;
27        (value - minimum).rem_euclid(period) + minimum
28    } else {
29        value.clamp(minimum, maximum)
30    }
31}
32
33/// Escape hatch for tamper-verification: `BREP_PROJECTION_WITNESS=0` restores
34/// the pre-2026-09-14 answer — the Newton walk from the nearest seed and
35/// nothing else — so a fixture that depends on the rescue can be shown to
36/// depend on it. Read once; the projector is called about a million times in
37/// one boolean.
38fn witness_rescue_enabled() -> bool {
39    switch!("BREP_PROJECTION_WITNESS", true)
40}
41
42/// Squared distance from `point` to the SEGMENT `a`-`b`.
43///
44/// The seed sweep's consecutive samples are the segments of a polyline that
45/// approximates the curve, and this is how far that polyline passes from the
46/// query — the witness the rescue below reads.
47fn point_segment_distance_squared(point: Vec3, a: Vec3, b: Vec3) -> f64 {
48    point.distance_to_segment_squared(a, b)
49}
50
51/// One Newton refinement of the foot parameter, seeded at `seed`.
52///
53/// `best_u` / `best_distance_squared` carry the best answer found SO FAR and
54/// every iterate is compared against them, so a run that diverges or stalls can
55/// only leave the answer where it was. The returned parameter is where the
56/// iteration exited, which the caller still has to evaluate: the stall branch
57/// takes its last step without evaluating it.
58#[allow(clippy::too_many_arguments)]
59fn curve_newton(
60    curve: &NurbsCurve,
61    point: Vec3,
62    seed: f64,
63    start: f64,
64    end: f64,
65    closed: bool,
66    best_u: &mut f64,
67    best_distance_squared: &mut f64,
68) -> Result<f64, String> {
69    let (last, nearest, squared) = curve_foot_newton_state(curve, point, seed, [start, end],
70        CurveFootPolicy {
71            domain: if closed { FootDomain::Wrap } else { FootDomain::Clamp },
72            iterations: MAX_NEWTON_ITERATIONS,
73            slope_guard: EPSILON,
74            step_limit: (end - start) / 4.0,
75            parameter_tolerance: 1e-15 * (end - start),
76            mode: FootMode::Witness,
77        })?;
78    if squared < *best_distance_squared {
79        *best_distance_squared = squared;
80        *best_u = nearest;
81    }
82    Ok(last)
83}
84
85/// The parameter in `[low, high]` nearest `point`, by golden-section search on
86/// the SQUARED distance.
87///
88/// Reads no derivative, so it is the one lane that still works where the
89/// tangent vanishes, and it can never return a point farther than the bracket's
90/// own ends. It assumes ONE minimum inside the bracket — true of a seed
91/// interval, which is one sample step of a single knot span wide.
92fn bracketed_nearest(curve: &NurbsCurve, point: Vec3, low: f64, high: f64) -> Result<f64, String> {
93    // 1/φ. 64 contractions take a bracket to 2.5e-14 of its width, below the
94    // parameter resolution the Newton polish that follows needs from a seed.
95    const CONTRACTION: f64 = 0.618_033_988_749_894_9;
96    const CONTRACTIONS: usize = 64;
97    let width = high - low;
98    if !(width > 0.0) {
99        return Ok(low);
100    }
101    let (mut low, mut high) = (low, high);
102    let mut c = high - CONTRACTION * (high - low);
103    let mut d = low + CONTRACTION * (high - low);
104    let mut value_c = curve.evaluate(c)?.sub(point).length_squared();
105    let mut value_d = curve.evaluate(d)?.sub(point).length_squared();
106    for _ in 0..CONTRACTIONS {
107        if high - low <= 1e-15 * width {
108            break;
109        }
110        if value_c < value_d {
111            high = d;
112            d = c;
113            value_d = value_c;
114            c = high - CONTRACTION * (high - low);
115            value_c = curve.evaluate(c)?.sub(point).length_squared();
116        } else {
117            low = c;
118            c = d;
119            value_c = value_d;
120            d = low + CONTRACTION * (high - low);
121            value_d = curve.evaluate(d)?.sub(point).length_squared();
122        }
123    }
124    Ok((low + high) * 0.5)
125}
126
127pub fn project_point_to_curve(curve: &NurbsCurve, point: Vec3) -> Result<CurveProjection, String> {
128    if projection_memo_enabled() {
129        if let Some(key) = straight_projection_key(curve, point) {
130            if let Some(hit) = PROJECTION_MEMO.with(|memo| memo.borrow().get(&key).copied()) {
131                return Ok(hit);
132            }
133            let projection = project_point_to_curve_uncached(curve, point)?;
134            PROJECTION_MEMO.with(|memo| {
135                let mut memo = memo.borrow_mut();
136                if memo.len() >= PROJECTION_MEMO_ENTRIES {
137                    memo.clear();
138                }
139                memo.insert(key, projection);
140            });
141            return Ok(projection);
142        }
143    }
144    project_point_to_curve_uncached(curve, point)
145}
146
147/// PROJECTION MEMO (lane O row 22, 2026-10-29; on by default since the batch 6
148/// promotion, `BREP_PROJECTION_MEMO=0` projects every call). The projector is a pure function of the curve's net and the
149/// point. On the D2 outward shells every one of its 512 calls a shell call
150/// projects onto a straight two-point edge, and 209 of them repeat a call
151/// already made (the section's pierces, its ambiguity reads and the endpoint
152/// exchange re-project the same points; counted, cad-bot). A straight curve's
153/// whole net and the point make a 16-word key compared exactly, so a hit
154/// returns the very answer the projector computed.
155const PROJECTION_MEMO_ENTRIES: usize = 4096;
156
157thread_local! {
158    static PROJECTION_MEMO: std::cell::RefCell<rustc_hash::FxHashMap<[u64; 16], CurveProjection>> = Default::default();
159}
160
161fn straight_projection_key(curve: &NurbsCurve, point: Vec3) -> Option<[u64; 16]> {
162    if curve.degree != 1 || curve.control_points.len() != 2 || curve.knots.len() != 4 {
163        return None;
164    }
165    let [a, b] = [curve.control_points[0], curve.control_points[1]];
166    let k = &curve.knots;
167    Some([
168        1, k[0].to_bits(), k[1].to_bits(), k[2].to_bits(), k[3].to_bits(),
169        a.x.to_bits(), a.y.to_bits(), a.z.to_bits(), a.w.to_bits(),
170        b.x.to_bits(), b.y.to_bits(), b.z.to_bits(), b.w.to_bits(),
171        point.x.to_bits(), point.y.to_bits(), point.z.to_bits(),
172    ])
173}
174
175fn projection_memo_enabled() -> bool {
176    switch!("BREP_PROJECTION_MEMO", true)
177}
178
179
180fn project_point_to_curve_uncached(curve: &NurbsCurve, point: Vec3) -> Result<CurveProjection, String> {
181    let [start, end] = curve.domain()?;
182    let mut breaks = vec![start];
183    breaks.extend(interior_knots(&curve.knots, curve.degree));
184    breaks.push(end);
185    let samples_per_span = 4usize.max(curve.degree + 2);
186    let mut best_u = start;
187    let mut best_distance_squared = f64::INFINITY;
188    // The sweep's FIRST sample is exactly `start` and its LAST is exactly `end`
189    // (`breaks` opens at `start` and closes at `end`, and index 0 / index
190    // `samples_per_span` hit a span's own ends), so closure costs no extra
191    // evaluation: keep those two points and compare them.
192    let mut start_point = None;
193    let mut end_point = None;
194    // The sampled polyline's own nearest approach, and the seed interval that
195    // achieves it. Squared throughout, so the sweep pays no square root.
196    let mut previous: Option<(f64, Vec3)> = None;
197    let mut witness_distance_squared = f64::INFINITY;
198    let mut witness_low = start;
199    let mut witness_high = start;
200    for pair in breaks.windows(2) {
201        for index in 0..=samples_per_span {
202            let parameter = pair[0] + (pair[1] - pair[0]) * index as f64 / samples_per_span as f64;
203            let sample = curve.evaluate(parameter)?;
204            if start_point.is_none() {
205                start_point = Some(sample);
206            }
207            end_point = Some(sample);
208            let distance_squared = sample.sub(point).length_squared();
209            if distance_squared < best_distance_squared {
210                best_distance_squared = distance_squared;
211                best_u = parameter;
212            }
213            if let Some((previous_parameter, previous_sample)) = previous {
214                let segment = point_segment_distance_squared(point, previous_sample, sample);
215                if segment < witness_distance_squared {
216                    witness_distance_squared = segment;
217                    witness_low = previous_parameter;
218                    witness_high = parameter;
219                }
220            }
221            previous = Some((parameter, sample));
222        }
223    }
224    // A closed curve's domain is PERIODIC, so Newton must wrap across the seam
225    // rather than stop at it. Clamping made the projector return the seam for
226    // any query whose foot sits within half a seed spacing of it: `start` and
227    // `end` are the same POINT, the sweep keeps the first of equal distances and
228    // so seeds at `start`, and a Newton step toward a foot just BEFORE the seam
229    // is then clamped back to `start`, reads as a zero-length step, and breaks.
230    // Measured on a rational-quadratic circle of radius 20: 4.796 mm out, for a
231    // point taken off the curve itself. Swapping the sweep's tie-break to keep
232    // the LAST equal sample does not fix this — it mirrors it, returning the
233    // same 4.796 mm for queries just AFTER the seam. The surface projector has
234    // always wrapped (`fit_parameter` at the `project_point_to_surface` Newton
235    // step); this is the curve side catching up.
236    const CLOSED_SEAM_TOLERANCE: f64 = 1e-6;
237    let closed = match (start_point, end_point) {
238        (Some(first), Some(last)) => first.sub(last).length() <= CLOSED_SEAM_TOLERANCE,
239        _ => false,
240    };
241
242    let parameter = curve_newton(
243        curve,
244        point,
245        best_u,
246        start,
247        end,
248        closed,
249        &mut best_u,
250        &mut best_distance_squared,
251    )?;
252    for candidate in [start, end, parameter] {
253        let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
254        if distance_squared < best_distance_squared {
255            best_distance_squared = distance_squared;
256            best_u = candidate;
257        }
258    }
259    // AN ANSWER THE SEED POLYLINE REFUTES IS NOT AN ANSWER. The sweep's
260    // consecutive samples are a polyline that approximates the curve, so the
261    // nearest that polyline passes to the query is a witness: the curve
262    // demonstrably comes about that close. When the answer is FARTHER than the
263    // witness by more than a factor of two, Newton converged in the wrong basin
264    // and the seed interval the witness names is where the foot really is.
265    //
266    // Two different defects reach here, both of them "a point ON the curve does
267    // not project to its own parameter", and both measured on real geometry:
268    //
269    // * A STATIONARY seed. Newton is a first-order method and
270    //   `f = C'(t) · (C(t) - P)` vanishes at a stationary parameter for EVERY
271    //   `P`, so the iteration reads convergence there whatever the point is, and
272    //   the step it would otherwise take is `-f/f''`, a second-order quantity
273    //   whose sign has nothing to do with where the foot is. The 2026-09-14
274    //   herringbone document is full of them: a flank pcurve is a two-span cubic
275    //   whose first two control points COINCIDE (an involute's speed is
276    //   `r_base * t`, so its Hermite start tangent is exactly zero), the sweep's
277    //   nearest sample to a query near the start IS that cusp, and a point lying
278    //   exactly on the curve at t = 0.125 answered t = 0, 2.159e-2 out in uv.
279    //   That is the wrong answer `build_loop`'s partial-run rescue declined on.
280    // * A seed grid coarser in ONE span than in the rest. An imported pcurve
281    //   from `abc_00000013.step` carries 511 interior knots whose first span is
282    //   eighty times the width of the median, and five samples per span leave
283    //   its samples 4.6e-3 of uv apart — so a point on the curve at fraction
284    //   0.01 read as 3.969e-3 from a DIFFERENT branch at fraction 0.99, which
285    //   was genuinely nearer than any sample of its own.
286    //
287    // The witness refuted both by 71x and by 293000x. Every update below is a
288    // strict improvement on the distance already found, so a rescue that finds
289    // nothing better leaves the answer exactly where it was.
290    const WITNESS_FACTOR: f64 = 2.0;
291    if witness_distance_squared * (WITNESS_FACTOR * WITNESS_FACTOR) < best_distance_squared
292        && witness_rescue_enabled()
293    {
294        let bracketed = bracketed_nearest(curve, point, witness_low, witness_high)?;
295        let polished = curve_newton(
296            curve,
297            point,
298            bracketed,
299            start,
300            end,
301            closed,
302            &mut best_u,
303            &mut best_distance_squared,
304        )?;
305        for candidate in [bracketed, polished] {
306            let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
307            if distance_squared < best_distance_squared {
308                best_distance_squared = distance_squared;
309                best_u = candidate;
310            }
311        }
312    }
313    let projected = curve.evaluate(best_u)?;
314    Ok(CurveProjection {
315        u: best_u,
316        point: projected,
317        distance: projected.sub(point).length(),
318    })
319}
320
321fn project_linear_revolution(
322    surface: &NurbsSurface,
323    point: Vec3,
324) -> Result<Option<SurfaceProjection>, String> {
325    let rows = &surface.control_points;
326    if surface.degree_u != 2
327        || surface.degree_v != 1
328        || rows.len() < 5
329        || rows[0].len() != 2
330        || !surface.closed_directions()?.0
331    {
332        return Ok(None);
333    }
334    let base = rows[0][0].point()?.add(rows[4][0].point()?).scale(0.5);
335    let top = rows[0][1].point()?.add(rows[4][1].point()?).scale(0.5);
336    let axis_vector = top.sub(base);
337    let height = axis_vector.length();
338    if height <= EPSILON {
339        return Ok(None);
340    }
341    let axis = axis_vector.normalized()?;
342    let [v0, v1] = surface.domain_v()?;
343    let axial = point.sub(base).dot(axis).clamp(0.0, height);
344    let v = v0 + (v1 - v0) * axial / height;
345    let circle = surface.iso_curve_v(v)?;
346    let projection = project_point_to_curve(&circle, point)?;
347    let [u0, u1] = surface.domain_u()?;
348    let u = projection.u.clamp(u0, u1);
349    let projected = surface.evaluate(u, v)?;
350    Ok(Some(SurfaceProjection {
351        u,
352        v,
353        point: projected,
354        distance: projected.sub(point).length(),
355    }))
356}
357
358#[derive(Clone, Copy)]
359struct NewtonResult {
360    u: f64,
361    v: f64,
362    distance_squared: f64,
363    converged: bool,
364}
365
366fn surface_newton(
367    surface: &NurbsSurface,
368    point: Vec3,
369    seed_u: f64,
370    seed_v: f64,
371    domains: [f64; 4],
372    closed_u: bool,
373    closed_v: bool,
374) -> Result<NewtonResult, String> {
375    surface_newton_impl(surface, point, seed_u, seed_v, domains, closed_u, closed_v,
376        switch!("BREP_PROJECTION_LAZY_DERIVATIVES", false))
377}
378
379/// Most seeded verification queries are already on the carrier. Row zero
380/// uses the same basis and rational arithmetic at counts zero and two; read
381/// it first, preserving the best-point and distance exit, and compute the
382/// remaining derivatives only when Newton actually needs to take a step.
383fn surface_newton_impl(
384    surface: &NurbsSurface,
385    point: Vec3,
386    seed_u: f64,
387    seed_v: f64,
388    domains: [f64; 4],
389    closed_u: bool,
390    closed_v: bool,
391    lazy: bool,
392) -> Result<NewtonResult, String> {
393    let [u0, u1, v0, v1] = domains;
394    let mut u = seed_u;
395    let mut v = seed_v;
396    let mut converged = false;
397    let mut best = NewtonResult {
398        u,
399        v,
400        distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
401        converged,
402    };
403    for iteration in 0..MAX_NEWTON_ITERATIONS {
404        crate::imprint::work_profile::add(|c| c.newton_iterations += 1);
405        let derivatives = surface.derivatives_small(u, v, if lazy { 0 } else { 2 })?;
406        let residual = derivatives[0][0].sub(point);
407        let distance_squared = residual.length_squared();
408        if distance_squared < best.distance_squared {
409            best.u = u;
410            best.v = v;
411            best.distance_squared = distance_squared;
412        }
413        let residual_length = distance_squared.sqrt();
414        if residual_length <= LINEAR_TOLERANCE {
415            crate::imprint::work_profile::add(|c| { c.newton_unused += 1; c.newton_unused_first += u64::from(iteration == 0); });
416            converged = true;
417            break;
418        }
419        let derivatives = if lazy { surface.derivatives_small(u, v, 2)? } else { derivatives };
420        let f = derivatives[1][0].dot(residual);
421        let g = derivatives[0][1].dot(residual);
422        let tangent_u_length = derivatives[1][0].length();
423        let tangent_v_length = derivatives[0][1].length();
424        let cosine_u = if tangent_u_length * residual_length <= EPSILON {
425            0.0
426        } else {
427            f.abs() / (tangent_u_length * residual_length)
428        };
429        let cosine_v = if tangent_v_length * residual_length <= EPSILON {
430            0.0
431        } else {
432            g.abs() / (tangent_v_length * residual_length)
433        };
434        if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
435            converged = true;
436            break;
437        }
438        let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
439        let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
440        let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
441        let determinant = j00 * j11 - j01 * j01;
442        if determinant.abs() <= EPSILON {
443            break;
444        }
445        let mut du = (-f * j11 + g * j01) / determinant;
446        let mut dv = (-g * j00 + f * j01) / determinant;
447        du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
448        dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
449        let next_u = fit_parameter(u + du, u0, u1, closed_u);
450        let next_v = fit_parameter(v + dv, v0, v1, closed_v);
451        let stalled =
452            (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
453        u = next_u;
454        v = next_v;
455        if stalled {
456            break;
457        }
458    }
459    let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
460    if final_distance_squared < best.distance_squared {
461        best.u = u;
462        best.v = v;
463        best.distance_squared = final_distance_squared;
464    }
465    best.converged = converged;
466    Ok(best)
467}
468
469/// The point of `surface` nearest `point`, over the surface's whole domain.
470///
471/// Analytic carriers answer in closed form; every other surface goes through
472/// `project_point_to_surface_general`, which says what it guarantees.
473/// `#[track_caller]` names the call site in the `BREP_PROJECTION_AUDIT` log.
474#[track_caller]
475pub fn project_point_to_surface(
476    surface: &NurbsSurface,
477    point: Vec3,
478) -> Result<SurfaceProjection, String> {
479    crate::imprint::work_profile::add(|c| c.projection_global += 1);
480    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_PROJECT, surface);
481    // Recognized analytic carriers (plane / cylinder / cone / sphere /
482    // torus) have exact closed-form projections in the surface's own
483    // rational parameterization — skip grid seeding and Newton entirely.
484    if let Some(analytic) = surface.analytic() {
485        if let Some(projection) = analytic.project(surface, point) {
486            return Ok(projection);
487        }
488    }
489    if let Some(ellipsoid) = surface.affine_sphere() {
490        if let Some(projection) = ellipsoid.project(surface, point) {
491            return Ok(projection);
492        }
493    }
494    project_point_to_surface_general(surface, point)
495}
496
497/// Project a point onto a surface starting Newton from an explicit `(u, v)`
498/// guess, WITHOUT the global grid seed. This is a footpoint refiner for
499/// continuity-preserving curve-on-surface tracing: seeding each edge sample
500/// from its neighbour's parameters keeps the fit on ONE branch of a surface
501/// that folds back over the small trimmed patch, where an independent global
502/// search would snap to whichever fold is momentarily closest and tear the
503/// pcurve into a self-crossing zig-zag. The caller compares the returned
504/// `distance` against the global answer and only adopts this result when it is
505/// geometrically just as valid, so a bad seed can never make a fit worse.
506pub fn project_point_to_surface_seeded(
507    surface: &NurbsSurface,
508    point: Vec3,
509    seed_u: f64,
510    seed_v: f64,
511) -> Result<SurfaceProjection, String> {
512    crate::imprint::work_profile::add(|c| c.projection_seeded += 1);
513    let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
514    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_SEEDED, surface);
515    // An ellipsoid's foot is unique off its medial axis: the closed form IS
516    // the branch answer, as it is for the analytic carriers at the seeded
517    // entry (`project_point_to_surface_from_seed`).
518    if let Some(ellipsoid) = surface.affine_sphere() {
519        if let Some(projection) = ellipsoid.project(surface, point) {
520            return Ok(projection);
521        }
522    }
523    let [u0, u1] = surface.domain_u()?;
524    let [v0, v1] = surface.domain_v()?;
525    let (closed_u, closed_v) = surface.closed_directions()?;
526    let result = surface_newton(
527        surface,
528        point,
529        seed_u,
530        seed_v,
531        [u0, u1, v0, v1],
532        closed_u,
533        closed_v,
534    )?;
535    let projected = surface.evaluate(result.u, result.v)?;
536    Ok(SurfaceProjection {
537        u: result.u,
538        v: result.v,
539        point: projected,
540        distance: projected.sub(point).length(),
541    })
542}
543
544/// The foot of `point` on the branch of `seed`: the closed form on an analytic
545/// carrier, otherwise Newton from the caller's own `(u, v)`
546/// ([`project_point_to_surface_seeded`]), WITHOUT the global search.
547///
548/// **Which callers.** One that already holds a foot on this surface from a
549/// geometrically adjacent query and whose output needs continuity with it: a
550/// pcurve's previous sample, a march's previous station, a Newton's previous
551/// iterate. [`project_point_to_surface`] answers a different question — where
552/// on the WHOLE surface the point is nearest — and on a carrier that comes back
553/// near itself, or for an off-surface query about as far from two sheets, that
554/// answer is a foot on another branch: a pcurve sample on the wrong sheet, an
555/// iterate that jumps. A caller with no such foot (a vertex, a probe, the first
556/// station) asks the nearest-point question and calls the projector.
557///
558/// **An analytic carrier** answers in closed form, as the projector does. Its
559/// only places with two equally valid feet are its seam, its poles and its focal
560/// set (axis, centre, tube circle), and the closed form answers those exactly as
561/// the nearest-point query always has; no march on one changes.
562///
563/// **What it does not promise.** It is a local answer: its distance is an upper
564/// bound, and a Newton that stalls returns its best iterate without saying so.
565/// Every caller keeps its own on-surface check.
566///
567/// **Instruments**, each read once. `BREP_PROJECTION_FROM_SEED=0` returns
568/// [`project_point_to_surface`]'s answer at every caller (the reading before
569/// 2026-09-15, for a same-binary A/B); `BREP_PROJECTION_FROM_SEED_SITES=<a,b,…>`
570/// keeps the seeded answer only at the call sites whose `file:line` contains
571/// one of the substrings. Under `BREP_PROJECTION_AUDIT=<dir>` every query whose
572/// seeded foot lies farther than the on-surface floor from the nearest point's
573/// foot appends one line to `<dir>/projection-seed-audit-<pid>.tsv`. That
574/// comparand is the nearest-point projector under the same caller, so its own
575/// replacements land in `projection-audit-<pid>.tsv` at this site's line: they
576/// are the audit's, not an answer the caller took.
577#[track_caller]
578pub(crate) fn project_point_to_surface_from_seed(
579    surface: &NurbsSurface,
580    point: Vec3,
581    seed: [f64; 2],
582) -> Result<SurfaceProjection, String> {
583    crate::imprint::work_profile::add(|c| c.projection_from_seed += 1);
584    let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
585    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_SEED, surface);
586    if let Some(analytic) = surface.analytic() {
587        if let Some(projection) = analytic.project(surface, point) {
588            return Ok(projection);
589        }
590    }
591    if let Some(ellipsoid) = surface.affine_sphere() {
592        if let Some(projection) = ellipsoid.project(surface, point) {
593            return Ok(projection);
594        }
595    }
596    let caller = std::panic::Location::caller();
597    if !seeded_answer_admitted(caller) {
598        return project_point_to_surface_general(surface, point);
599    }
600    let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
601    if seed_audit_enabled() {
602        let nearest = project_point_to_surface_general(surface, point)?;
603        if seeded.point.sub(nearest.point).length() > LINEAR_TOLERANCE {
604            seed_audit_record(caller, surface, seed, &seeded, &nearest);
605        }
606    }
607    Ok(seeded)
608}
609
610/// `BREP_PROJECTION_FROM_SEED=0` / `BREP_PROJECTION_FROM_SEED_SITES`: see
611/// [`project_point_to_surface_from_seed`].
612fn seeded_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
613    static SITES: std::sync::OnceLock<Option<Option<Vec<String>>>> = std::sync::OnceLock::new();
614    let sites = SITES.get_or_init(|| {
615        if std::env::var("BREP_PROJECTION_FROM_SEED").as_deref() == Ok("0") {
616            return None;
617        }
618        Some(std::env::var("BREP_PROJECTION_FROM_SEED_SITES").ok().map(|value| {
619            value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect()
620        }))
621    });
622    match sites {
623        None => false,
624        Some(None) => true,
625        Some(Some(only)) => {
626            let location = format!("{}:{}", caller.file(), caller.line());
627            only.iter().any(|site| location.contains(site.as_str()))
628        }
629    }
630}
631
632fn seed_audit_enabled() -> bool {
633    debug_flag!("BREP_PROJECTION_AUDIT")
634}
635
636/// One line per seeded answer whose foot is not the nearest point's: the call
637/// site, both distances, the seed, both feet's `(u, v)`, the separation of the
638/// two feet and the surface's shape.
639fn seed_audit_record(
640    caller: &std::panic::Location<'_>,
641    surface: &NurbsSurface,
642    seed: [f64; 2],
643    seeded: &SurfaceProjection,
644    nearest: &SurfaceProjection,
645) {
646    use std::io::Write;
647    static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
648        std::sync::OnceLock::new();
649    let sink = SINK.get_or_init(|| {
650        let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
651        let path = std::path::Path::new(&directory)
652            .join(format!("projection-seed-audit-{}.tsv", std::process::id()));
653        std::fs::OpenOptions::new()
654            .create(true)
655            .append(true)
656            .open(path)
657            .ok()
658            .map(std::sync::Mutex::new)
659    });
660    if let Some(file) = sink {
661        if let Ok(mut file) = file.lock() {
662            let _ = writeln!(
663                file,
664                "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.6e}\t{}x{} cp {}x{}",
665                caller.file(),
666                caller.line(),
667                seeded.distance,
668                nearest.distance,
669                seed[0],
670                seed[1],
671                seeded.u,
672                seeded.v,
673                nearest.u,
674                nearest.v,
675                seeded.point.sub(nearest.point).length(),
676                surface.degree_u,
677                surface.degree_v,
678                surface.control_points.len(),
679                surface.control_points.first().map(Vec::len).unwrap_or(0),
680            );
681        }
682    }
683}
684
685/// The Newton seed grid for general projection: (u, v, point) samples over
686/// every knot span. A pure function of the surface, cached on it — building
687/// the grid costs hundreds of evaluations and projection is the hottest
688/// entry point in the kernel.
689fn projection_seed_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
690    if let Some(grid) = surface.projection_grid.get() {
691        return Ok(grid);
692    }
693    let [u0, u1] = surface.domain_u()?;
694    let [v0, v1] = surface.domain_v()?;
695    let (closed_u, closed_v) = surface.closed_directions()?;
696    let mut breaks_u = vec![u0];
697    breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
698    breaks_u.push(u1);
699    let mut breaks_v = vec![v0];
700    breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
701    breaks_v.push(v1);
702    let samples_u = if closed_u {
703        8usize.max(surface.degree_u * 4)
704    } else {
705        3usize.max(surface.degree_u + 1)
706    };
707    let samples_v = if closed_v {
708        8usize.max(surface.degree_v * 4)
709    } else {
710        3usize.max(surface.degree_v + 1)
711    };
712    let mut grid = Vec::new();
713    for u_pair in breaks_u.windows(2) {
714        for v_pair in breaks_v.windows(2) {
715            for i in 0..=samples_u {
716                for j in 0..=samples_v {
717                    let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
718                    let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
719                    grid.push((u, v, surface.evaluate(u, v)?));
720                }
721            }
722        }
723    }
724    Ok(surface.projection_grid.get_or_init(|| SampleGrid::new(grid)))
725}
726
727/// The dense fallback grid for degenerate general projection: (u, v, point)
728/// over a `(count_u+1) × (count_v+1)` lattice with `count_u/count_v` derived
729/// from the surface's knot spans. A pure function of the surface, cached on
730/// it — on metre-unit-mm parts the degenerate fallback fires ~1.7M times and
731/// each re-evaluated all ~561 lattice points; caching them makes the argmin a
732/// pure distance scan over reused points (bit-identical seed).
733fn projection_dense_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
734    if let Some(grid) = surface.projection_dense_grid.get() {
735        return Ok(grid);
736    }
737    let [u0, u1] = surface.domain_u()?;
738    let [v0, v1] = surface.domain_v()?;
739    let mut breaks_u = vec![u0];
740    breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
741    breaks_u.push(u1);
742    let mut breaks_v = vec![v0];
743    breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
744    breaks_v.push(v1);
745    let count_u = 32usize.max((breaks_u.len() - 1) * 8);
746    let count_v = 16usize.max((breaks_v.len() - 1) * 8);
747    let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
748    for i in 0..=count_u {
749        for j in 0..=count_v {
750            let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
751            let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
752            grid.push((u, v, surface.evaluate(u, v)?));
753        }
754    }
755    Ok(surface.projection_dense_grid.get_or_init(|| SampleGrid::new(grid)))
756}
757
758/// Samples per block of a [`SampleGrid`].
759const SAMPLE_BLOCK: usize = 32;
760
761/// Runs per group: the scan's second level, a box over [`GROUP_BLOCKS`]
762/// consecutive runs ([`SampleGrid::groups`]).
763const GROUP_BLOCKS: usize = 32;
764
765/// A cached `(u, v, point)` sample grid, with the axis-aligned box of each run
766/// of [`SAMPLE_BLOCK`] consecutive samples, so the nearest-sample scan
767/// ([`nearest_sample`]) skips a run none of whose samples can beat the answer
768/// it holds.
769#[derive(Clone, Debug)]
770pub(crate) struct SampleGrid {
771    samples: Vec<(f64, f64, Vec3)>,
772    blocks: Vec<SampleBlock>,
773    /// The union box of each [`GROUP_BLOCKS`] consecutive runs, prunable only
774    /// when every run in it is. A union box's floor is at most each of its
775    /// runs' floors as computed (the box faces are the runs' own, and the gap
776    /// to a face farther out is no larger, IEEE rounding being monotone).
777    groups: Vec<SampleBlock>,
778}
779
780/// One run's box. `prunable` is false when a sample has a non-finite
781/// coordinate: the box says nothing about it, and the run is always scanned.
782#[derive(Clone, Copy, Debug)]
783struct SampleBlock {
784    low: Vec3,
785    high: Vec3,
786    prunable: bool,
787}
788
789impl SampleGrid {
790    fn new(samples: Vec<(f64, f64, Vec3)>) -> Self {
791        let blocks = samples
792            .chunks(SAMPLE_BLOCK)
793            .map(|chunk| {
794                let mut low = chunk[0].2;
795                let mut high = low;
796                let mut prunable = true;
797                for &(_, _, p) in chunk {
798                    prunable &= p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
799                    low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
800                    high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
801                }
802                SampleBlock { low, high, prunable }
803            })
804            .collect::<Vec<_>>();
805        let groups = blocks
806            .chunks(GROUP_BLOCKS)
807            .map(|chunk| {
808                let mut group = chunk[0];
809                for block in chunk {
810                    group.low = Vec3::new(group.low.x.min(block.low.x), group.low.y.min(block.low.y), group.low.z.min(block.low.z));
811                    group.high = Vec3::new(group.high.x.max(block.high.x), group.high.y.max(block.high.y), group.high.z.max(block.high.z));
812                    group.prunable &= block.prunable;
813                }
814                group
815            })
816            .collect();
817        Self { samples, blocks, groups }
818    }
819}
820
821impl SampleBlock {
822    /// A floor on `sample.sub(point).length_squared()` over the run's samples,
823    /// as COMPUTED, not only as real numbers: per axis the gap from `point` to
824    /// the box is the same subtraction against the box face, and IEEE rounding
825    /// is monotone, so `|fl(s - p)| >= fl(face - p)`; squaring and the same
826    /// left-to-right sum of non-negative terms keep the order (`Vec3::dot`
827    /// has no fused multiply-add). A NaN `point` reads gaps of 0 and every
828    /// distance is NaN, which no scan takes, so skipping changes nothing.
829    fn lower_bound(&self, point: Vec3) -> f64 {
830        let gap = |p: f64, low: f64, high: f64| {
831            if p < low {
832                low - p
833            } else if p > high {
834                p - high
835            } else {
836                0.0
837            }
838        };
839        let x = gap(point.x, self.low.x, self.high.x);
840        let y = gap(point.y, self.low.y, self.high.y);
841        let z = gap(point.z, self.low.z, self.high.z);
842        x * x + y * y + z * z
843    }
844}
845
846/// `BREP_PROJECTION_PRUNED_SCAN=0` scans every sample, as before 2026-11-10,
847/// for a same-binary A/B. Read once per process.
848fn pruned_scan() -> bool {
849    switch!("BREP_PROJECTION_PRUNED_SCAN", true)
850}
851
852/// Replace `best` with the grid's nearest sample when that is strictly nearer,
853/// the first such in scan order: the bits of the linear scan, which this was
854/// until 2026-11-10. A run is skipped only when its box's floor is not below
855/// the distance held, so none of its samples could have replaced it (the
856/// strict `<`), or lies above the bound read first ([`bound_first_scan`]),
857/// and the runs are visited in scan order. ABC 00027566's
858/// 166-span rational faces take the dense fallback on 71% of their pcurve
859/// probes (the first Newton not converged), each scanning 22.7k samples: 9.1 s
860/// of a face's 11.5 s of local answers.
861fn nearest_sample(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult) {
862    nearest_sample_with(grid, point, best, pruned_scan(), bound_first_scan());
863}
864
865/// `BREP_PROJECTION_BOUND_FIRST=0` prunes against the distance held alone, as
866/// on 2026-11-10 before this pass, for a same-binary A/B. Read once per
867/// process.
868///
869/// With it on, the runs are boxed again in groups of [`GROUP_BLOCKS`]. The scan
870/// first reads the run of lowest floor inside the group of lowest floor and
871/// takes the nearest of its samples as a BOUND, then visits the groups and
872/// their runs in scan order, skipping any whose floor is at or above the
873/// distance held or strictly above that bound. The runs alone started the seed
874/// scan from no answer (its incoming distance is infinite), so every run ahead
875/// of the nearest sample was read before the held distance fell, and every
876/// scan computed one floor per run: on ABC 00027566 the seed
877/// scans averaged 63k samples (361 s over 5M calls) and the dense ones 180k
878/// (167 s over 3.5M), while a bound over the runs alone cut the first to 190 s
879/// but doubled the second to 311 s with its extra pass of floors.
880fn bound_first_scan() -> bool {
881    switch!("BREP_PROJECTION_BOUND_FIRST", true)
882}
883
884/// The scan behind [`nearest_sample`]. With `bound_first`, the bound is the
885/// smaller of the incoming distance and one real sample distance, so it is
886/// never below the grid's least distance `m`. The first sample at `m` lies in
887/// a run whose floor is at most `m`, hence not above the bound and not at or
888/// above the distance held when the scan reaches it (every sample before it is
889/// strictly farther than `m`): that run is read and the sample taken, as the
890/// linear scan takes it, and nothing after it is strictly nearer. A run whose
891/// floor is above the bound holds no sample at `m`. The bound pass reads only
892/// prunable runs, and updates on a strict `<`, so a NaN never enters it.
893fn nearest_sample_with(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult, pruned: bool, bound_first: bool) {
894    if !(pruned && bound_first) {
895        for (block, chunk) in grid.blocks.iter().zip(grid.samples.chunks(SAMPLE_BLOCK)) {
896            if pruned && block.prunable && block.lower_bound(point) >= best.distance_squared {
897                continue;
898            }
899            scan_run(chunk, point, best);
900        }
901        return;
902    }
903    // The bound: one real sample distance (or the incoming one), from the run
904    // of lowest floor inside the group of lowest floor.
905    let mut bound = best.distance_squared;
906    let lowest_group = lowest_floor(&grid.groups, 0..grid.groups.len(), point);
907    if let Some(group) = lowest_group {
908        let runs = group * GROUP_BLOCKS..((group + 1) * GROUP_BLOCKS).min(grid.blocks.len());
909        if let Some(run) = lowest_floor(&grid.blocks, runs, point) {
910            let start = run * SAMPLE_BLOCK;
911            let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
912            for &(_, _, sample) in &grid.samples[start..end] {
913                let distance_squared = sample.sub(point).length_squared();
914                if distance_squared < bound {
915                    bound = distance_squared;
916                }
917            }
918        }
919    }
920    let skip = |box_: &SampleBlock, held: f64| {
921        if !box_.prunable {
922            return false;
923        }
924        let floor = box_.lower_bound(point);
925        floor >= held || floor > bound
926    };
927    for (group_index, group) in grid.groups.iter().enumerate() {
928        if skip(group, best.distance_squared) {
929            continue;
930        }
931        let first = group_index * GROUP_BLOCKS;
932        let last = (first + GROUP_BLOCKS).min(grid.blocks.len());
933        for run in first..last {
934            if skip(&grid.blocks[run], best.distance_squared) {
935                continue;
936            }
937            let start = run * SAMPLE_BLOCK;
938            let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
939            scan_run(&grid.samples[start..end], point, best);
940        }
941    }
942}
943
944/// The linear scan of one run: strict `<`, so the first of equal samples
945/// stands.
946fn scan_run(run: &[(f64, f64, Vec3)], point: Vec3, best: &mut NewtonResult) {
947    for &(u, v, sample) in run {
948        let distance_squared = sample.sub(point).length_squared();
949        if distance_squared < best.distance_squared {
950            best.u = u;
951            best.v = v;
952            best.distance_squared = distance_squared;
953        }
954    }
955}
956
957/// The index, within `range`, of the prunable box of least floor (the first
958/// on a tie), or None when none is prunable.
959fn lowest_floor(boxes: &[SampleBlock], range: std::ops::Range<usize>, point: Vec3) -> Option<usize> {
960    let mut lowest: Option<(usize, f64)> = None;
961    for index in range {
962        let box_ = &boxes[index];
963        if !box_.prunable {
964            continue;
965        }
966        let floor = box_.lower_bound(point);
967        if lowest.is_none_or(|(_, held)| floor < held) {
968            lowest = Some((index, floor));
969        }
970    }
971    lowest.map(|(index, _)| index)
972}
973
974// Boundary-ring cache geometry: 4 sides × 22 exponents × 33 indices. The scan
975// positions depend only on the surface domain and these fixed integers, so the
976// evaluated points are a pure function of the surface and cacheable.
977const RING_EXPONENTS: usize = 22;
978const RING_INDICES: usize = 33; // index 0..=32
979
980#[inline]
981fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
982    (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
983}
984
985/// The boundary-ring sample points for the degenerate projection fallback,
986/// flattened by `ring_slot(side, exponent-1, index)`. Sides: 0 = v-low
987/// (`v = v0 + (v1-v0)·offset`), 1 = v-high (`v1 - (v1-v0)·offset`), 2 = u-low
988/// (`u0 + (u1-u0)·offset`), 3 = u-high (`u1 - (u1-u0)·offset`), where
989/// `offset = 1/2^exponent`. A pure function of the surface, cached on it — the
990/// fallback's boundary ring re-evaluated these on every one of ~1.7M calls.
991fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
992    if let Some(grid) = surface.projection_ring_grid.get() {
993        return Ok(grid);
994    }
995    let [u0, u1] = surface.domain_u()?;
996    let [v0, v1] = surface.domain_v()?;
997    let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
998    for exponent in 1..=RING_EXPONENTS as i32 {
999        let offset = 1.0 / 2f64.powi(exponent);
1000        let e = exponent as usize - 1;
1001        let v_lo = v0 + (v1 - v0) * offset;
1002        let v_hi = v1 - (v1 - v0) * offset;
1003        let u_lo = u0 + (u1 - u0) * offset;
1004        let u_hi = u1 - (u1 - u0) * offset;
1005        for index in 0..=32 {
1006            let u = u0 + (u1 - u0) * index as f64 / 32.0;
1007            grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
1008            grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
1009        }
1010        for index in 0..=32 {
1011            let v = v0 + (v1 - v0) * index as f64 / 32.0;
1012            grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
1013            grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
1014        }
1015    }
1016    Ok(surface.projection_ring_grid.get_or_init(|| grid))
1017}
1018
1019/// The LOCAL answer: Newton from the seed grid's single nearest sample (or the
1020/// linear-revolution foot), with the degenerate-parameterization fallbacks.
1021///
1022/// This is the whole of what the general projector returned before
1023/// 2026-09-14, and it is a local minimum, not the minimum: on a general patch
1024/// nearly closed in one direction the nearest sample can sit on the far end,
1025/// and Newton converges there (abc_00000026's thread flanks, 2.16e-2 .. 4.1e-2
1026/// against a true 1e-13). [`project_point_to_surface_general`] runs it first
1027/// and keeps its answer wherever no other basin beats it.
1028fn nearest_seed_answer(
1029    surface: &NurbsSurface,
1030    point: Vec3,
1031    [u0, u1, v0, v1]: [f64; 4],
1032    closed_u: bool,
1033    closed_v: bool,
1034) -> Result<NewtonResult, String> {
1035    let mut best = NewtonResult {
1036        u: u0,
1037        v: v0,
1038        distance_squared: f64::INFINITY,
1039        converged: false,
1040    };
1041    nearest_sample(projection_seed_grid(surface)?, point, &mut best);
1042    if let Some(revolution) = project_linear_revolution(surface, point)? {
1043        if revolution.distance * revolution.distance < best.distance_squared {
1044            best.u = revolution.u;
1045            best.v = revolution.v;
1046            best.distance_squared = revolution.distance * revolution.distance;
1047        }
1048    }
1049    let first_newton = surface_newton(
1050        surface,
1051        point,
1052        best.u,
1053        best.v,
1054        [u0, u1, v0, v1],
1055        closed_u,
1056        closed_v,
1057    )?;
1058    if first_newton.distance_squared < best.distance_squared {
1059        best = first_newton;
1060    }
1061    let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1062    let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
1063    if degenerate || !first_newton.converged {
1064        let mut grid = best;
1065        nearest_sample(projection_dense_grid(surface)?, point, &mut grid);
1066        if grid.distance_squared < best.distance_squared {
1067            best = grid;
1068        }
1069        let polished = surface_newton(
1070            surface,
1071            point,
1072            grid.u,
1073            grid.v,
1074            [u0, u1, v0, v1],
1075            closed_u,
1076            closed_v,
1077        )?;
1078        if polished.distance_squared < best.distance_squared {
1079            best = polished;
1080        }
1081
1082        let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1083        if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
1084            let ring_grid = projection_ring_grid(surface)?;
1085            let mut ring = best;
1086            // Read the pre-evaluated ring points at the exact same parameter
1087            // positions the live scans used (side/exponent/index → `ring_slot`),
1088            // preserving scan order and argmin tie-breaking for bit-identity.
1089            let scan = |side: usize,
1090                        exponent_index: usize,
1091                        along_u: bool,
1092                        fixed: f64,
1093                        candidate: &mut NewtonResult| {
1094                for index in 0..=32 {
1095                    let moving = if along_u {
1096                        u0 + (u1 - u0) * index as f64 / 32.0
1097                    } else {
1098                        v0 + (v1 - v0) * index as f64 / 32.0
1099                    };
1100                    let (u, v) = if along_u {
1101                        (moving, fixed)
1102                    } else {
1103                        (fixed, moving)
1104                    };
1105                    let distance_squared = ring_grid
1106                        [ring_slot(side, exponent_index, index as usize)]
1107                    .sub(point)
1108                    .length_squared();
1109                    if distance_squared < candidate.distance_squared {
1110                        candidate.u = u;
1111                        candidate.v = v;
1112                        candidate.distance_squared = distance_squared;
1113                    }
1114                }
1115            };
1116            for exponent in 1..=22 {
1117                let offset = 1.0 / 2f64.powi(exponent);
1118                let e = exponent as usize - 1;
1119                if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
1120                    scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
1121                }
1122                if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
1123                    scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
1124                }
1125                if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
1126                    scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
1127                }
1128                if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
1129                    scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
1130                }
1131            }
1132            if ring.distance_squared < best.distance_squared {
1133                best = ring;
1134            }
1135            let polished = surface_newton(
1136                surface,
1137                point,
1138                ring.u,
1139                ring.v,
1140                [u0, u1, v0, v1],
1141                closed_u,
1142                closed_v,
1143            )?;
1144            if polished.distance_squared < best.distance_squared {
1145                best = polished;
1146            }
1147        }
1148    }
1149    Ok(best)
1150}
1151
1152/// Escape hatch for A/B measurement: `BREP_PROJECTION_GLOBAL=0` returns the
1153/// local answer alone ([`nearest_seed_answer`], the pre-2026-09-14 projector),
1154/// so a population run can be repeated with and without the global stage from
1155/// one binary. Read once.
1156fn global_stage_enabled() -> bool {
1157    switch!("BREP_PROJECTION_GLOBAL", true)
1158}
1159
1160/// `BREP_PROJECTION_GLOBAL_SITES=<a,b,…>`: the global stage's answer is kept
1161/// only for call sites whose `file:line` contains one of the comma-separated
1162/// substrings, and the local answer is returned everywhere else — so a result
1163/// that moves can be attributed to the call site that moved it.
1164/// `BREP_PROJECTION_GLOBAL_EXCEPT=<a,b,…>` is the complement: every site but
1165/// those. Unset: every site. Read once.
1166/// Whether a `BREP_PROJECTION_GLOBAL_SITES`/`_EXCEPT` filter is set, i.e.
1167/// whether [`project_point_to_surface`]'s answer can depend on its call site.
1168pub(crate) fn global_site_filter_active() -> bool {
1169    // Read once per process, like the site lists `global_answer_admitted`
1170    // reads: the common reader asks this for every probe (two `getenv` calls,
1171    // ~3.7% of the elephant's B12 prefix on the production sampler,
1172    // 2026-11-02), and nothing sets these variables after start-up.
1173    static ACTIVE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1174    *ACTIVE.get_or_init(|| {
1175        std::env::var_os("BREP_PROJECTION_GLOBAL_SITES").is_some() || std::env::var_os("BREP_PROJECTION_GLOBAL_EXCEPT").is_some()
1176    })
1177}
1178
1179fn global_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
1180    fn list(name: &str) -> Option<Vec<String>> {
1181        std::env::var(name)
1182            .ok()
1183            .map(|value| value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect())
1184    }
1185    static SITES: std::sync::OnceLock<(Option<Vec<String>>, Option<Vec<String>>)> =
1186        std::sync::OnceLock::new();
1187    let (only, except) = SITES.get_or_init(|| {
1188        (list("BREP_PROJECTION_GLOBAL_SITES"), list("BREP_PROJECTION_GLOBAL_EXCEPT"))
1189    });
1190    if only.is_none() && except.is_none() {
1191        return true;
1192    }
1193    let location = format!("{}:{}", caller.file(), caller.line());
1194    let named = |sites: &Vec<String>| sites.iter().any(|site| location.contains(site.as_str()));
1195    only.as_ref().is_none_or(named) && !except.as_ref().is_some_and(named)
1196}
1197
1198/// `BREP_PROJECTION_AUDIT=<dir>`: every query on which the global stage
1199/// replaces the local answer appends one tab-separated line to
1200/// `<dir>/projection-audit-<pid>.tsv` — the call site, the local and the
1201/// global distance, both feet's `(u, v)` and the surface's shape. Per process,
1202/// because the case gate forks a child per case and loses its stderr.
1203fn audit_record(caller: &std::panic::Location<'_>, surface: &NurbsSurface, local: &NewtonResult, global: &NewtonResult) {
1204    use std::io::Write;
1205    static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
1206        std::sync::OnceLock::new();
1207    let sink = SINK.get_or_init(|| {
1208        let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
1209        let path = std::path::Path::new(&directory)
1210            .join(format!("projection-audit-{}.tsv", std::process::id()));
1211        std::fs::OpenOptions::new()
1212            .create(true)
1213            .append(true)
1214            .open(path)
1215            .ok()
1216            .map(std::sync::Mutex::new)
1217    });
1218    if let Some(file) = sink {
1219        if let Ok(mut file) = file.lock() {
1220            let _ = writeln!(
1221                file,
1222                "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{}x{} cp {}x{}",
1223                caller.file(),
1224                caller.line(),
1225                local.distance_squared.sqrt(),
1226                global.distance_squared.sqrt(),
1227                local.u,
1228                local.v,
1229                global.u,
1230                global.v,
1231                surface.degree_u,
1232                surface.degree_v,
1233                surface.control_points.len(),
1234                surface.control_points.first().map(Vec::len).unwrap_or(0),
1235            );
1236        }
1237    }
1238}
1239
1240/// The point of a general surface nearest `point`: the least local minimum of
1241/// the distance over the surface's whole domain.
1242///
1243/// **What it does.** First the local answer ([`nearest_seed_answer`]: Newton
1244/// from the nearest seed-grid sample, with its degenerate fallbacks — the whole
1245/// projector before 2026-09-14). Then the global stage ([`global_minimum`]):
1246/// the domain is cut into the cells of the projection lattice (every knot span
1247/// sampled `max(3, degree + 1)` times in an open direction and
1248/// `max(8, 4 × degree)` times in a closed one — the seed grid's own density),
1249/// and each cell is enclosed in a box built from the control points of the
1250/// surface REFINED to that cell. A NURBS patch with positive weights lies in
1251/// the convex hull of the control points that support it, so the distance from
1252/// the query to a cell's box is a rigorous LOWER bound on the distance to every
1253/// surface point of the cell. Every cell whose bound is below the best answer
1254/// so far is a candidate basin, and is refined by a descent confined to the
1255/// cell ([`confined_descent`]: Newton where the Hessian is positive definite,
1256/// the metric-scaled gradient where it is not, halved until the distance does
1257/// not rise, so it can end at a minimum but not at a saddle or a maximum),
1258/// seeded at the cell's nearest lattice node and continued over the domain
1259/// when the cell's least point is on its edge. The least answer wins. A closed
1260/// direction's seam is not special: the lattice spans the whole period, the
1261/// cells on either side of the seam are ordinary cells, and the continuation
1262/// wraps.
1263///
1264/// **What is guaranteed.**
1265/// * The answer is a point ON the surface, so its distance never under-reads.
1266/// * Every cell that could hold a point nearer than the answer by more than the
1267///   resolution has been descended in, unless it already held a verified
1268///   minimum: a cell left out was excluded by its hull bound, which cannot
1269///   over-read. [`GlobalStats::hull_lower_bound`] is that certificate: the
1270///   true distance lies in `[hull_lower_bound, answer]`, always.
1271/// * **The one step not certified in general: one basin in the cell that holds
1272///   the least minimum.** A descent finds the basin of its seed. A second basin
1273///   in any OTHER cell costs nothing — that cell's better basin is still no
1274///   nearer than the least minimum — but a second local minimum in the cell
1275///   holding the least minimum can catch the descent seeded at that cell's
1276///   nearest lattice node, and the least minimum is missed by at most
1277///   `answer − hull_lower_bound`. That takes a query past the focal distance of
1278///   part of the cell (farther than its radius of normal curvature on the
1279///   query's side, where the distance's Hessian `G − d·II` stops being positive
1280///   definite) or a cell whose image turns back toward the query. A descent that
1281///   runs out of iterations without a verified minimum fails the same way, with
1282///   the same bound. Seeding every corner of every candidate cell was measured
1283///   and changed no answer on the bench.
1284/// * **Certified on the analytic carriers** — the sphere, torus, cylinder and
1285///   cone, each a revolution of a circle or a line, and the plane. A lattice cell
1286///   there is an (azimuth × generatrix) rectangle under half a turn wide. In the
1287///   cell holding the least minimum the azimuthal derivative of the squared
1288///   distance, `2 ρ(v) ρ_P sin(λ − λ_P)`, vanishes only on the query's own
1289///   meridian, and moving toward that meridian lowers the distance from every
1290///   other point of the cell, so each local minimum over the cell is a local
1291///   minimum of the planar distance from the query's meridian image to the
1292///   generatrix over the cell's `v` range — exactly one on a circle arc under
1293///   half a turn or on a segment. At the focal sets (the axis, the sphere's
1294///   centre, the tube's centre circle) the minimum is a continuum of equal
1295///   distances and every foot is right. A general profile of revolution and a
1296///   general patch are not certified.
1297/// * Where the local answer is already the least minimum, it is returned
1298///   unchanged, bit for bit: the global stage replaces it only with a point
1299///   nearer by more than the resolution.
1300///
1301/// **The two numbers it reads.** The *on-surface floor* is `LINEAR_TOLERANCE`,
1302/// INHERITED unchanged: it is the local Newton's own stopping residual, and a
1303/// local answer within it of the query is on the surface and nothing is
1304/// searched. The *resolution* is the one new number, and it is the surface's
1305/// own: `(degree_u + 1)(degree_v + 1) · ε · scale`, `scale` the largest
1306/// control-point coordinate magnitude — the rounding of one evaluation, which
1307/// sums that many terms of that size. Every box is grown by it, and no
1308/// improvement finer than it is taken.
1309///
1310/// **What it costs.** A query settled by the floor costs what the local answer
1311/// always cost. Any other query builds the cell enclosure once per surface (one
1312/// knot refinement of the net to the lattice, cached on the surface beside the
1313/// seed grid), walks a bounding-sphere tree over the cells, and runs one
1314/// descent per candidate cell the hull bound does not exclude.
1315#[track_caller]
1316pub(crate) fn project_point_to_surface_general(
1317    surface: &NurbsSurface,
1318    point: Vec3,
1319) -> Result<SurfaceProjection, String> {
1320    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_GENERAL, surface);
1321    let [u0, u1] = surface.domain_u()?;
1322    let [v0, v1] = surface.domain_v()?;
1323    let (closed_u, closed_v) = surface.closed_directions()?;
1324    let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1325    let mut best = local;
1326    if global_stage_enabled() {
1327        global_minimum(
1328            surface,
1329            point,
1330            [u0, u1, v0, v1],
1331            (closed_u, closed_v),
1332            &mut best,
1333            &mut GlobalStats::default(),
1334        )?;
1335        if best.u != local.u || best.v != local.v {
1336            let caller = std::panic::Location::caller();
1337            if global_answer_admitted(caller) {
1338                audit_record(caller, surface, &local, &best);
1339            } else {
1340                best = local;
1341            }
1342        }
1343    }
1344    let projected = surface.evaluate(best.u, best.v)?;
1345    Ok(SurfaceProjection {
1346        u: best.u,
1347        v: best.v,
1348        point: projected,
1349        distance: projected.sub(point).length(),
1350    })
1351}
1352
1353/// The single-seed inversion by itself: the closed form on an analytic
1354/// carrier, otherwise [`nearest_seed_answer`] — Newton from the seed grid's
1355/// nearest sample, WITHOUT the global stage. Its distance is an upper bound and
1356/// its foot a local minimum. It is not a nearest-point query: it exists for a
1357/// caller that asks a question ABOUT that inversion, and there is one —
1358/// `joint_branch_pcurves` (`io/step_import/builder/rims.rs`) offers a loop the
1359/// joint branch only when this inversion alternates between branches along it,
1360/// which it does on a carrier that comes back within one seed-grid spacing of
1361/// itself (`abc_00000026`'s nearly closed thread flanks).
1362pub(crate) fn project_point_to_surface_from_nearest_seed(
1363    surface: &NurbsSurface,
1364    point: Vec3,
1365) -> Result<SurfaceProjection, String> {
1366    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_NEAREST_SEED, surface);
1367    if let Some(analytic) = surface.analytic() {
1368        if let Some(projection) = analytic.project(surface, point) {
1369            return Ok(projection);
1370        }
1371    }
1372    if let Some(ellipsoid) = surface.affine_sphere() {
1373        if let Some(projection) = ellipsoid.project(surface, point) {
1374            return Ok(projection);
1375        }
1376    }
1377    let [u0, u1] = surface.domain_u()?;
1378    let [v0, v1] = surface.domain_v()?;
1379    let (closed_u, closed_v) = surface.closed_directions()?;
1380    let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1381    let projected = surface.evaluate(local.u, local.v)?;
1382    Ok(SurfaceProjection {
1383        u: local.u,
1384        v: local.v,
1385        point: projected,
1386        distance: projected.sub(point).length(),
1387    })
1388}
1389
1390/// Both answers for one query, and what the global stage did — the bench's
1391/// seam (`examples/projector_bench.rs`). Not for kernel callers.
1392#[doc(hidden)]
1393pub fn project_point_to_surface_general_lanes(
1394    surface: &NurbsSurface,
1395    point: Vec3,
1396    run_global: bool,
1397) -> Result<(SurfaceProjection, GlobalStats), String> {
1398    let [u0, u1] = surface.domain_u()?;
1399    let [v0, v1] = surface.domain_v()?;
1400    let (closed_u, closed_v) = surface.closed_directions()?;
1401    let mut best = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1402    let mut stats = GlobalStats::default();
1403    if run_global {
1404        global_minimum(surface, point, [u0, u1, v0, v1], (closed_u, closed_v), &mut best, &mut stats)?;
1405    }
1406    let projected = surface.evaluate(best.u, best.v)?;
1407    Ok((
1408        SurfaceProjection {
1409            u: best.u,
1410            v: best.v,
1411            point: projected,
1412            distance: projected.sub(point).length(),
1413        },
1414        stats,
1415    ))
1416}
1417
1418/// How many basins the cells of one query hold — the bench's measure of the
1419/// global stage's one uncertified step (see [`project_point_to_surface_general`]).
1420#[doc(hidden)]
1421#[derive(Clone, Debug, Default)]
1422pub struct BasinCensus {
1423    /// Cells whose hull bound is below the answer's distance: every cell the
1424    /// global stage could have descended in.
1425    pub cells: usize,
1426    /// Descents run: one from each corner of those cells and one from the middle.
1427    pub descents: usize,
1428    /// Descents that stopped without a verified minimum.
1429    pub unconverged: usize,
1430    /// Cells where two verified minima differ in distance by more than `bar`.
1431    /// Distances within the on-surface floor count as equal: the descent stops
1432    /// there, so two feet under it are one foot polished to different depths.
1433    pub cells_with_two_basins: usize,
1434    /// Of those, cells whose rectangle holds the least foot any descent found:
1435    /// the only cells where a second basin can cost the answer anything.
1436    pub foot_cells_with_two_basins: usize,
1437    /// The least distance any descent reached, and where.
1438    pub least: f64,
1439    pub least_uv: (f64, f64),
1440    /// Every descent in a two-basin cell that holds the least foot: the cell,
1441    /// its rectangle, the seed, where it stopped, its distance, and whether that
1442    /// is a verified minimum.
1443    pub foot_cell_descents: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)>,
1444}
1445
1446/// Descend in every cell whose hull bound is below `answer_distance` from each
1447/// of its corners and its middle, and count the cells whose verified minima
1448/// disagree by more than `bar`. For the bench only: it costs five descents per
1449/// cell, which the projector itself was measured not to need.
1450#[doc(hidden)]
1451pub fn project_point_to_surface_basin_census(
1452    surface: &NurbsSurface,
1453    point: Vec3,
1454    answer_distance: f64,
1455    bar: f64,
1456) -> Result<BasinCensus, String> {
1457    let mut census = BasinCensus {
1458        least: f64::INFINITY,
1459        ..BasinCensus::default()
1460    };
1461    let Some(cells) = projection_cells(surface)? else {
1462        return Ok(census);
1463    };
1464    let columns = cells.vs.len() - 1;
1465    let mut spreads: Vec<(usize, f64)> = Vec::new();
1466    let mut runs: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)> = Vec::new();
1467    for (cell, cell_box) in cells.boxes.iter().enumerate() {
1468        if cell_box.lower_bound(point) >= answer_distance {
1469            continue;
1470        }
1471        census.cells += 1;
1472        let (row, column) = (cell / columns, cell % columns);
1473        let rectangle = [cells.us[row], cells.us[row + 1], cells.vs[column], cells.vs[column + 1]];
1474        let middle = ((rectangle[0] + rectangle[1]) * 0.5, (rectangle[2] + rectangle[3]) * 0.5);
1475        let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
1476        for seed in [
1477            (rectangle[0], rectangle[2]),
1478            (rectangle[1], rectangle[2]),
1479            (rectangle[0], rectangle[3]),
1480            (rectangle[1], rectangle[3]),
1481            middle,
1482        ] {
1483            census.descents += 1;
1484            let (found, _) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1485            let distance = found.distance_squared.sqrt();
1486            runs.push((cell, rectangle, seed, (found.u, found.v), distance, found.converged));
1487            if distance < census.least {
1488                census.least = distance;
1489                census.least_uv = (found.u, found.v);
1490            }
1491            if !found.converged {
1492                census.unconverged += 1;
1493                continue;
1494            }
1495            low = low.min(distance.max(LINEAR_TOLERANCE));
1496            high = high.max(distance.max(LINEAR_TOLERANCE));
1497        }
1498        if high - low > bar {
1499            spreads.push((cell, high - low));
1500        }
1501    }
1502    census.cells_with_two_basins = spreads.len();
1503    let (u, v) = census.least_uv;
1504    let foot_cells: Vec<usize> = spreads
1505        .iter()
1506        .map(|&(cell, _)| cell)
1507        .filter(|&cell| {
1508            let (row, column) = (cell / columns, cell % columns);
1509            cells.us[row] <= u && u <= cells.us[row + 1] && cells.vs[column] <= v && v <= cells.vs[column + 1]
1510        })
1511        .collect();
1512    census.foot_cells_with_two_basins = foot_cells.len();
1513    census.foot_cell_descents = runs.into_iter().filter(|run| foot_cells.contains(&run.0)).collect();
1514    Ok(census)
1515}
1516
1517/// What the global stage did on one query.
1518#[doc(hidden)]
1519#[derive(Clone, Copy, Debug, Default)]
1520pub struct GlobalStats {
1521    /// The local answer was within the on-surface floor; nothing searched.
1522    pub settled_by_floor: bool,
1523    /// Cells whose hull bound admitted them.
1524    pub candidates: usize,
1525    /// Confined Newtons run (candidates not already holding a converged foot).
1526    pub refinements: usize,
1527    /// Continuations across the domain from a cell edge.
1528    pub continuations: usize,
1529    /// The global stage replaced the local answer.
1530    pub replaced: bool,
1531    /// The least hull bound over every cell — the true distance is never
1532    /// below it (0 when the floor settled the query).
1533    pub hull_lower_bound: f64,
1534}
1535
1536/// One lattice cell's enclosure: an oriented box around the control points
1537/// that support the cell, in an orthonormal frame fitted to the cell.
1538#[derive(Clone, Debug)]
1539struct CellBox {
1540    axes: [Vec3; 3],
1541    low: [f64; 3],
1542    high: [f64; 3],
1543}
1544
1545impl CellBox {
1546    /// Distance from `point` to the box: a lower bound on the distance to
1547    /// every surface point of the cell.
1548    fn lower_bound(&self, point: Vec3) -> f64 {
1549        let mut squared = 0.0;
1550        for axis in 0..3 {
1551            let coordinate = point.dot(self.axes[axis]);
1552            let excess = (self.low[axis] - coordinate).max(coordinate - self.high[axis]).max(0.0);
1553            squared += excess * excess;
1554        }
1555        squared.sqrt()
1556    }
1557}
1558
1559/// A node of the bounding-sphere tree over the cells. A leaf names its cell;
1560/// an internal node its two children.
1561#[derive(Clone, Copy, Debug)]
1562struct CellTreeNode {
1563    centre: Vec3,
1564    radius: f64,
1565    children: Option<(u32, u32)>,
1566    cell: u32,
1567}
1568
1569/// The cached enclosure of every projection lattice cell (see
1570/// [`project_point_to_surface_general`]).
1571#[derive(Clone, Debug)]
1572pub(crate) struct ProjectionCells {
1573    us: Vec<f64>,
1574    vs: Vec<f64>,
1575    /// `us.len() × vs.len()` lattice points, row-major in u.
1576    nodes: Vec<Vec3>,
1577    /// `(us.len() - 1) × (vs.len() - 1)` cells, row-major in u.
1578    boxes: Vec<CellBox>,
1579    /// The tree's root is the LAST node.
1580    tree: Vec<CellTreeNode>,
1581    resolution: f64,
1582}
1583
1584/// The projection lattice's parameters along one direction: every knot span
1585/// cut into `samples` equal pieces, the seed grid's density, shared span ends
1586/// listed once.
1587fn lattice_parameters(knots: &[f64], degree: usize, closed: bool) -> Vec<f64> {
1588    let [start, end] = crate::curve::knot_domain(knots, degree);
1589    let samples = if closed {
1590        8usize.max(degree * 4)
1591    } else {
1592        3usize.max(degree + 1)
1593    };
1594    let mut breaks = vec![start];
1595    breaks.extend(interior_knots(knots, degree));
1596    breaks.push(end);
1597    let mut parameters = Vec::with_capacity((breaks.len() - 1) * samples + 1);
1598    for pair in breaks.windows(2) {
1599        for index in 0..samples {
1600            parameters.push(pair[0] + (pair[1] - pair[0]) * index as f64 / samples as f64);
1601        }
1602    }
1603    parameters.push(end);
1604    parameters
1605}
1606
1607/// Raise every lattice parameter to full multiplicity in the knot vector, so
1608/// each lattice cell is one Bézier piece of the refined net.
1609///
1610/// `net[i]` is the i-th control point along the refined direction, held as a
1611/// row across the other direction, and every row takes the same combination.
1612/// Knot refinement in one pass (Piegl & Tiller, *The NURBS Book*, A5.4), in
1613/// homogeneous coordinates, so a rational net refines exactly and the cost is
1614/// linear in the refined net's size.
1615fn refine_to_lattice(net: &mut Vec<Vec<Vec4>>, knots: &mut Vec<f64>, degree: usize, parameters: &[f64]) {
1616    let p = degree;
1617    let n = net.len() - 1;
1618    let mut inserted = Vec::new();
1619    for &parameter in parameters {
1620        let multiplicity = knots.iter().filter(|&&knot| knot == parameter).count();
1621        for _ in multiplicity..p {
1622            inserted.push(parameter);
1623        }
1624    }
1625    if inserted.is_empty() || p == 0 {
1626        return;
1627    }
1628    inserted.sort_by(f64::total_cmp);
1629    let find_span = |parameter: f64| -> usize {
1630        if parameter >= knots[n + 1] {
1631            return n;
1632        }
1633        (knots.partition_point(|&knot| knot <= parameter) - 1).clamp(p, n)
1634    };
1635    let r = inserted.len() - 1;
1636    let m = n + p + 1;
1637    let a = find_span(inserted[0]);
1638    let b = find_span(inserted[r]) + 1;
1639    let columns = net[0].len();
1640    let blank = vec![
1641        Vec4 {
1642            x: 0.0,
1643            y: 0.0,
1644            z: 0.0,
1645            w: 0.0,
1646        };
1647        columns
1648    ];
1649    let mut refined = vec![blank; n + r + 2];
1650    let mut refined_knots = vec![0.0; m + r + 2];
1651    for j in 0..=a - p {
1652        refined[j] = net[j].clone();
1653    }
1654    for j in b - 1..=n {
1655        refined[j + r + 1] = net[j].clone();
1656    }
1657    refined_knots[..=a].copy_from_slice(&knots[..=a]);
1658    for j in b + p..=m {
1659        refined_knots[j + r + 1] = knots[j];
1660    }
1661    let mut i = b + p - 1;
1662    let mut k = b + p + r;
1663    for j in (0..=r).rev() {
1664        while inserted[j] <= knots[i] && i > a {
1665            refined[k - p - 1] = net[i - p - 1].clone();
1666            refined_knots[k] = knots[i];
1667            k -= 1;
1668            i -= 1;
1669        }
1670        refined[k - p - 1] = refined[k - p].clone();
1671        for l in 1..=p {
1672            let index = k - p + l;
1673            let numerator = refined_knots[k + l] - inserted[j];
1674            if numerator == 0.0 {
1675                refined[index - 1] = refined[index].clone();
1676            } else {
1677                let alpha = numerator / (refined_knots[k + l] - knots[i - p + l]);
1678                let (head, tail) = refined.split_at_mut(index);
1679                for (before, &here) in head[index - 1].iter_mut().zip(&tail[0]) {
1680                    *before = before.scale(alpha).add(here.scale(1.0 - alpha));
1681                }
1682            }
1683        }
1684        refined_knots[k] = inserted[j];
1685        k = k.saturating_sub(1);
1686    }
1687    *net = refined;
1688    *knots = refined_knots;
1689}
1690
1691/// The control-point index range supporting the parameter interval
1692/// `[low, high]`: every basis function non-zero anywhere on it.
1693fn supporting_range(knots: &[f64], degree: usize, count: usize, low: f64, high: f64) -> (usize, usize) {
1694    let first_span = knots.partition_point(|&knot| knot <= low).saturating_sub(1);
1695    let last_span = knots.partition_point(|&knot| knot < high).saturating_sub(1);
1696    (
1697        first_span.saturating_sub(degree).min(count - 1),
1698        last_span.max(first_span).min(count - 1),
1699    )
1700}
1701
1702fn unit_or(vector: Vec3, fallback: Vec3) -> Vec3 {
1703    let length = vector.length();
1704    if length > 0.0 && length.is_finite() {
1705        vector.scale(1.0 / length)
1706    } else {
1707        fallback
1708    }
1709}
1710
1711fn enclosing_sphere(a: (Vec3, f64), b: (Vec3, f64)) -> (Vec3, f64) {
1712    let offset = b.0.sub(a.0);
1713    let separation = offset.length();
1714    if separation + b.1 <= a.1 {
1715        return a;
1716    }
1717    if separation + a.1 <= b.1 {
1718        return b;
1719    }
1720    let radius = (separation + a.1 + b.1) * 0.5;
1721    let centre = a.0.add(offset.scale((radius - a.1) / separation));
1722    (centre, radius)
1723}
1724
1725fn build_cell_tree(
1726    tree: &mut Vec<CellTreeNode>,
1727    spheres: &[(Vec3, f64)],
1728    columns: usize,
1729    [row_low, row_high, column_low, column_high]: [usize; 4],
1730) -> u32 {
1731    if row_high - row_low == 1 && column_high - column_low == 1 {
1732        let cell = row_low * columns + column_low;
1733        tree.push(CellTreeNode {
1734            centre: spheres[cell].0,
1735            radius: spheres[cell].1,
1736            children: None,
1737            cell: cell as u32,
1738        });
1739        return (tree.len() - 1) as u32;
1740    }
1741    let (first, second) = if row_high - row_low >= column_high - column_low {
1742        let middle = (row_low + row_high) / 2;
1743        (
1744            [row_low, middle, column_low, column_high],
1745            [middle, row_high, column_low, column_high],
1746        )
1747    } else {
1748        let middle = (column_low + column_high) / 2;
1749        (
1750            [row_low, row_high, column_low, middle],
1751            [row_low, row_high, middle, column_high],
1752        )
1753    };
1754    let first = build_cell_tree(tree, spheres, columns, first);
1755    let second = build_cell_tree(tree, spheres, columns, second);
1756    let a = tree[first as usize];
1757    let b = tree[second as usize];
1758    // The resolution-inflated leaf spheres already absorb rounding; the merge
1759    // adds one more rounding of the same order, absorbed by growing the radius
1760    // by the larger child's own ulp.
1761    let (centre, radius) = enclosing_sphere((a.centre, a.radius), (b.centre, b.radius));
1762    tree.push(CellTreeNode {
1763        centre,
1764        radius: radius * (1.0 + 4.0 * f64::EPSILON),
1765        children: Some((first, second)),
1766        cell: u32::MAX,
1767    });
1768    (tree.len() - 1) as u32
1769}
1770
1771fn projection_cells(surface: &NurbsSurface) -> Result<Option<&ProjectionCells>, String> {
1772    if let Some(cells) = surface.projection_cells.get() {
1773        return Ok(cells.as_ref());
1774    }
1775    let cells = build_projection_cells(surface)?;
1776    Ok(surface.projection_cells.get_or_init(|| cells).as_ref())
1777}
1778
1779fn build_projection_cells(surface: &NurbsSurface) -> Result<Option<ProjectionCells>, String> {
1780    let (closed_u, closed_v) = surface.closed_directions()?;
1781    let us = lattice_parameters(&surface.knots_u, surface.degree_u, closed_u);
1782    let vs = lattice_parameters(&surface.knots_v, surface.degree_v, closed_v);
1783    if us.len() < 2 || vs.len() < 2 {
1784        return Ok(None);
1785    }
1786    let mut scale = 0.0f64;
1787    for control in surface.control_points.iter().flatten() {
1788        let point = control.point()?;
1789        scale = scale.max(point.x.abs()).max(point.y.abs()).max(point.z.abs());
1790    }
1791    let resolution =
1792        ((surface.degree_u + 1) * (surface.degree_v + 1)) as f64 * f64::EPSILON * scale;
1793
1794    // Refine u (the net's rows), then v (its columns, as rows of the transpose).
1795    let mut net = surface.control_points.clone();
1796    let mut knots_u = surface.knots_u.clone();
1797    refine_to_lattice(&mut net, &mut knots_u, surface.degree_u, &us);
1798    let columns = net.first().map(Vec::len).unwrap_or(0);
1799    let mut transposed: Vec<Vec<Vec4>> =
1800        (0..columns).map(|column| net.iter().map(|row| row[column]).collect()).collect();
1801    let mut knots_v = surface.knots_v.clone();
1802    refine_to_lattice(&mut transposed, &mut knots_v, surface.degree_v, &vs);
1803    let rows = transposed.first().map(Vec::len).unwrap_or(0);
1804    let columns = transposed.len();
1805    if rows == 0 || columns == 0 {
1806        return Ok(None);
1807    }
1808    let mut points = vec![Vec3::default(); rows * columns];
1809    for (column, fibre) in transposed.iter().enumerate() {
1810        for (row, control) in fibre.iter().enumerate() {
1811            points[row * columns + column] = control.point()?;
1812        }
1813    }
1814
1815    let mut nodes = Vec::with_capacity(us.len() * vs.len());
1816    for &u in &us {
1817        for &v in &vs {
1818            nodes.push(surface.evaluate(u, v)?);
1819        }
1820    }
1821
1822    let cell_rows = us.len() - 1;
1823    let cell_columns = vs.len() - 1;
1824    let mut boxes = Vec::with_capacity(cell_rows * cell_columns);
1825    let mut spheres = Vec::with_capacity(cell_rows * cell_columns);
1826    for i in 0..cell_rows {
1827        let (row_low, row_high) =
1828            supporting_range(&knots_u, surface.degree_u, rows, us[i], us[i + 1]);
1829        for j in 0..cell_columns {
1830            let (column_low, column_high) =
1831                supporting_range(&knots_v, surface.degree_v, columns, vs[j], vs[j + 1]);
1832            let at = |row: usize, column: usize| points[row * columns + column];
1833            let c00 = at(row_low, column_low);
1834            let c10 = at(row_high, column_low);
1835            let c01 = at(row_low, column_high);
1836            let c11 = at(row_high, column_high);
1837            let along_u = c10.sub(c00).add(c11.sub(c01));
1838            let along_v = c01.sub(c00).add(c11.sub(c10));
1839            let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
1840            let normal = unit_or(e1.cross(along_v), e1.perpendicular()?);
1841            let e2 = unit_or(normal.cross(e1), e1.perpendicular()?);
1842            let e3 = e1.cross(e2);
1843            let axes = [e1, e2, e3];
1844            let mut low = [f64::INFINITY; 3];
1845            let mut high = [f64::NEG_INFINITY; 3];
1846            for row in row_low..=row_high {
1847                for column in column_low..=column_high {
1848                    let point = at(row, column);
1849                    for axis in 0..3 {
1850                        let coordinate = point.dot(axes[axis]);
1851                        low[axis] = low[axis].min(coordinate);
1852                        high[axis] = high[axis].max(coordinate);
1853                    }
1854                }
1855            }
1856            for axis in 0..3 {
1857                low[axis] -= resolution;
1858                high[axis] += resolution;
1859            }
1860            let mut centre = Vec3::default();
1861            let mut half_diagonal_squared = 0.0;
1862            for axis in 0..3 {
1863                centre = centre.add(axes[axis].scale((low[axis] + high[axis]) * 0.5));
1864                let half = (high[axis] - low[axis]) * 0.5;
1865                half_diagonal_squared += half * half;
1866            }
1867            if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
1868                return Ok(None);
1869            }
1870            spheres.push((centre, half_diagonal_squared.sqrt() + resolution));
1871            boxes.push(CellBox { axes, low, high });
1872        }
1873    }
1874    let mut tree = Vec::with_capacity(2 * boxes.len());
1875    build_cell_tree(&mut tree, &spheres, cell_columns, [0, cell_rows, 0, cell_columns]);
1876    Ok(Some(ProjectionCells {
1877        us,
1878        vs,
1879        nodes,
1880        boxes,
1881        tree,
1882        resolution,
1883    }))
1884}
1885
1886/// First- and second-order optimality of `(u, v)` for the distance over the
1887/// rectangle: stationary on the coordinates the rectangle leaves free (the
1888/// descent's own cosine test), with a positive definite Hessian there. A
1889/// `periodic` direction has no edge to be pinned against: its seam is interior.
1890fn optimality(
1891    derivatives: &[[Vec3; 3]; 3],
1892    point: Vec3,
1893    (u, v): (f64, f64),
1894    [ua, ub, va, vb]: [f64; 4],
1895    periodic: (bool, bool),
1896) -> Optimality {
1897    let residual = derivatives[0][0].sub(point);
1898    let residual_length = residual.length();
1899    let su = derivatives[1][0];
1900    let sv = derivatives[0][1];
1901    let f = su.dot(residual);
1902    let g = sv.dot(residual);
1903    let metric_u = su.length_squared();
1904    let metric_v = sv.length_squared();
1905    let stationary_u = metric_u.sqrt() * residual_length <= EPSILON
1906        || f.abs() <= 1e-10 * metric_u.sqrt() * residual_length;
1907    let stationary_v = metric_v.sqrt() * residual_length <= EPSILON
1908        || g.abs() <= 1e-10 * metric_v.sqrt() * residual_length;
1909    let pinned_u = !periodic.0 && ((u <= ua && f > 0.0) || (u >= ub && f < 0.0));
1910    let pinned_v = !periodic.1 && ((v <= va && g > 0.0) || (v >= vb && g < 0.0));
1911    let j00 = derivatives[2][0].dot(residual) + metric_u;
1912    let j01 = derivatives[1][1].dot(residual) + su.dot(sv);
1913    let j11 = derivatives[0][2].dot(residual) + metric_v;
1914    let determinant = j00 * j11 - j01 * j01;
1915    let convex = match (!pinned_u, !pinned_v) {
1916        (true, true) => j00 > 0.0 && determinant > 0.0,
1917        (true, false) => j00 > 0.0,
1918        (false, true) => j11 > 0.0,
1919        (false, false) => true,
1920    };
1921    Optimality {
1922        residual_length,
1923        gradient: (f, g),
1924        metric: (metric_u, metric_v),
1925        hessian: (j00, j01, j11),
1926        determinant,
1927        pinned: (pinned_u, pinned_v),
1928        convex,
1929        minimum: residual_length <= LINEAR_TOLERANCE
1930            || ((stationary_u || pinned_u) && (stationary_v || pinned_v) && convex),
1931    }
1932}
1933
1934struct Optimality {
1935    residual_length: f64,
1936    gradient: (f64, f64),
1937    metric: (f64, f64),
1938    hessian: (f64, f64, f64),
1939    determinant: f64,
1940    pinned: (bool, bool),
1941    convex: bool,
1942    minimum: bool,
1943}
1944
1945fn is_verified_minimum(
1946    surface: &NurbsSurface,
1947    point: Vec3,
1948    at: (f64, f64),
1949    rectangle: [f64; 4],
1950    periodic: (bool, bool),
1951) -> Result<bool, String> {
1952    let derivatives = surface.derivatives_small(at.0, at.1, 2)?;
1953    Ok(optimality(&derivatives, point, at, rectangle, periodic).minimum)
1954}
1955
1956/// A descent to a local minimum of the distance over the rectangle
1957/// `[ua, ub] × [va, vb]`, from a seed inside it.
1958///
1959/// Newton on the gradient converges to ANY critical point — a saddle or a
1960/// maximum as readily as a minimum — and reads convergence there, so every
1961/// step here must lower the distance: the Newton step where the Hessian is
1962/// positive definite on the free coordinates, the metric-scaled gradient step
1963/// where it is not, halved until the distance does not rise. A coordinate
1964/// pinned at the rectangle's edge by a gradient pointing out of it is held
1965/// there and the other takes the one-dimensional step, so a minimum ON the edge
1966/// converges as one (the Karush-Kuhn-Tucker point) instead of stalling.
1967///
1968/// Returns the best iterate, whether it is a verified minimum (stationary on
1969/// the free coordinates with a positive definite Hessian there, or within the
1970/// on-surface floor), and whether it lies on the rectangle's edge.
1971fn confined_descent(
1972    surface: &NurbsSurface,
1973    point: Vec3,
1974    seed: (f64, f64),
1975    rectangle: [f64; 4],
1976    periodic: (bool, bool),
1977) -> Result<(NewtonResult, bool), String> {
1978    let [ua, ub, va, vb] = rectangle;
1979    let mut u = fit_parameter(seed.0, ua, ub, periodic.0);
1980    let mut v = fit_parameter(seed.1, va, vb, periodic.1);
1981    let mut converged = false;
1982    let mut current = surface.evaluate(u, v)?.sub(point).length_squared();
1983    for _ in 0..MAX_NEWTON_ITERATIONS {
1984        let derivatives = surface.derivatives_small(u, v, 2)?;
1985        let state = optimality(&derivatives, point, (u, v), rectangle, periodic);
1986        current = state.residual_length * state.residual_length;
1987        if state.minimum {
1988            converged = true;
1989            break;
1990        }
1991        let (f, g) = state.gradient;
1992        let (metric_u, metric_v) = state.metric;
1993        let (j00, j01, j11) = state.hessian;
1994        let determinant = state.determinant;
1995        let (free_u, free_v) = (!state.pinned.0, !state.pinned.1);
1996        let (mut du, mut dv) = match (free_u, free_v) {
1997            (true, true) if state.convex => (
1998                (-f * j11 + g * j01) / determinant,
1999                (-g * j00 + f * j01) / determinant,
2000            ),
2001            (true, false) if state.convex => (-f / j00, 0.0),
2002            (false, true) if state.convex => (0.0, -g / j11),
2003            _ => (
2004                if free_u && metric_u > 0.0 { -f / metric_u } else { 0.0 },
2005                if free_v && metric_v > 0.0 { -g / metric_v } else { 0.0 },
2006            ),
2007        };
2008        if !(du.is_finite() && dv.is_finite()) || (du == 0.0 && dv == 0.0) {
2009            break;
2010        }
2011        let mut moved = false;
2012        // The step actually taken, a whole period removed across a seam.
2013        let taken = |next: f64, here: f64, low: f64, high: f64, wraps: bool| {
2014            let raw = next - here;
2015            if wraps {
2016                raw - (high - low) * (raw / (high - low)).round()
2017            } else {
2018                raw
2019            }
2020        };
2021        for _ in 0..40 {
2022            let next_u = fit_parameter(u + du, ua, ub, periodic.0);
2023            let next_v = fit_parameter(v + dv, va, vb, periodic.1);
2024            if next_u == u && next_v == v {
2025                break;
2026            }
2027            let trial = surface.evaluate(next_u, next_v)?.sub(point).length_squared();
2028            if trial <= current {
2029                moved = taken(next_u, u, ua, ub, periodic.0).abs() > 1e-15 * (ub - ua)
2030                    || taken(next_v, v, va, vb, periodic.1).abs() > 1e-15 * (vb - va);
2031                u = next_u;
2032                v = next_v;
2033                current = trial;
2034                break;
2035            }
2036            du *= 0.5;
2037            dv *= 0.5;
2038        }
2039        if !moved {
2040            break;
2041        }
2042    }
2043    let result = NewtonResult {
2044        u,
2045        v,
2046        distance_squared: current,
2047        converged,
2048    };
2049    let on_edge =
2050        (!periodic.0 && (u <= ua || u >= ub)) || (!periodic.1 && (v <= va || v >= vb));
2051    Ok((result, on_edge))
2052}
2053
2054/// The global stage of [`project_point_to_surface_general`]: replace `best`
2055/// with the least local minimum over every cell the hull bound admits.
2056#[allow(clippy::too_many_arguments)]
2057fn global_minimum(
2058    surface: &NurbsSurface,
2059    point: Vec3,
2060    domains: [f64; 4],
2061    periodic: (bool, bool),
2062    best: &mut NewtonResult,
2063    stats: &mut GlobalStats,
2064) -> Result<(), String> {
2065    if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2066        stats.settled_by_floor = true;
2067        return Ok(());
2068    }
2069    let Some(cells) = projection_cells(surface)? else {
2070        return Ok(());
2071    };
2072    let resolution = cells.resolution;
2073    let columns = cells.vs.len() - 1;
2074    let columns_u = cells.us.len() - 1;
2075    let mut bound = best.distance_squared.sqrt() - resolution;
2076
2077    // Every cell whose box comes nearer than the answer; the least bound over
2078    // what the walk excluded is the certificate.
2079    let mut candidates: Vec<(f64, usize)> = Vec::new();
2080    let mut excluded = f64::INFINITY;
2081    let mut stack: Vec<u32> = Vec::with_capacity(64);
2082    stack.push((cells.tree.len() - 1) as u32);
2083    while let Some(index) = stack.pop() {
2084        let node = cells.tree[index as usize];
2085        let sphere_bound = (point.sub(node.centre).length() - node.radius).max(0.0);
2086        if sphere_bound >= bound {
2087            excluded = excluded.min(sphere_bound);
2088            continue;
2089        }
2090        match node.children {
2091            Some((first, second)) => {
2092                stack.push(first);
2093                stack.push(second);
2094            }
2095            None => {
2096                let cell = node.cell as usize;
2097                let box_bound = cells.boxes[cell].lower_bound(point);
2098                if box_bound < bound {
2099                    candidates.push((box_bound, cell));
2100                } else {
2101                    excluded = excluded.min(box_bound);
2102                }
2103            }
2104        }
2105    }
2106    stats.candidates = candidates.len();
2107    candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
2108    stats.hull_lower_bound = candidates
2109        .first()
2110        .map_or(excluded, |&(least, _)| least.min(excluded));
2111
2112    // Cells already holding a verified minimum need no second descent. A foot
2113    // names ONE cell even when it sits on a lattice line shared by up to four:
2114    // a neighbour can hold a better basin of its own in its interior, and it
2115    // was admitted precisely because its bound beat this answer. The local
2116    // answer is one when it passes the same test the descent applies.
2117    let cell_of = |u: f64, v: f64| {
2118        let row = (cells.us.partition_point(|&value| value <= u).max(1) - 1).min(columns_u - 1);
2119        let column = (cells.vs.partition_point(|&value| value <= v).max(1) - 1).min(columns - 1);
2120        row * columns + column
2121    };
2122    let mut settled: Vec<usize> = Vec::new();
2123    if is_verified_minimum(surface, point, (best.u, best.v), domains, periodic)? {
2124        settled.push(cell_of(best.u, best.v));
2125    }
2126    for (cell_bound, cell) in candidates {
2127        if cell_bound >= bound {
2128            break;
2129        }
2130        let row = cell / columns;
2131        let column = cell % columns;
2132        let rectangle = [
2133            cells.us[row],
2134            cells.us[row + 1],
2135            cells.vs[column],
2136            cells.vs[column + 1],
2137        ];
2138        if settled.contains(&cell) {
2139            continue;
2140        }
2141        let mut seed = (rectangle[0], rectangle[2]);
2142        let mut seed_distance = f64::INFINITY;
2143        for (node_row, node_column) in
2144            [(row, column), (row + 1, column), (row, column + 1), (row + 1, column + 1)]
2145        {
2146            let distance = cells.nodes[node_row * cells.vs.len() + node_column]
2147                .sub(point)
2148                .length_squared();
2149            if distance < seed_distance {
2150                seed_distance = distance;
2151                seed = (cells.us[node_row], cells.vs[node_column]);
2152            }
2153        }
2154        stats.refinements += 1;
2155        let (confined, on_edge) = confined_descent(surface, point, seed, rectangle, (false, false))?;
2156        let mut found = confined;
2157        if on_edge && confined.distance_squared.sqrt() < bound {
2158            // The cell's least point is on its edge and beats the answer:
2159            // the basin continues past the cell.
2160            stats.continuations += 1;
2161            let (continued, _) =
2162                confined_descent(surface, point, (confined.u, confined.v), domains, periodic)?;
2163            if continued.distance_squared < found.distance_squared {
2164                found = continued;
2165            }
2166            if continued.converged {
2167                settled.push(cell_of(continued.u, continued.v));
2168            }
2169        }
2170        if found.distance_squared.sqrt() < bound {
2171            *best = found;
2172            bound = best.distance_squared.sqrt() - resolution;
2173            stats.replaced = true;
2174            if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2175                return Ok(());
2176            }
2177        }
2178    }
2179    Ok(())
2180}
2181
2182
2183
2184/// Domain handling for a local foot-point Newton walk.
2185#[derive(Clone, Copy)]
2186pub(crate) enum FootDomain {
2187    Clamp,
2188    Wrap,
2189    Extended,
2190}
2191
2192#[derive(Clone, Copy)]
2193pub(crate) struct CurveFootPolicy {
2194    pub domain: FootDomain,
2195    pub iterations: usize,
2196    pub slope_guard: f64,
2197    pub step_limit: f64,
2198    pub parameter_tolerance: f64,
2199    pub mode: FootMode,
2200}
2201
2202/// Acceptance and convergence differ between a distance minimum and a stationary root.
2203#[derive(Clone, Copy)]
2204pub(crate) enum FootMode {
2205    Monotone,
2206    Witness,
2207    Stationary { positive_slope: bool, relative_step: bool },
2208    Backtracked,
2209    Planar,
2210}
2211
2212/// Local Newton foot. Seeding and enumeration of local minima belong to the caller.
2213/// Returns the last parameter, best witnessed parameter, and squared distance.
2214fn curve_foot_newton_state(
2215    curve: &NurbsCurve,
2216    point: Vec3,
2217    seed: f64,
2218    bracket: [f64; 2],
2219    policy: CurveFootPolicy,
2220) -> Result<(f64, f64, f64), String> {
2221    let fit = |u| match policy.domain {
2222        FootDomain::Clamp => fit_parameter(u, bracket[0], bracket[1], false),
2223        FootDomain::Wrap => fit_parameter(u, bracket[0], bracket[1], true),
2224        FootDomain::Extended => u,
2225    };
2226    let extended = matches!(policy.domain, FootDomain::Extended) || matches!(policy.mode, FootMode::Backtracked);
2227    let planar = matches!(policy.mode, FootMode::Planar);
2228    let project = |p: Vec3| if planar { Vec3::new(p.x, p.y, 0.0) } else { p };
2229    let point = project(point);
2230    let distance = |u| -> Result<f64, String> {
2231        let p = if extended { curve.derivatives_extended(u, 0)?[0] } else { curve.evaluate(u)? };
2232        Ok(project(p).sub(point).length())
2233    };
2234    let monotone = matches!(policy.mode, FootMode::Monotone | FootMode::Backtracked);
2235    let witness = matches!(policy.mode, FootMode::Witness);
2236    let mut u = if witness { seed } else { fit(seed) };
2237    let mut nearest_u = seed;
2238    let mut nearest_squared = f64::INFINITY;
2239    let mut best = if monotone { distance(u)? } else { f64::INFINITY };
2240    for _ in 0..policy.iterations {
2241        let derivatives = if extended {
2242            let values = curve.derivatives_extended(u, 2)?;
2243            [values[0], values[1], values[2]]
2244        } else if matches!(policy.mode, FootMode::Stationary { .. })
2245            || (planar && !crate::curve::small_curve_derivatives_enabled()) {
2246            let values = curve.derivatives(u, 2)?;
2247            [values[0], values[1], values[2]]
2248        } else {
2249            curve.derivatives_small(u, 2)?
2250        }.map(project);
2251        let offset = derivatives[0].sub(point);
2252        let numerator = offset.dot(derivatives[1]);
2253        let denominator = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
2254        if witness {
2255            let squared = offset.length_squared();
2256            if squared < nearest_squared {
2257                nearest_u = u;
2258                nearest_squared = squared;
2259            }
2260            let residual_length = squared.sqrt();
2261            if residual_length <= LINEAR_TOLERANCE
2262                || numerator.abs() <= EPSILON + 1e-10 * derivatives[1].length() * residual_length {
2263                break;
2264            }
2265        }
2266        let singular = if planar { denominator.abs() < policy.slope_guard }
2267            else { denominator.abs() <= policy.slope_guard };
2268        if singular || (matches!(policy.mode, FootMode::Stationary { positive_slope: true, .. }) && !(denominator > 0.0)) { break; }
2269        let mut step = if matches!(policy.mode, FootMode::Backtracked) && !(denominator > 0.0) {
2270            policy.step_limit.copysign(numerator)
2271        } else {
2272            (numerator / denominator).clamp(-policy.step_limit, policy.step_limit)
2273        };
2274        let mut next = fit(u - step);
2275        let mut next_distance = if monotone { distance(next)? } else { f64::INFINITY };
2276        if matches!(policy.mode, FootMode::Backtracked) {
2277            for _ in 0..30 {
2278                if next_distance < best { break; }
2279                step *= 0.5;
2280                next = fit(u - step);
2281                next_distance = distance(next)?;
2282            }
2283            if !(next_distance < best) { break; }
2284        } else if monotone && next_distance >= best { break; }
2285        let moved = if planar || matches!(policy.domain, FootDomain::Extended) { step }
2286            else if matches!(policy.domain, FootDomain::Wrap) {
2287                let period = bracket[1] - bracket[0];
2288                let raw = next - u;
2289                raw - period * (raw / period).round()
2290            } else { next - u };
2291        let tolerance = if matches!(policy.mode, FootMode::Stationary { relative_step: true, .. }) {
2292            policy.parameter_tolerance * (1.0 + u.abs())
2293        } else { policy.parameter_tolerance };
2294        let small = if planar || matches!(policy.mode, FootMode::Backtracked) { moved.abs() < tolerance }
2295            else { moved.abs() <= tolerance };
2296        if small && matches!(policy.mode, FootMode::Monotone) { break; }
2297        u = next;
2298        best = next_distance;
2299        if small { break; }
2300    }
2301    if witness { return Ok((u, nearest_u, nearest_squared)); }
2302    if !monotone { best = distance(u)?; }
2303    Ok((u, u, best * best))
2304}
2305
2306pub(crate) fn curve_foot_newton(
2307    curve: &NurbsCurve, point: Vec3, seed: f64, bracket: [f64; 2], policy: CurveFootPolicy,
2308) -> Result<(f64, f64), String> {
2309    let (_, nearest, squared) = curve_foot_newton_state(curve, point, seed, bracket, policy)?;
2310    Ok((nearest, squared))
2311}
2312