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