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), LINEAR_TOLERANCE)
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    linear_tolerance: f64,
393) -> Result<NewtonResult, String> {
394    let [u0, u1, v0, v1] = domains;
395    let mut u = seed_u;
396    let mut v = seed_v;
397    let mut converged = false;
398    let mut best = NewtonResult {
399        u,
400        v,
401        distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
402        converged,
403    };
404    for iteration in 0..MAX_NEWTON_ITERATIONS {
405        crate::imprint::work_profile::add(|c| c.newton_iterations += 1);
406        let derivatives = surface.derivatives_small(u, v, if lazy { 0 } else { 2 })?;
407        let residual = derivatives[0][0].sub(point);
408        let distance_squared = residual.length_squared();
409        if distance_squared < best.distance_squared {
410            best.u = u;
411            best.v = v;
412            best.distance_squared = distance_squared;
413        }
414        let residual_length = distance_squared.sqrt();
415        if residual_length <= linear_tolerance {
416            crate::imprint::work_profile::add(|c| { c.newton_unused += 1; c.newton_unused_first += u64::from(iteration == 0); });
417            converged = true;
418            break;
419        }
420        let derivatives = if lazy { surface.derivatives_small(u, v, 2)? } else { derivatives };
421        let f = derivatives[1][0].dot(residual);
422        let g = derivatives[0][1].dot(residual);
423        let tangent_u_length = derivatives[1][0].length();
424        let tangent_v_length = derivatives[0][1].length();
425        let cosine_u = if tangent_u_length * residual_length <= EPSILON {
426            0.0
427        } else {
428            f.abs() / (tangent_u_length * residual_length)
429        };
430        let cosine_v = if tangent_v_length * residual_length <= EPSILON {
431            0.0
432        } else {
433            g.abs() / (tangent_v_length * residual_length)
434        };
435        if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
436            converged = true;
437            break;
438        }
439        let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
440        let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
441        let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
442        let determinant = j00 * j11 - j01 * j01;
443        if determinant.abs() <= EPSILON {
444            break;
445        }
446        let mut du = (-f * j11 + g * j01) / determinant;
447        let mut dv = (-g * j00 + f * j01) / determinant;
448        du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
449        dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
450        let next_u = fit_parameter(u + du, u0, u1, closed_u);
451        let next_v = fit_parameter(v + dv, v0, v1, closed_v);
452        let stalled =
453            (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
454        u = next_u;
455        v = next_v;
456        if stalled {
457            break;
458        }
459    }
460    let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
461    if final_distance_squared < best.distance_squared {
462        best.u = u;
463        best.v = v;
464        best.distance_squared = final_distance_squared;
465    }
466    best.converged = converged;
467    Ok(best)
468}
469
470/// Refine an existing branch foot for precision trim construction. The normal
471/// projector's distance exit is an on-carrier admission test, not a bound on
472/// tangential placement suitable for interpolating a more accurate trim.
473pub(crate) fn refine_trim_foot(
474    surface: &NurbsSurface, point: Vec3, foot: SurfaceProjection,
475) -> Result<SurfaceProjection, String> {
476    let [u0, u1] = surface.domain_u()?;
477    let [v0, v1] = surface.domain_v()?;
478    let (cu, cv) = surface.closed_directions()?;
479    let result = surface_newton_impl(surface, point, foot.u, foot.v,
480        [u0, u1, v0, v1], cu, cv, false, 1e-11)?;
481    let projected = surface.evaluate(result.u, result.v)?;
482    Ok(SurfaceProjection { u: result.u, v: result.v, point: projected,
483        distance: projected.sub(point).length() })
484}
485
486/// The point of `surface` nearest `point`, over the surface's whole domain.
487///
488/// Analytic carriers answer in closed form; every other surface goes through
489/// `project_point_to_surface_general`, which says what it guarantees.
490/// `#[track_caller]` names the call site in the `BREP_PROJECTION_AUDIT` log.
491#[track_caller]
492pub fn project_point_to_surface(
493    surface: &NurbsSurface,
494    point: Vec3,
495) -> Result<SurfaceProjection, String> {
496    crate::imprint::work_profile::add(|c| c.projection_global += 1);
497    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_PROJECT, surface);
498    // Recognized analytic carriers (plane / cylinder / cone / sphere /
499    // torus) have exact closed-form projections in the surface's own
500    // rational parameterization — skip grid seeding and Newton entirely.
501    if let Some(analytic) = surface.analytic() {
502        if let Some(projection) = analytic.project(surface, point) {
503            return Ok(projection);
504        }
505    }
506    if let Some(ellipsoid) = surface.affine_sphere() {
507        if let Some(projection) = ellipsoid.project(surface, point) {
508            return Ok(projection);
509        }
510    }
511    project_point_to_surface_general(surface, point)
512}
513
514/// Project a point onto a surface starting Newton from an explicit `(u, v)`
515/// guess, WITHOUT the global grid seed. This is a footpoint refiner for
516/// continuity-preserving curve-on-surface tracing: seeding each edge sample
517/// from its neighbour's parameters keeps the fit on ONE branch of a surface
518/// that folds back over the small trimmed patch, where an independent global
519/// search would snap to whichever fold is momentarily closest and tear the
520/// pcurve into a self-crossing zig-zag. The caller compares the returned
521/// `distance` against the global answer and only adopts this result when it is
522/// geometrically just as valid, so a bad seed can never make a fit worse.
523pub fn project_point_to_surface_seeded(
524    surface: &NurbsSurface,
525    point: Vec3,
526    seed_u: f64,
527    seed_v: f64,
528) -> Result<SurfaceProjection, String> {
529    crate::imprint::work_profile::add(|c| c.projection_seeded += 1);
530    let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
531    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_SEEDED, surface);
532    // An ellipsoid's foot is unique off its medial axis: the closed form IS
533    // the branch answer, as it is for the analytic carriers at the seeded
534    // entry (`project_point_to_surface_from_seed`).
535    if let Some(ellipsoid) = surface.affine_sphere() {
536        if let Some(projection) = ellipsoid.project(surface, point) {
537            return Ok(projection);
538        }
539    }
540    let [u0, u1] = surface.domain_u()?;
541    let [v0, v1] = surface.domain_v()?;
542    let (closed_u, closed_v) = surface.closed_directions()?;
543    let result = surface_newton(
544        surface,
545        point,
546        seed_u,
547        seed_v,
548        [u0, u1, v0, v1],
549        closed_u,
550        closed_v,
551    )?;
552    let projected = surface.evaluate(result.u, result.v)?;
553    Ok(SurfaceProjection {
554        u: result.u,
555        v: result.v,
556        point: projected,
557        distance: projected.sub(point).length(),
558    })
559}
560
561/// The foot of `point` on the branch of `seed`: the closed form on an analytic
562/// carrier, otherwise Newton from the caller's own `(u, v)`
563/// ([`project_point_to_surface_seeded`]), WITHOUT the global search.
564///
565/// **Which callers.** One that already holds a foot on this surface from a
566/// geometrically adjacent query and whose output needs continuity with it: a
567/// pcurve's previous sample, a march's previous station, a Newton's previous
568/// iterate. [`project_point_to_surface`] answers a different question — where
569/// on the WHOLE surface the point is nearest — and on a carrier that comes back
570/// near itself, or for an off-surface query about as far from two sheets, that
571/// answer is a foot on another branch: a pcurve sample on the wrong sheet, an
572/// iterate that jumps. A caller with no such foot (a vertex, a probe, the first
573/// station) asks the nearest-point question and calls the projector.
574///
575/// **An analytic carrier** answers in closed form, as the projector does. Its
576/// only places with two equally valid feet are its seam, its poles and its focal
577/// set (axis, centre, tube circle), and the closed form answers those exactly as
578/// the nearest-point query always has; no march on one changes.
579///
580/// **What it does not promise.** It is a local answer: its distance is an upper
581/// bound, and a Newton that stalls returns its best iterate without saying so.
582/// Every caller keeps its own on-surface check.
583///
584/// **Instruments**, each read once. `BREP_PROJECTION_FROM_SEED=0` returns
585/// [`project_point_to_surface`]'s answer at every caller (the reading before
586/// 2026-09-15, for a same-binary A/B); `BREP_PROJECTION_FROM_SEED_SITES=<a,b,…>`
587/// keeps the seeded answer only at the call sites whose `file:line` contains
588/// one of the substrings. Under `BREP_PROJECTION_AUDIT=<dir>` every query whose
589/// seeded foot lies farther than the on-surface floor from the nearest point's
590/// foot appends one line to `<dir>/projection-seed-audit-<pid>.tsv`. That
591/// comparand is the nearest-point projector under the same caller, so its own
592/// replacements land in `projection-audit-<pid>.tsv` at this site's line: they
593/// are the audit's, not an answer the caller took.
594#[track_caller]
595pub(crate) fn project_point_to_surface_from_seed(
596    surface: &NurbsSurface,
597    point: Vec3,
598    seed: [f64; 2],
599) -> Result<SurfaceProjection, String> {
600    crate::imprint::work_profile::add(|c| c.projection_from_seed += 1);
601    let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::SEEDED);
602    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_SEED, surface);
603    if let Some(analytic) = surface.analytic() {
604        if let Some(projection) = analytic.project(surface, point) {
605            return Ok(projection);
606        }
607    }
608    if let Some(ellipsoid) = surface.affine_sphere() {
609        if let Some(projection) = ellipsoid.project(surface, point) {
610            return Ok(projection);
611        }
612    }
613    let caller = std::panic::Location::caller();
614    if !seeded_answer_admitted(caller) {
615        return project_point_to_surface_general(surface, point);
616    }
617    let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
618    if seed_audit_enabled() {
619        let nearest = project_point_to_surface_general(surface, point)?;
620        if seeded.point.sub(nearest.point).length() > LINEAR_TOLERANCE {
621            seed_audit_record(caller, surface, seed, &seeded, &nearest);
622        }
623    }
624    Ok(seeded)
625}
626
627/// `BREP_PROJECTION_FROM_SEED=0` / `BREP_PROJECTION_FROM_SEED_SITES`: see
628/// [`project_point_to_surface_from_seed`].
629fn seeded_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
630    static SITES: std::sync::OnceLock<Option<Option<Vec<String>>>> = std::sync::OnceLock::new();
631    let sites = SITES.get_or_init(|| {
632        if std::env::var("BREP_PROJECTION_FROM_SEED").as_deref() == Ok("0") {
633            return None;
634        }
635        Some(std::env::var("BREP_PROJECTION_FROM_SEED_SITES").ok().map(|value| {
636            value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect()
637        }))
638    });
639    match sites {
640        None => false,
641        Some(None) => true,
642        Some(Some(only)) => {
643            let location = format!("{}:{}", caller.file(), caller.line());
644            only.iter().any(|site| location.contains(site.as_str()))
645        }
646    }
647}
648
649fn seed_audit_enabled() -> bool {
650    debug_flag!("BREP_PROJECTION_AUDIT")
651}
652
653/// One line per seeded answer whose foot is not the nearest point's: the call
654/// site, both distances, the seed, both feet's `(u, v)`, the separation of the
655/// two feet and the surface's shape.
656fn seed_audit_record(
657    caller: &std::panic::Location<'_>,
658    surface: &NurbsSurface,
659    seed: [f64; 2],
660    seeded: &SurfaceProjection,
661    nearest: &SurfaceProjection,
662) {
663    use std::io::Write;
664    static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
665        std::sync::OnceLock::new();
666    let sink = SINK.get_or_init(|| {
667        let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
668        let path = std::path::Path::new(&directory)
669            .join(format!("projection-seed-audit-{}.tsv", std::process::id()));
670        std::fs::OpenOptions::new()
671            .create(true)
672            .append(true)
673            .open(path)
674            .ok()
675            .map(std::sync::Mutex::new)
676    });
677    if let Some(file) = sink {
678        if let Ok(mut file) = file.lock() {
679            let _ = writeln!(
680                file,
681                "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.6e}\t{}x{} cp {}x{}",
682                caller.file(),
683                caller.line(),
684                seeded.distance,
685                nearest.distance,
686                seed[0],
687                seed[1],
688                seeded.u,
689                seeded.v,
690                nearest.u,
691                nearest.v,
692                seeded.point.sub(nearest.point).length(),
693                surface.degree_u,
694                surface.degree_v,
695                surface.control_points.len(),
696                surface.control_points.first().map(Vec::len).unwrap_or(0),
697            );
698        }
699    }
700}
701
702/// The Newton seed grid for general projection: (u, v, point) samples over
703/// every knot span. A pure function of the surface, cached on it — building
704/// the grid costs hundreds of evaluations and projection is the hottest
705/// entry point in the kernel.
706fn projection_seed_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
707    if let Some(grid) = surface.projection_grid.get() {
708        return Ok(grid);
709    }
710    let [u0, u1] = surface.domain_u()?;
711    let [v0, v1] = surface.domain_v()?;
712    let (closed_u, closed_v) = surface.closed_directions()?;
713    let mut breaks_u = vec![u0];
714    breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
715    breaks_u.push(u1);
716    let mut breaks_v = vec![v0];
717    breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
718    breaks_v.push(v1);
719    let samples_u = if closed_u {
720        8usize.max(surface.degree_u * 4)
721    } else {
722        3usize.max(surface.degree_u + 1)
723    };
724    let samples_v = if closed_v {
725        8usize.max(surface.degree_v * 4)
726    } else {
727        3usize.max(surface.degree_v + 1)
728    };
729    let mut grid = Vec::new();
730    for u_pair in breaks_u.windows(2) {
731        for v_pair in breaks_v.windows(2) {
732            for i in 0..=samples_u {
733                for j in 0..=samples_v {
734                    let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
735                    let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
736                    grid.push((u, v, surface.evaluate(u, v)?));
737                }
738            }
739        }
740    }
741    Ok(surface.projection_grid.get_or_init(|| SampleGrid::new(grid)))
742}
743
744/// The dense fallback grid for degenerate general projection: (u, v, point)
745/// over a `(count_u+1) × (count_v+1)` lattice with `count_u/count_v` derived
746/// from the surface's knot spans. A pure function of the surface, cached on
747/// it — on metre-unit-mm parts the degenerate fallback fires ~1.7M times and
748/// each re-evaluated all ~561 lattice points; caching them makes the argmin a
749/// pure distance scan over reused points (bit-identical seed).
750fn projection_dense_grid(surface: &NurbsSurface) -> Result<&SampleGrid, String> {
751    if let Some(grid) = surface.projection_dense_grid.get() {
752        return Ok(grid);
753    }
754    let [u0, u1] = surface.domain_u()?;
755    let [v0, v1] = surface.domain_v()?;
756    let mut breaks_u = vec![u0];
757    breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
758    breaks_u.push(u1);
759    let mut breaks_v = vec![v0];
760    breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
761    breaks_v.push(v1);
762    let count_u = 32usize.max((breaks_u.len() - 1) * 8);
763    let count_v = 16usize.max((breaks_v.len() - 1) * 8);
764    let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
765    for i in 0..=count_u {
766        for j in 0..=count_v {
767            let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
768            let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
769            grid.push((u, v, surface.evaluate(u, v)?));
770        }
771    }
772    Ok(surface.projection_dense_grid.get_or_init(|| SampleGrid::with_tree(grid)))
773}
774
775/// Samples per block of a [`SampleGrid`].
776const SAMPLE_BLOCK: usize = 32;
777
778/// Runs per group: the scan's second level, a box over [`GROUP_BLOCKS`]
779/// consecutive runs ([`SampleGrid::groups`]).
780const GROUP_BLOCKS: usize = 32;
781
782/// A cached `(u, v, point)` sample grid, with the axis-aligned box of each run
783/// of [`SAMPLE_BLOCK`] consecutive samples, so the nearest-sample scan
784/// ([`nearest_sample`]) skips a run none of whose samples can beat the answer
785/// it holds.
786#[derive(Clone, Debug)]
787pub(crate) struct SampleGrid {
788    samples: Vec<(f64, f64, Vec3)>,
789    blocks: Vec<SampleBlock>,
790    /// The union box of each [`GROUP_BLOCKS`] consecutive runs, prunable only
791    /// when every run in it is. A union box's floor is at most each of its
792    /// runs' floors as computed (the box faces are the runs' own, and the gap
793    /// to a face farther out is no larger, IEEE rounding being monotone).
794    groups: Vec<SampleBlock>,
795    /// The box tree over the runs ([`nearest_sample_tree`]): `tree[0]` boxes
796    /// each [`TREE_FANOUT`] consecutive runs, `tree[k]` each [`TREE_FANOUT`]
797    /// consecutive boxes of `tree[k - 1]`, up to a single root. Built only by
798    /// [`SampleGrid::with_tree`] (the dense grid), and empty when the grid has
799    /// at most one group of runs.
800    tree: Vec<Vec<SampleBlock>>,
801}
802
803/// Children per node of [`SampleGrid::tree`].
804const TREE_FANOUT: usize = 4;
805
806/// The union box of consecutive boxes, prunable only when each of them is.
807fn union_boxes(boxes: &[SampleBlock]) -> Vec<SampleBlock> {
808    boxes
809        .chunks(TREE_FANOUT)
810        .map(|chunk| {
811            let mut union = chunk[0];
812            for block in chunk {
813                union.low = Vec3::new(union.low.x.min(block.low.x), union.low.y.min(block.low.y), union.low.z.min(block.low.z));
814                union.high = Vec3::new(union.high.x.max(block.high.x), union.high.y.max(block.high.y), union.high.z.max(block.high.z));
815                union.prunable &= block.prunable;
816            }
817            union
818        })
819        .collect()
820}
821
822/// One run's box. `prunable` is false when a sample has a non-finite
823/// coordinate: the box says nothing about it, and the run is always scanned.
824#[derive(Clone, Copy, Debug)]
825struct SampleBlock {
826    low: Vec3,
827    high: Vec3,
828    prunable: bool,
829}
830
831impl SampleGrid {
832    fn new(samples: Vec<(f64, f64, Vec3)>) -> Self {
833        let blocks = samples
834            .chunks(SAMPLE_BLOCK)
835            .map(|chunk| {
836                let mut low = chunk[0].2;
837                let mut high = low;
838                let mut prunable = true;
839                for &(_, _, p) in chunk {
840                    prunable &= p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
841                    low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
842                    high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
843                }
844                SampleBlock { low, high, prunable }
845            })
846            .collect::<Vec<_>>();
847        let groups = blocks
848            .chunks(GROUP_BLOCKS)
849            .map(|chunk| {
850                let mut group = chunk[0];
851                for block in chunk {
852                    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));
853                    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));
854                    group.prunable &= block.prunable;
855                }
856                group
857            })
858            .collect();
859        Self { samples, blocks, groups, tree: Vec::new() }
860    }
861
862    /// The grid with its box tree, for a scan that starts from a held answer
863    /// (the dense fallback after a Newton): there the tree reads few floors.
864    /// A scan from no answer reads its bound from a greedy descent, which on a
865    /// surface whose boxes mostly hold the point lands on an arbitrary run;
866    /// ABC 00027566's seed scans took 75 s through the tree against 44 s
867    /// through the groups, so the seed grid keeps the groups.
868    fn with_tree(samples: Vec<(f64, f64, Vec3)>) -> Self {
869        let mut grid = Self::new(samples);
870        if grid.blocks.len() > GROUP_BLOCKS {
871            grid.tree.push(union_boxes(&grid.blocks));
872            while grid.tree.last().is_some_and(|level| level.len() > 1) {
873                let next = union_boxes(grid.tree.last().unwrap());
874                grid.tree.push(next);
875            }
876        }
877        grid
878    }
879}
880
881impl SampleBlock {
882    /// A floor on `sample.sub(point).length_squared()` over the run's samples,
883    /// as COMPUTED, not only as real numbers: per axis the gap from `point` to
884    /// the box is the same subtraction against the box face, and IEEE rounding
885    /// is monotone, so `|fl(s - p)| >= fl(face - p)`; squaring and the same
886    /// left-to-right sum of non-negative terms keep the order (`Vec3::dot`
887    /// has no fused multiply-add). A NaN `point` reads gaps of 0 and every
888    /// distance is NaN, which no scan takes, so skipping changes nothing.
889    fn lower_bound(&self, point: Vec3) -> f64 {
890        let gap = |p: f64, low: f64, high: f64| {
891            if p < low {
892                low - p
893            } else if p > high {
894                p - high
895            } else {
896                0.0
897            }
898        };
899        let x = gap(point.x, self.low.x, self.high.x);
900        let y = gap(point.y, self.low.y, self.high.y);
901        let z = gap(point.z, self.low.z, self.high.z);
902        x * x + y * y + z * z
903    }
904}
905
906/// `BREP_PROJECTION_PRUNED_SCAN=0` scans every sample, as before 2026-11-10,
907/// for a same-binary A/B. Read once per process.
908fn pruned_scan() -> bool {
909    switch!("BREP_PROJECTION_PRUNED_SCAN", true)
910}
911
912/// Replace `best` with the grid's nearest sample when that is strictly nearer,
913/// the first such in scan order: the bits of the linear scan, which this was
914/// until 2026-11-10. A run is skipped only when its box's floor is not below
915/// the distance held, so none of its samples could have replaced it (the
916/// strict `<`), or lies above the bound read first ([`bound_first_scan`]),
917/// and the runs are visited in scan order. ABC 00027566's
918/// 166-span rational faces take the dense fallback on 71% of their pcurve
919/// probes (the first Newton not converged), each scanning 22.7k samples: 9.1 s
920/// of a face's 11.5 s of local answers.
921fn nearest_sample(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult) {
922    if !grid.tree.is_empty() && scan_tree() && pruned_scan() && bound_first_scan() {
923        nearest_sample_tree(grid, point, best);
924        return;
925    }
926    nearest_sample_with(grid, point, best, pruned_scan(), bound_first_scan());
927}
928
929/// `BREP_PROJECTION_SCAN_TREE=0` scans through the two levels of groups and
930/// runs alone, as before 2026-11-11, for a same-binary A/B. Read once per
931/// process.
932///
933/// With it on, a grid built with a tree ([`SampleGrid::with_tree`]: the dense
934/// grid, when it has more than one group) is scanned through
935/// [`SampleGrid::tree`]. ABC 00027566's dense fallback on its 166-span faces
936/// passed 52 of 287 group boxes per call (a group is a strip across the
937/// surface, and many strips' boxes hold a point on it) and then read 1680 run
938/// floors to scan 0.65 runs, never improving the answer it held: 26 s of floors
939/// at one pcurve site.
940fn scan_tree() -> bool {
941    switch!("BREP_PROJECTION_SCAN_TREE", true)
942}
943
944/// `BREP_PROJECTION_SCAN_HINT=0` reads the bound from the descent alone, as
945/// before 2026-11-11, for a same-binary A/B. Read once per process.
946///
947/// With it on, a bound-first scan also reads the run that held the answer of
948/// the last scan of the same grid on this thread. Consecutive queries are
949/// mostly neighbours (a pcurve's probes): ABC 00027566's busiest pcurve site
950/// found its answer in the previous answer's run on 98.8% of seed scans, while
951/// the descent's run held it on 40%; with the hint the bound is the answer on
952/// 99.4% and the seed scans there fell from 43.6 s to 18.4 s.
953fn scan_hint() -> bool {
954    switch!("BREP_PROJECTION_SCAN_HINT", true)
955}
956
957const HINT_SLOTS: usize = 8;
958
959thread_local! {
960    /// Direct-mapped by grid address: (grid samples address, run of its last answer).
961    static SCAN_HINTS: std::cell::Cell<[(usize, usize); HINT_SLOTS]> =
962        const { std::cell::Cell::new([(0, usize::MAX); HINT_SLOTS]) };
963}
964
965fn hint_slot(grid: &SampleGrid) -> (usize, usize) {
966    let address = grid.samples.as_ptr() as usize;
967    (address, (address >> 4) % HINT_SLOTS)
968}
969
970/// The bound lowered by the hinted run's samples. The hint is only a run
971/// index: it is range-checked and read from the grid in hand, so a stale one
972/// (another grid at a reused address) costs time, never bits. It lowers the
973/// BOUND alone, never the answer held: each value it can take is a real
974/// sample distance, at least the grid's least distance `m`, which is all the
975/// bound-first argument asks of it.
976fn hinted_bound(grid: &SampleGrid, point: Vec3, mut bound: f64) -> f64 {
977    if !scan_hint() {
978        return bound;
979    }
980    let (address, slot) = hint_slot(grid);
981    let (held, run) = SCAN_HINTS.with(|hints| hints.get()[slot]);
982    if held != address || run >= grid.blocks.len() || !grid.blocks[run].prunable {
983        return bound;
984    }
985    let start = run * SAMPLE_BLOCK;
986    let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
987    for &(_, _, sample) in &grid.samples[start..end] {
988        let distance_squared = sample.sub(point).length_squared();
989        if distance_squared < bound {
990            bound = distance_squared;
991        }
992    }
993    bound
994}
995
996/// Remember the run that last improved the answer of a scan of `grid`.
997fn remember_hint(grid: &SampleGrid, run: Option<usize>) {
998    if let Some(run) = run.filter(|_| scan_hint()) {
999        let (address, slot) = hint_slot(grid);
1000        SCAN_HINTS.with(|hints| {
1001            let mut all = hints.get();
1002            all[slot] = (address, run);
1003            hints.set(all);
1004        });
1005    }
1006}
1007
1008/// [`nearest_sample_with`]'s bound-first scan over the box tree: the bound is
1009/// read from the run reached by descending from the root through the child of
1010/// lowest floor (a real sample distance, or the incoming one, so never below
1011/// the grid's least distance `m`), then the tree is visited depth first in
1012/// scan order, skipping a box whose floor is at or above the distance held or
1013/// strictly above the bound. A union box's floor is at most each of its
1014/// children's as computed (the box faces are theirs, and IEEE rounding is
1015/// monotone), so a box is skipped only when every run under it would have
1016/// been, and the runs that are read are read in scan order: the bits of the
1017/// linear scan, by the argument [`nearest_sample_with`] gives for its groups.
1018fn nearest_sample_tree(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult) {
1019    let top = grid.tree.len() - 1;
1020    let mut bound = best.distance_squared;
1021    // Descend: at each level the child of lowest floor among the prunable ones.
1022    let mut node = 0usize;
1023    let mut reached = true;
1024    for level in (0..=top).rev() {
1025        let children = |count: usize| node * TREE_FANOUT..((node + 1) * TREE_FANOUT).min(count);
1026        let below = if level == 0 { &grid.blocks } else { &grid.tree[level - 1] };
1027        match lowest_floor(below, children(below.len()), point) {
1028            Some(child) => node = child,
1029            None => {
1030                reached = false;
1031                break;
1032            }
1033        }
1034    }
1035    if reached {
1036        let start = node * SAMPLE_BLOCK;
1037        let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1038        for &(_, _, sample) in &grid.samples[start..end] {
1039            let distance_squared = sample.sub(point).length_squared();
1040            if distance_squared < bound {
1041                bound = distance_squared;
1042            }
1043        }
1044    }
1045    let bound = hinted_bound(grid, point, bound);
1046    let skip = |box_: &SampleBlock, held: f64| {
1047        if !box_.prunable {
1048            return false;
1049        }
1050        let floor = box_.lower_bound(point);
1051        floor >= held || floor > bound
1052    };
1053    let mut answer_run = None;
1054    // Depth first in scan order: (level, index), level 0 of the stack meaning a run.
1055    let mut stack: Vec<(usize, usize)> = Vec::with_capacity(TREE_FANOUT * (top + 2));
1056    stack.push((top + 1, 0));
1057    while let Some((level, index)) = stack.pop() {
1058        if level == 0 {
1059            if skip(&grid.blocks[index], best.distance_squared) {
1060                continue;
1061            }
1062            let start = index * SAMPLE_BLOCK;
1063            let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1064            let held = best.distance_squared;
1065            scan_run(&grid.samples[start..end], point, best);
1066            if best.distance_squared < held {
1067                answer_run = Some(index);
1068            }
1069            continue;
1070        }
1071        if skip(&grid.tree[level - 1][index], best.distance_squared) {
1072            continue;
1073        }
1074        let count = if level == 1 { grid.blocks.len() } else { grid.tree[level - 2].len() };
1075        let children = index * TREE_FANOUT..((index + 1) * TREE_FANOUT).min(count);
1076        // Pushed last-first so they pop in scan order.
1077        for child in children.rev() {
1078            stack.push((level - 1, child));
1079        }
1080    }
1081    remember_hint(grid, answer_run);
1082}
1083
1084/// `BREP_PROJECTION_BOUND_FIRST=0` prunes against the distance held alone, as
1085/// on 2026-11-10 before this pass, for a same-binary A/B. Read once per
1086/// process.
1087///
1088/// With it on, the runs are boxed again in groups of [`GROUP_BLOCKS`]. The scan
1089/// first reads the run of lowest floor inside the group of lowest floor and
1090/// takes the nearest of its samples as a BOUND, then visits the groups and
1091/// their runs in scan order, skipping any whose floor is at or above the
1092/// distance held or strictly above that bound. The runs alone started the seed
1093/// scan from no answer (its incoming distance is infinite), so every run ahead
1094/// of the nearest sample was read before the held distance fell, and every
1095/// scan computed one floor per run: on ABC 00027566 the seed
1096/// scans averaged 63k samples (361 s over 5M calls) and the dense ones 180k
1097/// (167 s over 3.5M), while a bound over the runs alone cut the first to 190 s
1098/// but doubled the second to 311 s with its extra pass of floors.
1099fn bound_first_scan() -> bool {
1100    switch!("BREP_PROJECTION_BOUND_FIRST", true)
1101}
1102
1103/// The scan behind [`nearest_sample`]. With `bound_first`, the bound is the
1104/// smaller of the incoming distance and one real sample distance, so it is
1105/// never below the grid's least distance `m`. The first sample at `m` lies in
1106/// a run whose floor is at most `m`, hence not above the bound and not at or
1107/// above the distance held when the scan reaches it (every sample before it is
1108/// strictly farther than `m`): that run is read and the sample taken, as the
1109/// linear scan takes it, and nothing after it is strictly nearer. A run whose
1110/// floor is above the bound holds no sample at `m`. The bound pass reads only
1111/// prunable runs, and updates on a strict `<`, so a NaN never enters it.
1112fn nearest_sample_with(grid: &SampleGrid, point: Vec3, best: &mut NewtonResult, pruned: bool, bound_first: bool) {
1113    if !(pruned && bound_first) {
1114        for (block, chunk) in grid.blocks.iter().zip(grid.samples.chunks(SAMPLE_BLOCK)) {
1115            if pruned && block.prunable && block.lower_bound(point) >= best.distance_squared {
1116                continue;
1117            }
1118            scan_run(chunk, point, best);
1119        }
1120        return;
1121    }
1122    // The bound: one real sample distance (or the incoming one), from the run
1123    // of lowest floor inside the group of lowest floor.
1124    let mut bound = best.distance_squared;
1125    let lowest_group = lowest_floor(&grid.groups, 0..grid.groups.len(), point);
1126    if let Some(group) = lowest_group {
1127        let runs = group * GROUP_BLOCKS..((group + 1) * GROUP_BLOCKS).min(grid.blocks.len());
1128        if let Some(run) = lowest_floor(&grid.blocks, runs, point) {
1129            let start = run * SAMPLE_BLOCK;
1130            let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1131            for &(_, _, sample) in &grid.samples[start..end] {
1132                let distance_squared = sample.sub(point).length_squared();
1133                if distance_squared < bound {
1134                    bound = distance_squared;
1135                }
1136            }
1137        }
1138    }
1139    let bound = hinted_bound(grid, point, bound);
1140    let skip = |box_: &SampleBlock, held: f64| {
1141        if !box_.prunable {
1142            return false;
1143        }
1144        let floor = box_.lower_bound(point);
1145        floor >= held || floor > bound
1146    };
1147    let mut answer_run = None;
1148    for (group_index, group) in grid.groups.iter().enumerate() {
1149        if skip(group, best.distance_squared) {
1150            continue;
1151        }
1152        let first = group_index * GROUP_BLOCKS;
1153        let last = (first + GROUP_BLOCKS).min(grid.blocks.len());
1154        for run in first..last {
1155            if skip(&grid.blocks[run], best.distance_squared) {
1156                continue;
1157            }
1158            let start = run * SAMPLE_BLOCK;
1159            let end = (start + SAMPLE_BLOCK).min(grid.samples.len());
1160            let held = best.distance_squared;
1161            scan_run(&grid.samples[start..end], point, best);
1162            if best.distance_squared < held {
1163                answer_run = Some(run);
1164            }
1165        }
1166    }
1167    remember_hint(grid, answer_run);
1168}
1169
1170/// The linear scan of one run: strict `<`, so the first of equal samples
1171/// stands.
1172fn scan_run(run: &[(f64, f64, Vec3)], point: Vec3, best: &mut NewtonResult) {
1173    for &(u, v, sample) in run {
1174        let distance_squared = sample.sub(point).length_squared();
1175        if distance_squared < best.distance_squared {
1176            best.u = u;
1177            best.v = v;
1178            best.distance_squared = distance_squared;
1179        }
1180    }
1181}
1182
1183/// The index, within `range`, of the prunable box of least floor (the first
1184/// on a tie), or None when none is prunable.
1185fn lowest_floor(boxes: &[SampleBlock], range: std::ops::Range<usize>, point: Vec3) -> Option<usize> {
1186    let mut lowest: Option<(usize, f64)> = None;
1187    for index in range {
1188        let box_ = &boxes[index];
1189        if !box_.prunable {
1190            continue;
1191        }
1192        let floor = box_.lower_bound(point);
1193        if lowest.is_none_or(|(_, held)| floor < held) {
1194            lowest = Some((index, floor));
1195        }
1196    }
1197    lowest.map(|(index, _)| index)
1198}
1199
1200// Boundary-ring cache geometry: 4 sides × 22 exponents × 33 indices. The scan
1201// positions depend only on the surface domain and these fixed integers, so the
1202// evaluated points are a pure function of the surface and cacheable.
1203const RING_EXPONENTS: usize = 22;
1204const RING_INDICES: usize = 33; // index 0..=32
1205
1206#[inline]
1207fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
1208    (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
1209}
1210
1211/// The boundary-ring sample points for the degenerate projection fallback,
1212/// flattened by `ring_slot(side, exponent-1, index)`. Sides: 0 = v-low
1213/// (`v = v0 + (v1-v0)·offset`), 1 = v-high (`v1 - (v1-v0)·offset`), 2 = u-low
1214/// (`u0 + (u1-u0)·offset`), 3 = u-high (`u1 - (u1-u0)·offset`), where
1215/// `offset = 1/2^exponent`. A pure function of the surface, cached on it — the
1216/// fallback's boundary ring re-evaluated these on every one of ~1.7M calls.
1217fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
1218    if let Some(grid) = surface.projection_ring_grid.get() {
1219        return Ok(grid);
1220    }
1221    let [u0, u1] = surface.domain_u()?;
1222    let [v0, v1] = surface.domain_v()?;
1223    let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
1224    for exponent in 1..=RING_EXPONENTS as i32 {
1225        let offset = 1.0 / 2f64.powi(exponent);
1226        let e = exponent as usize - 1;
1227        let v_lo = v0 + (v1 - v0) * offset;
1228        let v_hi = v1 - (v1 - v0) * offset;
1229        let u_lo = u0 + (u1 - u0) * offset;
1230        let u_hi = u1 - (u1 - u0) * offset;
1231        for index in 0..=32 {
1232            let u = u0 + (u1 - u0) * index as f64 / 32.0;
1233            grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
1234            grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
1235        }
1236        for index in 0..=32 {
1237            let v = v0 + (v1 - v0) * index as f64 / 32.0;
1238            grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
1239            grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
1240        }
1241    }
1242    Ok(surface.projection_ring_grid.get_or_init(|| grid))
1243}
1244
1245/// The LOCAL answer: Newton from the seed grid's single nearest sample (or the
1246/// linear-revolution foot), with the degenerate-parameterization fallbacks.
1247///
1248/// This is the whole of what the general projector returned before
1249/// 2026-09-14, and it is a local minimum, not the minimum: on a general patch
1250/// nearly closed in one direction the nearest sample can sit on the far end,
1251/// and Newton converges there (abc_00000026's thread flanks, 2.16e-2 .. 4.1e-2
1252/// against a true 1e-13). [`project_point_to_surface_general`] runs it first
1253/// and keeps its answer wherever no other basin beats it.
1254fn nearest_seed_answer(
1255    surface: &NurbsSurface,
1256    point: Vec3,
1257    [u0, u1, v0, v1]: [f64; 4],
1258    closed_u: bool,
1259    closed_v: bool,
1260) -> Result<NewtonResult, String> {
1261    let mut best = NewtonResult {
1262        u: u0,
1263        v: v0,
1264        distance_squared: f64::INFINITY,
1265        converged: false,
1266    };
1267    nearest_sample(projection_seed_grid(surface)?, point, &mut best);
1268    if let Some(revolution) = project_linear_revolution(surface, point)? {
1269        if revolution.distance * revolution.distance < best.distance_squared {
1270            best.u = revolution.u;
1271            best.v = revolution.v;
1272            best.distance_squared = revolution.distance * revolution.distance;
1273        }
1274    }
1275    let first_newton = surface_newton(
1276        surface,
1277        point,
1278        best.u,
1279        best.v,
1280        [u0, u1, v0, v1],
1281        closed_u,
1282        closed_v,
1283    )?;
1284    if first_newton.distance_squared < best.distance_squared {
1285        best = first_newton;
1286    }
1287    let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1288    let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
1289    if degenerate || !first_newton.converged {
1290        let mut grid = best;
1291        nearest_sample(projection_dense_grid(surface)?, point, &mut grid);
1292        if grid.distance_squared < best.distance_squared {
1293            best = grid;
1294        }
1295        let polished = surface_newton(
1296            surface,
1297            point,
1298            grid.u,
1299            grid.v,
1300            [u0, u1, v0, v1],
1301            closed_u,
1302            closed_v,
1303        )?;
1304        if polished.distance_squared < best.distance_squared {
1305            best = polished;
1306        }
1307
1308        let (_, su, sv) = surface.deriv1(best.u, best.v)?;
1309        if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
1310            let ring_grid = projection_ring_grid(surface)?;
1311            let mut ring = best;
1312            // Read the pre-evaluated ring points at the exact same parameter
1313            // positions the live scans used (side/exponent/index → `ring_slot`),
1314            // preserving scan order and argmin tie-breaking for bit-identity.
1315            let scan = |side: usize,
1316                        exponent_index: usize,
1317                        along_u: bool,
1318                        fixed: f64,
1319                        candidate: &mut NewtonResult| {
1320                for index in 0..=32 {
1321                    let moving = if along_u {
1322                        u0 + (u1 - u0) * index as f64 / 32.0
1323                    } else {
1324                        v0 + (v1 - v0) * index as f64 / 32.0
1325                    };
1326                    let (u, v) = if along_u {
1327                        (moving, fixed)
1328                    } else {
1329                        (fixed, moving)
1330                    };
1331                    let distance_squared = ring_grid
1332                        [ring_slot(side, exponent_index, index as usize)]
1333                    .sub(point)
1334                    .length_squared();
1335                    if distance_squared < candidate.distance_squared {
1336                        candidate.u = u;
1337                        candidate.v = v;
1338                        candidate.distance_squared = distance_squared;
1339                    }
1340                }
1341            };
1342            for exponent in 1..=22 {
1343                let offset = 1.0 / 2f64.powi(exponent);
1344                let e = exponent as usize - 1;
1345                if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
1346                    scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
1347                }
1348                if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
1349                    scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
1350                }
1351                if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
1352                    scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
1353                }
1354                if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
1355                    scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
1356                }
1357            }
1358            if ring.distance_squared < best.distance_squared {
1359                best = ring;
1360            }
1361            let polished = surface_newton(
1362                surface,
1363                point,
1364                ring.u,
1365                ring.v,
1366                [u0, u1, v0, v1],
1367                closed_u,
1368                closed_v,
1369            )?;
1370            if polished.distance_squared < best.distance_squared {
1371                best = polished;
1372            }
1373        }
1374    }
1375    Ok(best)
1376}
1377
1378/// Escape hatch for A/B measurement: `BREP_PROJECTION_GLOBAL=0` returns the
1379/// local answer alone ([`nearest_seed_answer`], the pre-2026-09-14 projector),
1380/// so a population run can be repeated with and without the global stage from
1381/// one binary. Read once.
1382fn global_stage_enabled() -> bool {
1383    switch!("BREP_PROJECTION_GLOBAL", true)
1384}
1385
1386/// `BREP_PROJECTION_GLOBAL_SITES=<a,b,…>`: the global stage's answer is kept
1387/// only for call sites whose `file:line` contains one of the comma-separated
1388/// substrings, and the local answer is returned everywhere else — so a result
1389/// that moves can be attributed to the call site that moved it.
1390/// `BREP_PROJECTION_GLOBAL_EXCEPT=<a,b,…>` is the complement: every site but
1391/// those. Unset: every site. Read once.
1392/// Whether a `BREP_PROJECTION_GLOBAL_SITES`/`_EXCEPT` filter is set, i.e.
1393/// whether [`project_point_to_surface`]'s answer can depend on its call site.
1394pub(crate) fn global_site_filter_active() -> bool {
1395    // Read once per process, like the site lists `global_answer_admitted`
1396    // reads: the common reader asks this for every probe (two `getenv` calls,
1397    // ~3.7% of the elephant's B12 prefix on the production sampler,
1398    // 2026-11-02), and nothing sets these variables after start-up.
1399    static ACTIVE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1400    *ACTIVE.get_or_init(|| {
1401        std::env::var_os("BREP_PROJECTION_GLOBAL_SITES").is_some() || std::env::var_os("BREP_PROJECTION_GLOBAL_EXCEPT").is_some()
1402    })
1403}
1404
1405fn global_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
1406    fn list(name: &str) -> Option<Vec<String>> {
1407        std::env::var(name)
1408            .ok()
1409            .map(|value| value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect())
1410    }
1411    static SITES: std::sync::OnceLock<(Option<Vec<String>>, Option<Vec<String>>)> =
1412        std::sync::OnceLock::new();
1413    let (only, except) = SITES.get_or_init(|| {
1414        (list("BREP_PROJECTION_GLOBAL_SITES"), list("BREP_PROJECTION_GLOBAL_EXCEPT"))
1415    });
1416    if only.is_none() && except.is_none() {
1417        return true;
1418    }
1419    let location = format!("{}:{}", caller.file(), caller.line());
1420    let named = |sites: &Vec<String>| sites.iter().any(|site| location.contains(site.as_str()));
1421    only.as_ref().is_none_or(named) && !except.as_ref().is_some_and(named)
1422}
1423
1424/// `BREP_PROJECTION_AUDIT=<dir>`: every query on which the global stage
1425/// replaces the local answer appends one tab-separated line to
1426/// `<dir>/projection-audit-<pid>.tsv` — the call site, the local and the
1427/// global distance, both feet's `(u, v)` and the surface's shape. Per process,
1428/// because the case gate forks a child per case and loses its stderr.
1429fn audit_record(caller: &std::panic::Location<'_>, surface: &NurbsSurface, local: &NewtonResult, global: &NewtonResult) {
1430    use std::io::Write;
1431    static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
1432        std::sync::OnceLock::new();
1433    let sink = SINK.get_or_init(|| {
1434        let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
1435        let path = std::path::Path::new(&directory)
1436            .join(format!("projection-audit-{}.tsv", std::process::id()));
1437        std::fs::OpenOptions::new()
1438            .create(true)
1439            .append(true)
1440            .open(path)
1441            .ok()
1442            .map(std::sync::Mutex::new)
1443    });
1444    if let Some(file) = sink {
1445        if let Ok(mut file) = file.lock() {
1446            let _ = writeln!(
1447                file,
1448                "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{}x{} cp {}x{}",
1449                caller.file(),
1450                caller.line(),
1451                local.distance_squared.sqrt(),
1452                global.distance_squared.sqrt(),
1453                local.u,
1454                local.v,
1455                global.u,
1456                global.v,
1457                surface.degree_u,
1458                surface.degree_v,
1459                surface.control_points.len(),
1460                surface.control_points.first().map(Vec::len).unwrap_or(0),
1461            );
1462        }
1463    }
1464}
1465
1466/// The point of a general surface nearest `point`: the least local minimum of
1467/// the distance over the surface's whole domain.
1468///
1469/// **What it does.** First the local answer ([`nearest_seed_answer`]: Newton
1470/// from the nearest seed-grid sample, with its degenerate fallbacks — the whole
1471/// projector before 2026-09-14). Then the global stage ([`global_minimum`]):
1472/// the domain is cut into the cells of the projection lattice (every knot span
1473/// sampled `max(3, degree + 1)` times in an open direction and
1474/// `max(8, 4 × degree)` times in a closed one — the seed grid's own density),
1475/// and each cell is enclosed in a box built from the control points of the
1476/// surface REFINED to that cell. A NURBS patch with positive weights lies in
1477/// the convex hull of the control points that support it, so the distance from
1478/// the query to a cell's box is a rigorous LOWER bound on the distance to every
1479/// surface point of the cell. Every cell whose bound is below the best answer
1480/// so far is a candidate basin, and is refined by a descent confined to the
1481/// cell ([`confined_descent`]: Newton where the Hessian is positive definite,
1482/// the metric-scaled gradient where it is not, halved until the distance does
1483/// not rise, so it can end at a minimum but not at a saddle or a maximum),
1484/// seeded at the cell's nearest lattice node and continued over the domain
1485/// when the cell's least point is on its edge. The least answer wins. A closed
1486/// direction's seam is not special: the lattice spans the whole period, the
1487/// cells on either side of the seam are ordinary cells, and the continuation
1488/// wraps.
1489///
1490/// **What is guaranteed.**
1491/// * The answer is a point ON the surface, so its distance never under-reads.
1492/// * Every cell that could hold a point nearer than the answer by more than the
1493///   resolution has been descended in, unless it already held a verified
1494///   minimum: a cell left out was excluded by its hull bound, which cannot
1495///   over-read. [`GlobalStats::hull_lower_bound`] is that certificate: the
1496///   true distance lies in `[hull_lower_bound, answer]`, always.
1497/// * **The one step not certified in general: one basin in the cell that holds
1498///   the least minimum.** A descent finds the basin of its seed. A second basin
1499///   in any OTHER cell costs nothing — that cell's better basin is still no
1500///   nearer than the least minimum — but a second local minimum in the cell
1501///   holding the least minimum can catch the descent seeded at that cell's
1502///   nearest lattice node, and the least minimum is missed by at most
1503///   `answer − hull_lower_bound`. That takes a query past the focal distance of
1504///   part of the cell (farther than its radius of normal curvature on the
1505///   query's side, where the distance's Hessian `G − d·II` stops being positive
1506///   definite) or a cell whose image turns back toward the query. A descent that
1507///   runs out of iterations without a verified minimum fails the same way, with
1508///   the same bound. Seeding every corner of every candidate cell was measured
1509///   and changed no answer on the bench.
1510/// * **Certified on the analytic carriers** — the sphere, torus, cylinder and
1511///   cone, each a revolution of a circle or a line, and the plane. A lattice cell
1512///   there is an (azimuth × generatrix) rectangle under half a turn wide. In the
1513///   cell holding the least minimum the azimuthal derivative of the squared
1514///   distance, `2 ρ(v) ρ_P sin(λ − λ_P)`, vanishes only on the query's own
1515///   meridian, and moving toward that meridian lowers the distance from every
1516///   other point of the cell, so each local minimum over the cell is a local
1517///   minimum of the planar distance from the query's meridian image to the
1518///   generatrix over the cell's `v` range — exactly one on a circle arc under
1519///   half a turn or on a segment. At the focal sets (the axis, the sphere's
1520///   centre, the tube's centre circle) the minimum is a continuum of equal
1521///   distances and every foot is right. A general profile of revolution and a
1522///   general patch are not certified.
1523/// * Where the local answer is already the least minimum, it is returned
1524///   unchanged, bit for bit: the global stage replaces it only with a point
1525///   nearer by more than the resolution.
1526///
1527/// **The two numbers it reads.** The *on-surface floor* is `LINEAR_TOLERANCE`,
1528/// INHERITED unchanged: it is the local Newton's own stopping residual, and a
1529/// local answer within it of the query is on the surface and nothing is
1530/// searched. The *resolution* is the one new number, and it is the surface's
1531/// own: `(degree_u + 1)(degree_v + 1) · ε · scale`, `scale` the largest
1532/// control-point coordinate magnitude — the rounding of one evaluation, which
1533/// sums that many terms of that size. Every box is grown by it, and no
1534/// improvement finer than it is taken.
1535///
1536/// **What it costs.** A query settled by the floor costs what the local answer
1537/// always cost. Any other query builds the cell enclosure once per surface (one
1538/// knot refinement of the net to the lattice, cached on the surface beside the
1539/// seed grid), walks a bounding-sphere tree over the cells, and runs one
1540/// descent per candidate cell the hull bound does not exclude.
1541#[track_caller]
1542pub(crate) fn project_point_to_surface_general(
1543    surface: &NurbsSurface,
1544    point: Vec3,
1545) -> Result<SurfaceProjection, String> {
1546    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_GENERAL, surface);
1547    let [u0, u1] = surface.domain_u()?;
1548    let [v0, v1] = surface.domain_v()?;
1549    let (closed_u, closed_v) = surface.closed_directions()?;
1550    let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1551    let mut best = local;
1552    if global_stage_enabled() {
1553        global_minimum(
1554            surface,
1555            point,
1556            [u0, u1, v0, v1],
1557            (closed_u, closed_v),
1558            &mut best,
1559            &mut GlobalStats::default(),
1560        )?;
1561        if best.u != local.u || best.v != local.v {
1562            let caller = std::panic::Location::caller();
1563            if global_answer_admitted(caller) {
1564                audit_record(caller, surface, &local, &best);
1565            } else {
1566                best = local;
1567            }
1568        }
1569    }
1570    let projected = surface.evaluate(best.u, best.v)?;
1571    Ok(SurfaceProjection {
1572        u: best.u,
1573        v: best.v,
1574        point: projected,
1575        distance: projected.sub(point).length(),
1576    })
1577}
1578
1579/// The single-seed inversion by itself: the closed form on an analytic
1580/// carrier, otherwise [`nearest_seed_answer`] — Newton from the seed grid's
1581/// nearest sample, WITHOUT the global stage. Its distance is an upper bound and
1582/// its foot a local minimum. It is not a nearest-point query: it exists for a
1583/// caller that asks a question ABOUT that inversion, and there is one —
1584/// `joint_branch_pcurves` (`io/step_import/builder/rims.rs`) offers a loop the
1585/// joint branch only when this inversion alternates between branches along it,
1586/// which it does on a carrier that comes back within one seed-grid spacing of
1587/// itself (`abc_00000026`'s nearly closed thread flanks).
1588pub(crate) fn project_point_to_surface_from_nearest_seed(
1589    surface: &NurbsSurface,
1590    point: Vec3,
1591) -> Result<SurfaceProjection, String> {
1592    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_NEAREST_SEED, surface);
1593    if let Some(analytic) = surface.analytic() {
1594        if let Some(projection) = analytic.project(surface, point) {
1595            return Ok(projection);
1596        }
1597    }
1598    if let Some(ellipsoid) = surface.affine_sphere() {
1599        if let Some(projection) = ellipsoid.project(surface, point) {
1600            return Ok(projection);
1601        }
1602    }
1603    let [u0, u1] = surface.domain_u()?;
1604    let [v0, v1] = surface.domain_v()?;
1605    let (closed_u, closed_v) = surface.closed_directions()?;
1606    let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1607    let projected = surface.evaluate(local.u, local.v)?;
1608    Ok(SurfaceProjection {
1609        u: local.u,
1610        v: local.v,
1611        point: projected,
1612        distance: projected.sub(point).length(),
1613    })
1614}
1615
1616/// Both answers for one query, and what the global stage did — the bench's
1617/// seam (`examples/projector_bench.rs`). Not for kernel callers.
1618#[doc(hidden)]
1619pub fn project_point_to_surface_general_lanes(
1620    surface: &NurbsSurface,
1621    point: Vec3,
1622    run_global: bool,
1623) -> Result<(SurfaceProjection, GlobalStats), String> {
1624    let [u0, u1] = surface.domain_u()?;
1625    let [v0, v1] = surface.domain_v()?;
1626    let (closed_u, closed_v) = surface.closed_directions()?;
1627    let mut best = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1628    let mut stats = GlobalStats::default();
1629    if run_global {
1630        global_minimum(surface, point, [u0, u1, v0, v1], (closed_u, closed_v), &mut best, &mut stats)?;
1631    }
1632    let projected = surface.evaluate(best.u, best.v)?;
1633    Ok((
1634        SurfaceProjection {
1635            u: best.u,
1636            v: best.v,
1637            point: projected,
1638            distance: projected.sub(point).length(),
1639        },
1640        stats,
1641    ))
1642}
1643
1644/// How many basins the cells of one query hold — the bench's measure of the
1645/// global stage's one uncertified step (see [`project_point_to_surface_general`]).
1646#[doc(hidden)]
1647#[derive(Clone, Debug, Default)]
1648pub struct BasinCensus {
1649    /// Cells whose hull bound is below the answer's distance: every cell the
1650    /// global stage could have descended in.
1651    pub cells: usize,
1652    /// Descents run: one from each corner of those cells and one from the middle.
1653    pub descents: usize,
1654    /// Descents that stopped without a verified minimum.
1655    pub unconverged: usize,
1656    /// Cells where two verified minima differ in distance by more than `bar`.
1657    /// Distances within the on-surface floor count as equal: the descent stops
1658    /// there, so two feet under it are one foot polished to different depths.
1659    pub cells_with_two_basins: usize,
1660    /// Of those, cells whose rectangle holds the least foot any descent found:
1661    /// the only cells where a second basin can cost the answer anything.
1662    pub foot_cells_with_two_basins: usize,
1663    /// The least distance any descent reached, and where.
1664    pub least: f64,
1665    pub least_uv: (f64, f64),
1666    /// Every descent in a two-basin cell that holds the least foot: the cell,
1667    /// its rectangle, the seed, where it stopped, its distance, and whether that
1668    /// is a verified minimum.
1669    pub foot_cell_descents: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)>,
1670}
1671
1672/// Descend in every cell whose hull bound is below `answer_distance` from each
1673/// of its corners and its middle, and count the cells whose verified minima
1674/// disagree by more than `bar`. For the bench only: it costs five descents per
1675/// cell, which the projector itself was measured not to need.
1676#[doc(hidden)]
1677pub fn project_point_to_surface_basin_census(
1678    surface: &NurbsSurface,
1679    point: Vec3,
1680    answer_distance: f64,
1681    bar: f64,
1682) -> Result<BasinCensus, String> {
1683    let mut census = BasinCensus {
1684        least: f64::INFINITY,
1685        ..BasinCensus::default()
1686    };
1687    let Some(cells) = projection_cells(surface)? else {
1688        return Ok(census);
1689    };
1690    let columns = cells.vs.len() - 1;
1691    let mut spreads: Vec<(usize, f64)> = Vec::new();
1692    let mut runs: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)> = Vec::new();
1693    for (cell, cell_box) in cells.boxes.iter().enumerate() {
1694        if cell_box.lower_bound(point) >= answer_distance {
1695            continue;
1696        }
1697        census.cells += 1;
1698        let (row, column) = (cell / columns, cell % columns);
1699        let rectangle = [cells.us[row], cells.us[row + 1], cells.vs[column], cells.vs[column + 1]];
1700        let middle = ((rectangle[0] + rectangle[1]) * 0.5, (rectangle[2] + rectangle[3]) * 0.5);
1701        let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
1702        for seed in [
1703            (rectangle[0], rectangle[2]),
1704            (rectangle[1], rectangle[2]),
1705            (rectangle[0], rectangle[3]),
1706            (rectangle[1], rectangle[3]),
1707            middle,
1708        ] {
1709            census.descents += 1;
1710            let (found, _) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1711            let distance = found.distance_squared.sqrt();
1712            runs.push((cell, rectangle, seed, (found.u, found.v), distance, found.converged));
1713            if distance < census.least {
1714                census.least = distance;
1715                census.least_uv = (found.u, found.v);
1716            }
1717            if !found.converged {
1718                census.unconverged += 1;
1719                continue;
1720            }
1721            low = low.min(distance.max(LINEAR_TOLERANCE));
1722            high = high.max(distance.max(LINEAR_TOLERANCE));
1723        }
1724        if high - low > bar {
1725            spreads.push((cell, high - low));
1726        }
1727    }
1728    census.cells_with_two_basins = spreads.len();
1729    let (u, v) = census.least_uv;
1730    let foot_cells: Vec<usize> = spreads
1731        .iter()
1732        .map(|&(cell, _)| cell)
1733        .filter(|&cell| {
1734            let (row, column) = (cell / columns, cell % columns);
1735            cells.us[row] <= u && u <= cells.us[row + 1] && cells.vs[column] <= v && v <= cells.vs[column + 1]
1736        })
1737        .collect();
1738    census.foot_cells_with_two_basins = foot_cells.len();
1739    census.foot_cell_descents = runs.into_iter().filter(|run| foot_cells.contains(&run.0)).collect();
1740    Ok(census)
1741}
1742
1743/// What the global stage did on one query.
1744#[doc(hidden)]
1745#[derive(Clone, Copy, Debug, Default)]
1746pub struct GlobalStats {
1747    /// The local answer was within the on-surface floor; nothing searched.
1748    pub settled_by_floor: bool,
1749    /// Cells whose hull bound admitted them.
1750    pub candidates: usize,
1751    /// Confined Newtons run (candidates not already holding a converged foot).
1752    pub refinements: usize,
1753    /// Continuations across the domain from a cell edge.
1754    pub continuations: usize,
1755    /// The global stage replaced the local answer.
1756    pub replaced: bool,
1757    /// The least hull bound over every cell — the true distance is never
1758    /// below it (0 when the floor settled the query).
1759    pub hull_lower_bound: f64,
1760}
1761
1762/// One lattice cell's enclosure: an oriented box around the control points
1763/// that support the cell, in an orthonormal frame fitted to the cell.
1764#[derive(Clone, Debug)]
1765struct CellBox {
1766    axes: [Vec3; 3],
1767    low: [f64; 3],
1768    high: [f64; 3],
1769}
1770
1771impl CellBox {
1772    /// Distance from `point` to the box: a lower bound on the distance to
1773    /// every surface point of the cell.
1774    fn lower_bound(&self, point: Vec3) -> f64 {
1775        let mut squared = 0.0;
1776        for axis in 0..3 {
1777            let coordinate = point.dot(self.axes[axis]);
1778            let excess = (self.low[axis] - coordinate).max(coordinate - self.high[axis]).max(0.0);
1779            squared += excess * excess;
1780        }
1781        squared.sqrt()
1782    }
1783}
1784
1785/// A node of the bounding-sphere tree over the cells. A leaf names its cell;
1786/// an internal node its two children.
1787#[derive(Clone, Copy, Debug)]
1788struct CellTreeNode {
1789    centre: Vec3,
1790    radius: f64,
1791    children: Option<(u32, u32)>,
1792    cell: u32,
1793}
1794
1795/// The cached enclosure of every projection lattice cell (see
1796/// [`project_point_to_surface_general`]).
1797#[derive(Clone, Debug)]
1798pub(crate) struct ProjectionCells {
1799    us: Vec<f64>,
1800    vs: Vec<f64>,
1801    /// `us.len() × vs.len()` lattice points, row-major in u.
1802    nodes: Vec<Vec3>,
1803    /// `(us.len() - 1) × (vs.len() - 1)` cells, row-major in u.
1804    boxes: Vec<CellBox>,
1805    /// The tree's root is the LAST node.
1806    tree: Vec<CellTreeNode>,
1807    /// One box per tree node: a leaf's is its cell's box, an internal node's
1808    /// holds both children's boxes grown by `node_margin` on every side (see
1809    /// [`hull_candidates`]). Empty when the surface has no usable scale.
1810    node_boxes: Vec<CellBox>,
1811    /// The growth of each internal node's box over its children's.
1812    node_margin: f64,
1813    /// The largest control-point coordinate magnitude.
1814    scale: f64,
1815    resolution: f64,
1816}
1817
1818/// The projection lattice's parameters along one direction: every knot span
1819/// cut into `samples` equal pieces, the seed grid's density, shared span ends
1820/// listed once.
1821fn lattice_parameters(knots: &[f64], degree: usize, closed: bool) -> Vec<f64> {
1822    let [start, end] = crate::curve::knot_domain(knots, degree);
1823    let samples = if closed {
1824        8usize.max(degree * 4)
1825    } else {
1826        3usize.max(degree + 1)
1827    };
1828    let mut breaks = vec![start];
1829    breaks.extend(interior_knots(knots, degree));
1830    breaks.push(end);
1831    let mut parameters = Vec::with_capacity((breaks.len() - 1) * samples + 1);
1832    for pair in breaks.windows(2) {
1833        for index in 0..samples {
1834            parameters.push(pair[0] + (pair[1] - pair[0]) * index as f64 / samples as f64);
1835        }
1836    }
1837    parameters.push(end);
1838    parameters
1839}
1840
1841/// Raise every lattice parameter to full multiplicity in the knot vector, so
1842/// each lattice cell is one Bézier piece of the refined net.
1843///
1844/// `net[i]` is the i-th control point along the refined direction, held as a
1845/// row across the other direction, and every row takes the same combination.
1846/// Knot refinement in one pass (Piegl & Tiller, *The NURBS Book*, A5.4), in
1847/// homogeneous coordinates, so a rational net refines exactly and the cost is
1848/// linear in the refined net's size.
1849fn refine_to_lattice(net: &mut Vec<Vec<Vec4>>, knots: &mut Vec<f64>, degree: usize, parameters: &[f64]) {
1850    let p = degree;
1851    let n = net.len() - 1;
1852    let mut inserted = Vec::new();
1853    for &parameter in parameters {
1854        let multiplicity = knots.iter().filter(|&&knot| knot == parameter).count();
1855        for _ in multiplicity..p {
1856            inserted.push(parameter);
1857        }
1858    }
1859    if inserted.is_empty() || p == 0 {
1860        return;
1861    }
1862    inserted.sort_by(f64::total_cmp);
1863    let find_span = |parameter: f64| -> usize {
1864        if parameter >= knots[n + 1] {
1865            return n;
1866        }
1867        (knots.partition_point(|&knot| knot <= parameter) - 1).clamp(p, n)
1868    };
1869    let r = inserted.len() - 1;
1870    let m = n + p + 1;
1871    let a = find_span(inserted[0]);
1872    let b = find_span(inserted[r]) + 1;
1873    let columns = net[0].len();
1874    let blank = vec![
1875        Vec4 {
1876            x: 0.0,
1877            y: 0.0,
1878            z: 0.0,
1879            w: 0.0,
1880        };
1881        columns
1882    ];
1883    let mut refined = vec![blank; n + r + 2];
1884    let mut refined_knots = vec![0.0; m + r + 2];
1885    for j in 0..=a - p {
1886        refined[j] = net[j].clone();
1887    }
1888    for j in b - 1..=n {
1889        refined[j + r + 1] = net[j].clone();
1890    }
1891    refined_knots[..=a].copy_from_slice(&knots[..=a]);
1892    for j in b + p..=m {
1893        refined_knots[j + r + 1] = knots[j];
1894    }
1895    let mut i = b + p - 1;
1896    let mut k = b + p + r;
1897    for j in (0..=r).rev() {
1898        while inserted[j] <= knots[i] && i > a {
1899            refined[k - p - 1] = net[i - p - 1].clone();
1900            refined_knots[k] = knots[i];
1901            k -= 1;
1902            i -= 1;
1903        }
1904        refined[k - p - 1] = refined[k - p].clone();
1905        for l in 1..=p {
1906            let index = k - p + l;
1907            let numerator = refined_knots[k + l] - inserted[j];
1908            if numerator == 0.0 {
1909                refined[index - 1] = refined[index].clone();
1910            } else {
1911                let alpha = numerator / (refined_knots[k + l] - knots[i - p + l]);
1912                let (head, tail) = refined.split_at_mut(index);
1913                for (before, &here) in head[index - 1].iter_mut().zip(&tail[0]) {
1914                    *before = before.scale(alpha).add(here.scale(1.0 - alpha));
1915                }
1916            }
1917        }
1918        refined_knots[k] = inserted[j];
1919        k = k.saturating_sub(1);
1920    }
1921    *net = refined;
1922    *knots = refined_knots;
1923}
1924
1925/// The control-point index range supporting the parameter interval
1926/// `[low, high]`: every basis function non-zero anywhere on it.
1927fn supporting_range(knots: &[f64], degree: usize, count: usize, low: f64, high: f64) -> (usize, usize) {
1928    let first_span = knots.partition_point(|&knot| knot <= low).saturating_sub(1);
1929    let last_span = knots.partition_point(|&knot| knot < high).saturating_sub(1);
1930    (
1931        first_span.saturating_sub(degree).min(count - 1),
1932        last_span.max(first_span).min(count - 1),
1933    )
1934}
1935
1936fn unit_or(vector: Vec3, fallback: Vec3) -> Vec3 {
1937    let length = vector.length();
1938    if length > 0.0 && length.is_finite() {
1939        vector.scale(1.0 / length)
1940    } else {
1941        fallback
1942    }
1943}
1944
1945fn enclosing_sphere(a: (Vec3, f64), b: (Vec3, f64)) -> (Vec3, f64) {
1946    let offset = b.0.sub(a.0);
1947    let separation = offset.length();
1948    if separation + b.1 <= a.1 {
1949        return a;
1950    }
1951    if separation + a.1 <= b.1 {
1952        return b;
1953    }
1954    let radius = (separation + a.1 + b.1) * 0.5;
1955    let centre = a.0.add(offset.scale((radius - a.1) / separation));
1956    (centre, radius)
1957}
1958
1959/// What [`build_cell_tree`] reads besides the spheres: the cells' boxes and
1960/// the lattice points (row-major, `lattice_columns` per row) that frame each
1961/// internal node's box, and the margin each internal box grows by.
1962struct NodeBoxInputs<'a> {
1963    boxes: &'a [CellBox],
1964    nodes: &'a [Vec3],
1965    lattice_columns: usize,
1966    margin: f64,
1967}
1968
1969/// The box of an internal node: framed like a cell's (along u, then the
1970/// normal of the region's lattice corners), spanning every corner of both
1971/// children's boxes, grown by `margin` on every side. Unbounded when a frame or
1972/// an extent is not finite, so it never prunes.
1973fn enclosing_node_box(corners: [Vec3; 4], children: [&CellBox; 2], margin: f64) -> CellBox {
1974    let unbounded = || CellBox {
1975        axes: [Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0)],
1976        low: [f64::NEG_INFINITY; 3],
1977        high: [f64::INFINITY; 3],
1978    };
1979    let [c00, c10, c01, c11] = corners;
1980    let along_u = c10.sub(c00).add(c11.sub(c01));
1981    let along_v = c01.sub(c00).add(c11.sub(c10));
1982    let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
1983    let Ok(fallback) = e1.perpendicular() else {
1984        return unbounded();
1985    };
1986    let normal = unit_or(e1.cross(along_v), fallback);
1987    let e2 = unit_or(normal.cross(e1), fallback);
1988    let e3 = e1.cross(e2);
1989    let axes = [e1, e2, e3];
1990    let mut low = [f64::INFINITY; 3];
1991    let mut high = [f64::NEG_INFINITY; 3];
1992    for child in children {
1993        for corner in 0..8 {
1994            let mut point = Vec3::default();
1995            for axis in 0..3 {
1996                let value = if corner >> axis & 1 == 0 { child.low[axis] } else { child.high[axis] };
1997                point = point.add(child.axes[axis].scale(value));
1998            }
1999            for axis in 0..3 {
2000                let coordinate = point.dot(axes[axis]);
2001                low[axis] = low[axis].min(coordinate);
2002                high[axis] = high[axis].max(coordinate);
2003            }
2004        }
2005    }
2006    for axis in 0..3 {
2007        low[axis] -= margin;
2008        high[axis] += margin;
2009    }
2010    if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
2011        return unbounded();
2012    }
2013    CellBox { axes, low, high }
2014}
2015
2016fn build_cell_tree(
2017    tree: &mut Vec<CellTreeNode>,
2018    node_boxes: &mut Vec<CellBox>,
2019    inputs: &NodeBoxInputs<'_>,
2020    spheres: &[(Vec3, f64)],
2021    columns: usize,
2022    [row_low, row_high, column_low, column_high]: [usize; 4],
2023) -> u32 {
2024    if row_high - row_low == 1 && column_high - column_low == 1 {
2025        let cell = row_low * columns + column_low;
2026        node_boxes.push(inputs.boxes[cell].clone());
2027        tree.push(CellTreeNode {
2028            centre: spheres[cell].0,
2029            radius: spheres[cell].1,
2030            children: None,
2031            cell: cell as u32,
2032        });
2033        return (tree.len() - 1) as u32;
2034    }
2035    let (first, second) = if row_high - row_low >= column_high - column_low {
2036        let middle = (row_low + row_high) / 2;
2037        (
2038            [row_low, middle, column_low, column_high],
2039            [middle, row_high, column_low, column_high],
2040        )
2041    } else {
2042        let middle = (column_low + column_high) / 2;
2043        (
2044            [row_low, row_high, column_low, middle],
2045            [row_low, row_high, middle, column_high],
2046        )
2047    };
2048    let first = build_cell_tree(tree, node_boxes, inputs, spheres, columns, first);
2049    let second = build_cell_tree(tree, node_boxes, inputs, spheres, columns, second);
2050    let a = tree[first as usize];
2051    let b = tree[second as usize];
2052    let lattice = |row: usize, column: usize| inputs.nodes[row * inputs.lattice_columns + column];
2053    node_boxes.push(enclosing_node_box(
2054        [
2055            lattice(row_low, column_low),
2056            lattice(row_high, column_low),
2057            lattice(row_low, column_high),
2058            lattice(row_high, column_high),
2059        ],
2060        [&node_boxes[first as usize], &node_boxes[second as usize]],
2061        inputs.margin,
2062    ));
2063    // The resolution-inflated leaf spheres already absorb rounding; the merge
2064    // adds one more rounding of the same order, absorbed by growing the radius
2065    // by the larger child's own ulp.
2066    let (centre, radius) = enclosing_sphere((a.centre, a.radius), (b.centre, b.radius));
2067    tree.push(CellTreeNode {
2068        centre,
2069        radius: radius * (1.0 + 4.0 * f64::EPSILON),
2070        children: Some((first, second)),
2071        cell: u32::MAX,
2072    });
2073    (tree.len() - 1) as u32
2074}
2075
2076fn projection_cells(surface: &NurbsSurface) -> Result<Option<&ProjectionCells>, String> {
2077    if let Some(cells) = surface.projection_cells.get() {
2078        return Ok(cells.as_ref());
2079    }
2080    let cells = build_projection_cells(surface)?;
2081    Ok(surface.projection_cells.get_or_init(|| cells).as_ref())
2082}
2083
2084fn build_projection_cells(surface: &NurbsSurface) -> Result<Option<ProjectionCells>, String> {
2085    let (closed_u, closed_v) = surface.closed_directions()?;
2086    let us = lattice_parameters(&surface.knots_u, surface.degree_u, closed_u);
2087    let vs = lattice_parameters(&surface.knots_v, surface.degree_v, closed_v);
2088    if us.len() < 2 || vs.len() < 2 {
2089        return Ok(None);
2090    }
2091    let mut scale = 0.0f64;
2092    for control in surface.control_points.iter().flatten() {
2093        let point = control.point()?;
2094        scale = scale.max(point.x.abs()).max(point.y.abs()).max(point.z.abs());
2095    }
2096    let resolution =
2097        ((surface.degree_u + 1) * (surface.degree_v + 1)) as f64 * f64::EPSILON * scale;
2098
2099    // Refine u (the net's rows), then v (its columns, as rows of the transpose).
2100    let mut net = surface.control_points.clone();
2101    let mut knots_u = surface.knots_u.clone();
2102    refine_to_lattice(&mut net, &mut knots_u, surface.degree_u, &us);
2103    let columns = net.first().map(Vec::len).unwrap_or(0);
2104    let mut transposed: Vec<Vec<Vec4>> =
2105        (0..columns).map(|column| net.iter().map(|row| row[column]).collect()).collect();
2106    let mut knots_v = surface.knots_v.clone();
2107    refine_to_lattice(&mut transposed, &mut knots_v, surface.degree_v, &vs);
2108    let rows = transposed.first().map(Vec::len).unwrap_or(0);
2109    let columns = transposed.len();
2110    if rows == 0 || columns == 0 {
2111        return Ok(None);
2112    }
2113    let mut points = vec![Vec3::default(); rows * columns];
2114    for (column, fibre) in transposed.iter().enumerate() {
2115        for (row, control) in fibre.iter().enumerate() {
2116            points[row * columns + column] = control.point()?;
2117        }
2118    }
2119
2120    let mut nodes = Vec::with_capacity(us.len() * vs.len());
2121    for &u in &us {
2122        for &v in &vs {
2123            nodes.push(surface.evaluate(u, v)?);
2124        }
2125    }
2126
2127    let cell_rows = us.len() - 1;
2128    let cell_columns = vs.len() - 1;
2129    let mut boxes = Vec::with_capacity(cell_rows * cell_columns);
2130    let mut spheres = Vec::with_capacity(cell_rows * cell_columns);
2131    for i in 0..cell_rows {
2132        let (row_low, row_high) =
2133            supporting_range(&knots_u, surface.degree_u, rows, us[i], us[i + 1]);
2134        for j in 0..cell_columns {
2135            let (column_low, column_high) =
2136                supporting_range(&knots_v, surface.degree_v, columns, vs[j], vs[j + 1]);
2137            let at = |row: usize, column: usize| points[row * columns + column];
2138            let c00 = at(row_low, column_low);
2139            let c10 = at(row_high, column_low);
2140            let c01 = at(row_low, column_high);
2141            let c11 = at(row_high, column_high);
2142            let along_u = c10.sub(c00).add(c11.sub(c01));
2143            let along_v = c01.sub(c00).add(c11.sub(c10));
2144            let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
2145            let normal = unit_or(e1.cross(along_v), e1.perpendicular()?);
2146            let e2 = unit_or(normal.cross(e1), e1.perpendicular()?);
2147            let e3 = e1.cross(e2);
2148            let axes = [e1, e2, e3];
2149            let mut low = [f64::INFINITY; 3];
2150            let mut high = [f64::NEG_INFINITY; 3];
2151            for row in row_low..=row_high {
2152                for column in column_low..=column_high {
2153                    let point = at(row, column);
2154                    for axis in 0..3 {
2155                        let coordinate = point.dot(axes[axis]);
2156                        low[axis] = low[axis].min(coordinate);
2157                        high[axis] = high[axis].max(coordinate);
2158                    }
2159                }
2160            }
2161            for axis in 0..3 {
2162                low[axis] -= resolution;
2163                high[axis] += resolution;
2164            }
2165            let mut centre = Vec3::default();
2166            let mut half_diagonal_squared = 0.0;
2167            for axis in 0..3 {
2168                centre = centre.add(axes[axis].scale((low[axis] + high[axis]) * 0.5));
2169                let half = (high[axis] - low[axis]) * 0.5;
2170                half_diagonal_squared += half * half;
2171            }
2172            if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
2173                return Ok(None);
2174            }
2175            spheres.push((centre, half_diagonal_squared.sqrt() + resolution));
2176            boxes.push(CellBox { axes, low, high });
2177        }
2178    }
2179    let mut tree = Vec::with_capacity(2 * boxes.len());
2180    let mut node_boxes = Vec::with_capacity(2 * boxes.len());
2181    // Each internal box grows by a billionth of the surface's scale over its
2182    // children: far above the rounding of the corners it spans and of the
2183    // bound read from it, so a box that excludes a point excludes every cell
2184    // under it (see `hull_candidates`).
2185    let node_margin = NODE_BOX_MARGIN * scale;
2186    let inputs = NodeBoxInputs { boxes: &boxes, nodes: &nodes, lattice_columns: vs.len(), margin: node_margin };
2187    build_cell_tree(&mut tree, &mut node_boxes, &inputs, &spheres, cell_columns, [0, cell_rows, 0, cell_columns]);
2188    if !(node_margin > 0.0 && node_margin.is_finite()) {
2189        node_boxes = Vec::new();
2190    }
2191    Ok(Some(ProjectionCells {
2192        us,
2193        vs,
2194        nodes,
2195        boxes,
2196        tree,
2197        node_boxes,
2198        node_margin,
2199        scale,
2200        resolution,
2201    }))
2202}
2203
2204/// First- and second-order optimality of `(u, v)` for the distance over the
2205/// rectangle: stationary on the coordinates the rectangle leaves free (the
2206/// descent's own cosine test), with a positive definite Hessian there. A
2207/// `periodic` direction has no edge to be pinned against: its seam is interior.
2208fn optimality(
2209    derivatives: &[[Vec3; 3]; 3],
2210    point: Vec3,
2211    (u, v): (f64, f64),
2212    [ua, ub, va, vb]: [f64; 4],
2213    periodic: (bool, bool),
2214) -> Optimality {
2215    let residual = derivatives[0][0].sub(point);
2216    let residual_length = residual.length();
2217    let su = derivatives[1][0];
2218    let sv = derivatives[0][1];
2219    let f = su.dot(residual);
2220    let g = sv.dot(residual);
2221    let metric_u = su.length_squared();
2222    let metric_v = sv.length_squared();
2223    let stationary_u = metric_u.sqrt() * residual_length <= EPSILON
2224        || f.abs() <= 1e-10 * metric_u.sqrt() * residual_length;
2225    let stationary_v = metric_v.sqrt() * residual_length <= EPSILON
2226        || g.abs() <= 1e-10 * metric_v.sqrt() * residual_length;
2227    let pinned_u = !periodic.0 && ((u <= ua && f > 0.0) || (u >= ub && f < 0.0));
2228    let pinned_v = !periodic.1 && ((v <= va && g > 0.0) || (v >= vb && g < 0.0));
2229    let j00 = derivatives[2][0].dot(residual) + metric_u;
2230    let j01 = derivatives[1][1].dot(residual) + su.dot(sv);
2231    let j11 = derivatives[0][2].dot(residual) + metric_v;
2232    let determinant = j00 * j11 - j01 * j01;
2233    let convex = match (!pinned_u, !pinned_v) {
2234        (true, true) => j00 > 0.0 && determinant > 0.0,
2235        (true, false) => j00 > 0.0,
2236        (false, true) => j11 > 0.0,
2237        (false, false) => true,
2238    };
2239    Optimality {
2240        residual_length,
2241        gradient: (f, g),
2242        metric: (metric_u, metric_v),
2243        hessian: (j00, j01, j11),
2244        determinant,
2245        pinned: (pinned_u, pinned_v),
2246        convex,
2247        minimum: residual_length <= LINEAR_TOLERANCE
2248            || ((stationary_u || pinned_u) && (stationary_v || pinned_v) && convex),
2249    }
2250}
2251
2252struct Optimality {
2253    residual_length: f64,
2254    gradient: (f64, f64),
2255    metric: (f64, f64),
2256    hessian: (f64, f64, f64),
2257    determinant: f64,
2258    pinned: (bool, bool),
2259    convex: bool,
2260    minimum: bool,
2261}
2262
2263fn is_verified_minimum(
2264    surface: &NurbsSurface,
2265    point: Vec3,
2266    at: (f64, f64),
2267    rectangle: [f64; 4],
2268    periodic: (bool, bool),
2269) -> Result<bool, String> {
2270    let derivatives = surface.derivatives_small(at.0, at.1, 2)?;
2271    Ok(optimality(&derivatives, point, at, rectangle, periodic).minimum)
2272}
2273
2274/// A descent to a local minimum of the distance over the rectangle
2275/// `[ua, ub] × [va, vb]`, from a seed inside it.
2276///
2277/// Newton on the gradient converges to ANY critical point — a saddle or a
2278/// maximum as readily as a minimum — and reads convergence there, so every
2279/// step here must lower the distance: the Newton step where the Hessian is
2280/// positive definite on the free coordinates, the metric-scaled gradient step
2281/// where it is not, halved until the distance does not rise. A coordinate
2282/// pinned at the rectangle's edge by a gradient pointing out of it is held
2283/// there and the other takes the one-dimensional step, so a minimum ON the edge
2284/// converges as one (the Karush-Kuhn-Tucker point) instead of stalling.
2285///
2286/// Returns the best iterate, whether it is a verified minimum (stationary on
2287/// the free coordinates with a positive definite Hessian there, or within the
2288/// on-surface floor), and whether it lies on the rectangle's edge.
2289fn confined_descent(
2290    surface: &NurbsSurface,
2291    point: Vec3,
2292    seed: (f64, f64),
2293    rectangle: [f64; 4],
2294    periodic: (bool, bool),
2295) -> Result<(NewtonResult, bool), String> {
2296    let [ua, ub, va, vb] = rectangle;
2297    let mut u = fit_parameter(seed.0, ua, ub, periodic.0);
2298    let mut v = fit_parameter(seed.1, va, vb, periodic.1);
2299    let mut converged = false;
2300    let mut current = surface.evaluate(u, v)?.sub(point).length_squared();
2301    for _ in 0..MAX_NEWTON_ITERATIONS {
2302        let derivatives = surface.derivatives_small(u, v, 2)?;
2303        let state = optimality(&derivatives, point, (u, v), rectangle, periodic);
2304        current = state.residual_length * state.residual_length;
2305        if state.minimum {
2306            converged = true;
2307            break;
2308        }
2309        let (f, g) = state.gradient;
2310        let (metric_u, metric_v) = state.metric;
2311        let (j00, j01, j11) = state.hessian;
2312        let determinant = state.determinant;
2313        let (free_u, free_v) = (!state.pinned.0, !state.pinned.1);
2314        let (mut du, mut dv) = match (free_u, free_v) {
2315            (true, true) if state.convex => (
2316                (-f * j11 + g * j01) / determinant,
2317                (-g * j00 + f * j01) / determinant,
2318            ),
2319            (true, false) if state.convex => (-f / j00, 0.0),
2320            (false, true) if state.convex => (0.0, -g / j11),
2321            _ => (
2322                if free_u && metric_u > 0.0 { -f / metric_u } else { 0.0 },
2323                if free_v && metric_v > 0.0 { -g / metric_v } else { 0.0 },
2324            ),
2325        };
2326        if !(du.is_finite() && dv.is_finite()) || (du == 0.0 && dv == 0.0) {
2327            break;
2328        }
2329        let mut moved = false;
2330        // The step actually taken, a whole period removed across a seam.
2331        let taken = |next: f64, here: f64, low: f64, high: f64, wraps: bool| {
2332            let raw = next - here;
2333            if wraps {
2334                raw - (high - low) * (raw / (high - low)).round()
2335            } else {
2336                raw
2337            }
2338        };
2339        for _ in 0..40 {
2340            let next_u = fit_parameter(u + du, ua, ub, periodic.0);
2341            let next_v = fit_parameter(v + dv, va, vb, periodic.1);
2342            if next_u == u && next_v == v {
2343                break;
2344            }
2345            let trial = surface.evaluate(next_u, next_v)?.sub(point).length_squared();
2346            if trial <= current {
2347                moved = taken(next_u, u, ua, ub, periodic.0).abs() > 1e-15 * (ub - ua)
2348                    || taken(next_v, v, va, vb, periodic.1).abs() > 1e-15 * (vb - va);
2349                u = next_u;
2350                v = next_v;
2351                current = trial;
2352                break;
2353            }
2354            du *= 0.5;
2355            dv *= 0.5;
2356        }
2357        if !moved {
2358            break;
2359        }
2360    }
2361    let result = NewtonResult {
2362        u,
2363        v,
2364        distance_squared: current,
2365        converged,
2366    };
2367    let on_edge =
2368        (!periodic.0 && (u <= ua || u >= ub)) || (!periodic.1 && (v <= va || v >= vb));
2369    Ok((result, on_edge))
2370}
2371
2372/// The growth of an internal node's box over its children's, per unit of
2373/// the surface's scale.
2374const NODE_BOX_MARGIN: f64 = 1e-9;
2375
2376/// `BREP_PROJECTION_NODE_BOXES` (default on): the cell-tree walk also reads
2377/// each internal node's box, not only its sphere. `=0` walks on spheres alone,
2378/// as before. Read once.
2379fn node_boxes_enabled() -> bool {
2380    switch!("BREP_PROJECTION_NODE_BOXES", true)
2381}
2382
2383/// The walk of the global stage: every cell whose box comes nearer `point`
2384/// than `bound`, with its box bound, in the walk's order, and the least bound
2385/// over everything the walk excluded.
2386///
2387/// A node is excluded when its sphere is no nearer than `bound`, and, with
2388/// `node_boxes`, when its box is not either. Cell spheres are loose on long,
2389/// thin cells (a 16 × 5632 lattice: about 3000 nodes visited and 200 cells
2390/// read per query to admit one), and an internal box is not. The box test
2391/// only removes subtrees whose every cell the cell test would reject, in the
2392/// same order, so the candidates are the same list to the bit with and
2393/// without it. Each internal box holds its children's boxes grown by
2394/// `node_margin` (`m`), and both bounds are read within `e` of their exact
2395/// value, `e` (below) set by the point's and the surface's magnitudes. A
2396/// box read at `b ≥ bound` with `b > 2e` is truly at least `b − e > 0` away,
2397/// so each cell box under it is truly at least `b − e + m` away and reads at
2398/// least `b − 2e + m ≥ bound` whenever `m ≥ 2e`; the test is skipped for a
2399/// point so far out that `4e > m` (beyond about 3·10⁴ scales), keeping half
2400/// of `m` for the rounding of the box corners themselves. Only the excluded
2401/// bound (the certificate in `GlobalStats::hull_lower_bound`) can differ: it
2402/// is still a lower bound, read from a tighter box.
2403fn hull_candidates(cells: &ProjectionCells, point: Vec3, bound: f64, node_boxes: bool) -> (Vec<(f64, usize)>, f64) {
2404    let read_error = 32.0 * f64::EPSILON * (point.x.abs() + point.y.abs() + point.z.abs() + 4.0 * cells.scale);
2405    let boxes = node_boxes && !cells.node_boxes.is_empty() && 4.0 * read_error <= cells.node_margin;
2406    let mut candidates: Vec<(f64, usize)> = Vec::new();
2407    let mut excluded = f64::INFINITY;
2408    let mut stack: Vec<u32> = Vec::with_capacity(64);
2409    stack.push((cells.tree.len() - 1) as u32);
2410    while let Some(index) = stack.pop() {
2411        let node = cells.tree[index as usize];
2412        let sphere_bound = (point.sub(node.centre).length() - node.radius).max(0.0);
2413        if sphere_bound >= bound {
2414            excluded = excluded.min(sphere_bound);
2415            continue;
2416        }
2417        match node.children {
2418            Some((first, second)) => {
2419                if boxes {
2420                    let box_bound = cells.node_boxes[index as usize].lower_bound(point);
2421                    if box_bound >= bound && box_bound > 2.0 * read_error {
2422                        excluded = excluded.min(box_bound);
2423                        continue;
2424                    }
2425                }
2426                stack.push(first);
2427                stack.push(second);
2428            }
2429            None => {
2430                let cell = node.cell as usize;
2431                let box_bound = cells.boxes[cell].lower_bound(point);
2432                if box_bound < bound {
2433                    candidates.push((box_bound, cell));
2434                } else {
2435                    excluded = excluded.min(box_bound);
2436                }
2437            }
2438        }
2439    }
2440    (candidates, excluded)
2441}
2442
2443/// The global stage of [`project_point_to_surface_general`]: replace `best`
2444/// with the least local minimum over every cell the hull bound admits.
2445#[allow(clippy::too_many_arguments)]
2446fn global_minimum(
2447    surface: &NurbsSurface,
2448    point: Vec3,
2449    domains: [f64; 4],
2450    periodic: (bool, bool),
2451    best: &mut NewtonResult,
2452    stats: &mut GlobalStats,
2453) -> Result<(), String> {
2454    if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2455        stats.settled_by_floor = true;
2456        return Ok(());
2457    }
2458    let Some(cells) = projection_cells(surface)? else {
2459        return Ok(());
2460    };
2461    let resolution = cells.resolution;
2462    let columns = cells.vs.len() - 1;
2463    let columns_u = cells.us.len() - 1;
2464    let mut bound = best.distance_squared.sqrt() - resolution;
2465
2466    // Every cell whose box comes nearer than the answer; the least bound over
2467    // what the walk excluded is the certificate.
2468    let (mut candidates, excluded) = hull_candidates(cells, point, bound, node_boxes_enabled());
2469    stats.candidates = candidates.len();
2470    candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
2471    stats.hull_lower_bound = candidates
2472        .first()
2473        .map_or(excluded, |&(least, _)| least.min(excluded));
2474
2475    // Cells already holding a verified minimum need no second descent. A foot
2476    // names ONE cell even when it sits on a lattice line shared by up to four:
2477    // a neighbour can hold a better basin of its own in its interior, and it
2478    // was admitted precisely because its bound beat this answer. The local
2479    // answer is one when it passes the same test the descent applies.
2480    let cell_of = |u: f64, v: f64| {
2481        let row = (cells.us.partition_point(|&value| value <= u).max(1) - 1).min(columns_u - 1);
2482        let column = (cells.vs.partition_point(|&value| value <= v).max(1) - 1).min(columns - 1);
2483        row * columns + column
2484    };
2485    let mut settled: Vec<usize> = Vec::new();
2486    if is_verified_minimum(surface, point, (best.u, best.v), domains, periodic)? {
2487        settled.push(cell_of(best.u, best.v));
2488    }
2489    for (cell_bound, cell) in candidates {
2490        if cell_bound >= bound {
2491            break;
2492        }
2493        let row = cell / columns;
2494        let column = cell % columns;
2495        let rectangle = [
2496            cells.us[row],
2497            cells.us[row + 1],
2498            cells.vs[column],
2499            cells.vs[column + 1],
2500        ];
2501        if settled.contains(&cell) {
2502            continue;
2503        }
2504        let mut seed = (rectangle[0], rectangle[2]);
2505        let mut seed_distance = f64::INFINITY;
2506        for (node_row, node_column) in
2507            [(row, column), (row + 1, column), (row, column + 1), (row + 1, column + 1)]
2508        {
2509            let distance = cells.nodes[node_row * cells.vs.len() + node_column]
2510                .sub(point)
2511                .length_squared();
2512            if distance < seed_distance {
2513                seed_distance = distance;
2514                seed = (cells.us[node_row], cells.vs[node_column]);
2515            }
2516        }
2517        stats.refinements += 1;
2518        let (confined, on_edge) = confined_descent(surface, point, seed, rectangle, (false, false))?;
2519        let mut found = confined;
2520        if on_edge && confined.distance_squared.sqrt() < bound {
2521            // The cell's least point is on its edge and beats the answer:
2522            // the basin continues past the cell.
2523            stats.continuations += 1;
2524            let (continued, _) =
2525                confined_descent(surface, point, (confined.u, confined.v), domains, periodic)?;
2526            if continued.distance_squared < found.distance_squared {
2527                found = continued;
2528            }
2529            if continued.converged {
2530                settled.push(cell_of(continued.u, continued.v));
2531            }
2532        }
2533        if found.distance_squared.sqrt() < bound {
2534            *best = found;
2535            bound = best.distance_squared.sqrt() - resolution;
2536            stats.replaced = true;
2537            if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
2538                return Ok(());
2539            }
2540        }
2541    }
2542    Ok(())
2543}
2544
2545
2546
2547/// Domain handling for a local foot-point Newton walk.
2548#[derive(Clone, Copy)]
2549pub(crate) enum FootDomain {
2550    Clamp,
2551    Wrap,
2552    Extended,
2553}
2554
2555#[derive(Clone, Copy)]
2556pub(crate) struct CurveFootPolicy {
2557    pub domain: FootDomain,
2558    pub iterations: usize,
2559    pub slope_guard: f64,
2560    pub step_limit: f64,
2561    pub parameter_tolerance: f64,
2562    pub mode: FootMode,
2563}
2564
2565/// Acceptance and convergence differ between a distance minimum and a stationary root.
2566#[derive(Clone, Copy)]
2567pub(crate) enum FootMode {
2568    Monotone,
2569    Witness,
2570    Stationary { positive_slope: bool, relative_step: bool },
2571    Backtracked,
2572    Planar,
2573}
2574
2575/// Local Newton foot. Seeding and enumeration of local minima belong to the caller.
2576/// Returns the last parameter, best witnessed parameter, and squared distance.
2577fn curve_foot_newton_state(
2578    curve: &NurbsCurve,
2579    point: Vec3,
2580    seed: f64,
2581    bracket: [f64; 2],
2582    policy: CurveFootPolicy,
2583) -> Result<(f64, f64, f64), String> {
2584    let fit = |u| match policy.domain {
2585        FootDomain::Clamp => fit_parameter(u, bracket[0], bracket[1], false),
2586        FootDomain::Wrap => fit_parameter(u, bracket[0], bracket[1], true),
2587        FootDomain::Extended => u,
2588    };
2589    let extended = matches!(policy.domain, FootDomain::Extended) || matches!(policy.mode, FootMode::Backtracked);
2590    let planar = matches!(policy.mode, FootMode::Planar);
2591    let project = |p: Vec3| if planar { Vec3::new(p.x, p.y, 0.0) } else { p };
2592    let point = project(point);
2593    let distance = |u| -> Result<f64, String> {
2594        let p = if extended { curve.derivatives_extended(u, 0)?[0] } else { curve.evaluate(u)? };
2595        Ok(project(p).sub(point).length())
2596    };
2597    let monotone = matches!(policy.mode, FootMode::Monotone | FootMode::Backtracked);
2598    let witness = matches!(policy.mode, FootMode::Witness);
2599    let mut u = if witness { seed } else { fit(seed) };
2600    let mut nearest_u = seed;
2601    let mut nearest_squared = f64::INFINITY;
2602    let mut best = if monotone { distance(u)? } else { f64::INFINITY };
2603    for _ in 0..policy.iterations {
2604        let derivatives = if extended {
2605            let values = curve.derivatives_extended(u, 2)?;
2606            [values[0], values[1], values[2]]
2607        } else if matches!(policy.mode, FootMode::Stationary { .. })
2608            || (planar && !crate::curve::small_curve_derivatives_enabled()) {
2609            let values = curve.derivatives(u, 2)?;
2610            [values[0], values[1], values[2]]
2611        } else {
2612            curve.derivatives_small(u, 2)?
2613        }.map(project);
2614        let offset = derivatives[0].sub(point);
2615        let numerator = offset.dot(derivatives[1]);
2616        let denominator = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
2617        if witness {
2618            let squared = offset.length_squared();
2619            if squared < nearest_squared {
2620                nearest_u = u;
2621                nearest_squared = squared;
2622            }
2623            let residual_length = squared.sqrt();
2624            if residual_length <= LINEAR_TOLERANCE
2625                || numerator.abs() <= EPSILON + 1e-10 * derivatives[1].length() * residual_length {
2626                break;
2627            }
2628        }
2629        let singular = if planar { denominator.abs() < policy.slope_guard }
2630            else { denominator.abs() <= policy.slope_guard };
2631        if singular || (matches!(policy.mode, FootMode::Stationary { positive_slope: true, .. }) && !(denominator > 0.0)) { break; }
2632        let mut step = if matches!(policy.mode, FootMode::Backtracked) && !(denominator > 0.0) {
2633            policy.step_limit.copysign(numerator)
2634        } else {
2635            (numerator / denominator).clamp(-policy.step_limit, policy.step_limit)
2636        };
2637        let mut next = fit(u - step);
2638        let mut next_distance = if monotone { distance(next)? } else { f64::INFINITY };
2639        if matches!(policy.mode, FootMode::Backtracked) {
2640            for _ in 0..30 {
2641                if next_distance < best { break; }
2642                step *= 0.5;
2643                next = fit(u - step);
2644                next_distance = distance(next)?;
2645            }
2646            if !(next_distance < best) { break; }
2647        } else if monotone && next_distance >= best { break; }
2648        let moved = if planar || matches!(policy.domain, FootDomain::Extended) { step }
2649            else if matches!(policy.domain, FootDomain::Wrap) {
2650                let period = bracket[1] - bracket[0];
2651                let raw = next - u;
2652                raw - period * (raw / period).round()
2653            } else { next - u };
2654        let tolerance = if matches!(policy.mode, FootMode::Stationary { relative_step: true, .. }) {
2655            policy.parameter_tolerance * (1.0 + u.abs())
2656        } else { policy.parameter_tolerance };
2657        let small = if planar || matches!(policy.mode, FootMode::Backtracked) { moved.abs() < tolerance }
2658            else { moved.abs() <= tolerance };
2659        if small && matches!(policy.mode, FootMode::Monotone) { break; }
2660        u = next;
2661        best = next_distance;
2662        if small { break; }
2663    }
2664    if witness { return Ok((u, nearest_u, nearest_squared)); }
2665    if !monotone { best = distance(u)?; }
2666    Ok((u, u, best * best))
2667}
2668
2669pub(crate) fn curve_foot_newton(
2670    curve: &NurbsCurve, point: Vec3, seed: f64, bracket: [f64; 2], policy: CurveFootPolicy,
2671) -> Result<(f64, f64), String> {
2672    let (_, nearest, squared) = curve_foot_newton_state(curve, point, seed, bracket, policy)?;
2673    Ok((nearest, squared))
2674}
2675
2676