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    let [u0, u1, v0, v1] = domains;
376    let mut u = seed_u;
377    let mut v = seed_v;
378    let mut converged = false;
379    let mut best = NewtonResult {
380        u,
381        v,
382        distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
383        converged,
384    };
385    for _ in 0..MAX_NEWTON_ITERATIONS {
386        let derivatives = surface.derivatives_small(u, v, 2)?;
387        let residual = derivatives[0][0].sub(point);
388        let distance_squared = residual.length_squared();
389        if distance_squared < best.distance_squared {
390            best.u = u;
391            best.v = v;
392            best.distance_squared = distance_squared;
393        }
394        let f = derivatives[1][0].dot(residual);
395        let g = derivatives[0][1].dot(residual);
396        let residual_length = distance_squared.sqrt();
397        if residual_length <= LINEAR_TOLERANCE {
398            converged = true;
399            break;
400        }
401        let tangent_u_length = derivatives[1][0].length();
402        let tangent_v_length = derivatives[0][1].length();
403        let cosine_u = if tangent_u_length * residual_length <= EPSILON {
404            0.0
405        } else {
406            f.abs() / (tangent_u_length * residual_length)
407        };
408        let cosine_v = if tangent_v_length * residual_length <= EPSILON {
409            0.0
410        } else {
411            g.abs() / (tangent_v_length * residual_length)
412        };
413        if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
414            converged = true;
415            break;
416        }
417        let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
418        let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
419        let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
420        let determinant = j00 * j11 - j01 * j01;
421        if determinant.abs() <= EPSILON {
422            break;
423        }
424        let mut du = (-f * j11 + g * j01) / determinant;
425        let mut dv = (-g * j00 + f * j01) / determinant;
426        du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
427        dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
428        let next_u = fit_parameter(u + du, u0, u1, closed_u);
429        let next_v = fit_parameter(v + dv, v0, v1, closed_v);
430        let stalled =
431            (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
432        u = next_u;
433        v = next_v;
434        if stalled {
435            break;
436        }
437    }
438    let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
439    if final_distance_squared < best.distance_squared {
440        best.u = u;
441        best.v = v;
442        best.distance_squared = final_distance_squared;
443    }
444    best.converged = converged;
445    Ok(best)
446}
447
448/// The point of `surface` nearest `point`, over the surface's whole domain.
449///
450/// Analytic carriers answer in closed form; every other surface goes through
451/// `project_point_to_surface_general`, which says what it guarantees.
452/// `#[track_caller]` names the call site in the `BREP_PROJECTION_AUDIT` log.
453#[track_caller]
454pub fn project_point_to_surface(
455    surface: &NurbsSurface,
456    point: Vec3,
457) -> Result<SurfaceProjection, String> {
458    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_PROJECT, surface);
459    // Recognized analytic carriers (plane / cylinder / cone / sphere /
460    // torus) have exact closed-form projections in the surface's own
461    // rational parameterization — skip grid seeding and Newton entirely.
462    if let Some(analytic) = surface.analytic() {
463        if let Some(projection) = analytic.project(surface, point) {
464            return Ok(projection);
465        }
466    }
467    if let Some(ellipsoid) = surface.affine_sphere() {
468        if let Some(projection) = ellipsoid.project(surface, point) {
469            return Ok(projection);
470        }
471    }
472    project_point_to_surface_general(surface, point)
473}
474
475/// Project a point onto a surface starting Newton from an explicit `(u, v)`
476/// guess, WITHOUT the global grid seed. This is a footpoint refiner for
477/// continuity-preserving curve-on-surface tracing: seeding each edge sample
478/// from its neighbour's parameters keeps the fit on ONE branch of a surface
479/// that folds back over the small trimmed patch, where an independent global
480/// search would snap to whichever fold is momentarily closest and tear the
481/// pcurve into a self-crossing zig-zag. The caller compares the returned
482/// `distance` against the global answer and only adopts this result when it is
483/// geometrically just as valid, so a bad seed can never make a fit worse.
484pub fn project_point_to_surface_seeded(
485    surface: &NurbsSurface,
486    point: Vec3,
487    seed_u: f64,
488    seed_v: f64,
489) -> Result<SurfaceProjection, String> {
490    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_SEEDED, surface);
491    // An ellipsoid's foot is unique off its medial axis: the closed form IS
492    // the branch answer, as it is for the analytic carriers at the seeded
493    // entry (`project_point_to_surface_from_seed`).
494    if let Some(ellipsoid) = surface.affine_sphere() {
495        if let Some(projection) = ellipsoid.project(surface, point) {
496            return Ok(projection);
497        }
498    }
499    let [u0, u1] = surface.domain_u()?;
500    let [v0, v1] = surface.domain_v()?;
501    let (closed_u, closed_v) = surface.closed_directions()?;
502    let result = surface_newton(
503        surface,
504        point,
505        seed_u,
506        seed_v,
507        [u0, u1, v0, v1],
508        closed_u,
509        closed_v,
510    )?;
511    let projected = surface.evaluate(result.u, result.v)?;
512    Ok(SurfaceProjection {
513        u: result.u,
514        v: result.v,
515        point: projected,
516        distance: projected.sub(point).length(),
517    })
518}
519
520/// The foot of `point` on the branch of `seed`: the closed form on an analytic
521/// carrier, otherwise Newton from the caller's own `(u, v)`
522/// ([`project_point_to_surface_seeded`]), WITHOUT the global search.
523///
524/// **Which callers.** One that already holds a foot on this surface from a
525/// geometrically adjacent query and whose output needs continuity with it: a
526/// pcurve's previous sample, a march's previous station, a Newton's previous
527/// iterate. [`project_point_to_surface`] answers a different question — where
528/// on the WHOLE surface the point is nearest — and on a carrier that comes back
529/// near itself, or for an off-surface query about as far from two sheets, that
530/// answer is a foot on another branch: a pcurve sample on the wrong sheet, an
531/// iterate that jumps. A caller with no such foot (a vertex, a probe, the first
532/// station) asks the nearest-point question and calls the projector.
533///
534/// **An analytic carrier** answers in closed form, as the projector does. Its
535/// only places with two equally valid feet are its seam, its poles and its focal
536/// set (axis, centre, tube circle), and the closed form answers those exactly as
537/// the nearest-point query always has; no march on one changes.
538///
539/// **What it does not promise.** It is a local answer: its distance is an upper
540/// bound, and a Newton that stalls returns its best iterate without saying so.
541/// Every caller keeps its own on-surface check.
542///
543/// **Instruments**, each read once. `BREP_PROJECTION_FROM_SEED=0` returns
544/// [`project_point_to_surface`]'s answer at every caller (the reading before
545/// 2026-09-15, for a same-binary A/B); `BREP_PROJECTION_FROM_SEED_SITES=<a,b,…>`
546/// keeps the seeded answer only at the call sites whose `file:line` contains
547/// one of the substrings. Under `BREP_PROJECTION_AUDIT=<dir>` every query whose
548/// seeded foot lies farther than the on-surface floor from the nearest point's
549/// foot appends one line to `<dir>/projection-seed-audit-<pid>.tsv`. That
550/// comparand is the nearest-point projector under the same caller, so its own
551/// replacements land in `projection-audit-<pid>.tsv` at this site's line: they
552/// are the audit's, not an answer the caller took.
553#[track_caller]
554pub(crate) fn project_point_to_surface_from_seed(
555    surface: &NurbsSurface,
556    point: Vec3,
557    seed: [f64; 2],
558) -> Result<SurfaceProjection, String> {
559    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_SEED, surface);
560    if let Some(analytic) = surface.analytic() {
561        if let Some(projection) = analytic.project(surface, point) {
562            return Ok(projection);
563        }
564    }
565    if let Some(ellipsoid) = surface.affine_sphere() {
566        if let Some(projection) = ellipsoid.project(surface, point) {
567            return Ok(projection);
568        }
569    }
570    let caller = std::panic::Location::caller();
571    if !seeded_answer_admitted(caller) {
572        return project_point_to_surface_general(surface, point);
573    }
574    let seeded = project_point_to_surface_seeded(surface, point, seed[0], seed[1])?;
575    if seed_audit_enabled() {
576        let nearest = project_point_to_surface_general(surface, point)?;
577        if seeded.point.sub(nearest.point).length() > LINEAR_TOLERANCE {
578            seed_audit_record(caller, surface, seed, &seeded, &nearest);
579        }
580    }
581    Ok(seeded)
582}
583
584/// `BREP_PROJECTION_FROM_SEED=0` / `BREP_PROJECTION_FROM_SEED_SITES`: see
585/// [`project_point_to_surface_from_seed`].
586fn seeded_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
587    static SITES: std::sync::OnceLock<Option<Option<Vec<String>>>> = std::sync::OnceLock::new();
588    let sites = SITES.get_or_init(|| {
589        if std::env::var("BREP_PROJECTION_FROM_SEED").as_deref() == Ok("0") {
590            return None;
591        }
592        Some(std::env::var("BREP_PROJECTION_FROM_SEED_SITES").ok().map(|value| {
593            value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect()
594        }))
595    });
596    match sites {
597        None => false,
598        Some(None) => true,
599        Some(Some(only)) => {
600            let location = format!("{}:{}", caller.file(), caller.line());
601            only.iter().any(|site| location.contains(site.as_str()))
602        }
603    }
604}
605
606fn seed_audit_enabled() -> bool {
607    debug_flag!("BREP_PROJECTION_AUDIT")
608}
609
610/// One line per seeded answer whose foot is not the nearest point's: the call
611/// site, both distances, the seed, both feet's `(u, v)`, the separation of the
612/// two feet and the surface's shape.
613fn seed_audit_record(
614    caller: &std::panic::Location<'_>,
615    surface: &NurbsSurface,
616    seed: [f64; 2],
617    seeded: &SurfaceProjection,
618    nearest: &SurfaceProjection,
619) {
620    use std::io::Write;
621    static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
622        std::sync::OnceLock::new();
623    let sink = SINK.get_or_init(|| {
624        let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
625        let path = std::path::Path::new(&directory)
626            .join(format!("projection-seed-audit-{}.tsv", std::process::id()));
627        std::fs::OpenOptions::new()
628            .create(true)
629            .append(true)
630            .open(path)
631            .ok()
632            .map(std::sync::Mutex::new)
633    });
634    if let Some(file) = sink {
635        if let Ok(mut file) = file.lock() {
636            let _ = writeln!(
637                file,
638                "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{:.6e}\t{}x{} cp {}x{}",
639                caller.file(),
640                caller.line(),
641                seeded.distance,
642                nearest.distance,
643                seed[0],
644                seed[1],
645                seeded.u,
646                seeded.v,
647                nearest.u,
648                nearest.v,
649                seeded.point.sub(nearest.point).length(),
650                surface.degree_u,
651                surface.degree_v,
652                surface.control_points.len(),
653                surface.control_points.first().map(Vec::len).unwrap_or(0),
654            );
655        }
656    }
657}
658
659/// The Newton seed grid for general projection: (u, v, point) samples over
660/// every knot span. A pure function of the surface, cached on it — building
661/// the grid costs hundreds of evaluations and projection is the hottest
662/// entry point in the kernel.
663fn projection_seed_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
664    if let Some(grid) = surface.projection_grid.get() {
665        return Ok(grid);
666    }
667    let [u0, u1] = surface.domain_u()?;
668    let [v0, v1] = surface.domain_v()?;
669    let (closed_u, closed_v) = surface.closed_directions()?;
670    let mut breaks_u = vec![u0];
671    breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
672    breaks_u.push(u1);
673    let mut breaks_v = vec![v0];
674    breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
675    breaks_v.push(v1);
676    let samples_u = if closed_u {
677        8usize.max(surface.degree_u * 4)
678    } else {
679        3usize.max(surface.degree_u + 1)
680    };
681    let samples_v = if closed_v {
682        8usize.max(surface.degree_v * 4)
683    } else {
684        3usize.max(surface.degree_v + 1)
685    };
686    let mut grid = Vec::new();
687    for u_pair in breaks_u.windows(2) {
688        for v_pair in breaks_v.windows(2) {
689            for i in 0..=samples_u {
690                for j in 0..=samples_v {
691                    let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
692                    let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
693                    grid.push((u, v, surface.evaluate(u, v)?));
694                }
695            }
696        }
697    }
698    Ok(surface.projection_grid.get_or_init(|| grid))
699}
700
701/// The dense fallback grid for degenerate general projection: (u, v, point)
702/// over a `(count_u+1) × (count_v+1)` lattice with `count_u/count_v` derived
703/// from the surface's knot spans. A pure function of the surface, cached on
704/// it — on metre-unit-mm parts the degenerate fallback fires ~1.7M times and
705/// each re-evaluated all ~561 lattice points; caching them makes the argmin a
706/// pure distance scan over reused points (bit-identical seed).
707fn projection_dense_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
708    if let Some(grid) = surface.projection_dense_grid.get() {
709        return Ok(grid);
710    }
711    let [u0, u1] = surface.domain_u()?;
712    let [v0, v1] = surface.domain_v()?;
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 count_u = 32usize.max((breaks_u.len() - 1) * 8);
720    let count_v = 16usize.max((breaks_v.len() - 1) * 8);
721    let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
722    for i in 0..=count_u {
723        for j in 0..=count_v {
724            let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
725            let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
726            grid.push((u, v, surface.evaluate(u, v)?));
727        }
728    }
729    Ok(surface.projection_dense_grid.get_or_init(|| grid))
730}
731
732// Boundary-ring cache geometry: 4 sides × 22 exponents × 33 indices. The scan
733// positions depend only on the surface domain and these fixed integers, so the
734// evaluated points are a pure function of the surface and cacheable.
735const RING_EXPONENTS: usize = 22;
736const RING_INDICES: usize = 33; // index 0..=32
737
738#[inline]
739fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
740    (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
741}
742
743/// The boundary-ring sample points for the degenerate projection fallback,
744/// flattened by `ring_slot(side, exponent-1, index)`. Sides: 0 = v-low
745/// (`v = v0 + (v1-v0)·offset`), 1 = v-high (`v1 - (v1-v0)·offset`), 2 = u-low
746/// (`u0 + (u1-u0)·offset`), 3 = u-high (`u1 - (u1-u0)·offset`), where
747/// `offset = 1/2^exponent`. A pure function of the surface, cached on it — the
748/// fallback's boundary ring re-evaluated these on every one of ~1.7M calls.
749fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
750    if let Some(grid) = surface.projection_ring_grid.get() {
751        return Ok(grid);
752    }
753    let [u0, u1] = surface.domain_u()?;
754    let [v0, v1] = surface.domain_v()?;
755    let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
756    for exponent in 1..=RING_EXPONENTS as i32 {
757        let offset = 1.0 / 2f64.powi(exponent);
758        let e = exponent as usize - 1;
759        let v_lo = v0 + (v1 - v0) * offset;
760        let v_hi = v1 - (v1 - v0) * offset;
761        let u_lo = u0 + (u1 - u0) * offset;
762        let u_hi = u1 - (u1 - u0) * offset;
763        for index in 0..=32 {
764            let u = u0 + (u1 - u0) * index as f64 / 32.0;
765            grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
766            grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
767        }
768        for index in 0..=32 {
769            let v = v0 + (v1 - v0) * index as f64 / 32.0;
770            grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
771            grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
772        }
773    }
774    Ok(surface.projection_ring_grid.get_or_init(|| grid))
775}
776
777/// The LOCAL answer: Newton from the seed grid's single nearest sample (or the
778/// linear-revolution foot), with the degenerate-parameterization fallbacks.
779///
780/// This is the whole of what the general projector returned before
781/// 2026-09-14, and it is a local minimum, not the minimum: on a general patch
782/// nearly closed in one direction the nearest sample can sit on the far end,
783/// and Newton converges there (abc_00000026's thread flanks, 2.16e-2 .. 4.1e-2
784/// against a true 1e-13). [`project_point_to_surface_general`] runs it first
785/// and keeps its answer wherever no other basin beats it.
786fn nearest_seed_answer(
787    surface: &NurbsSurface,
788    point: Vec3,
789    [u0, u1, v0, v1]: [f64; 4],
790    closed_u: bool,
791    closed_v: bool,
792) -> Result<NewtonResult, String> {
793    let mut best = NewtonResult {
794        u: u0,
795        v: v0,
796        distance_squared: f64::INFINITY,
797        converged: false,
798    };
799    for &(u, v, sample) in projection_seed_grid(surface)? {
800        let distance_squared = sample.sub(point).length_squared();
801        if distance_squared < best.distance_squared {
802            best.u = u;
803            best.v = v;
804            best.distance_squared = distance_squared;
805        }
806    }
807    if let Some(revolution) = project_linear_revolution(surface, point)? {
808        if revolution.distance * revolution.distance < best.distance_squared {
809            best.u = revolution.u;
810            best.v = revolution.v;
811            best.distance_squared = revolution.distance * revolution.distance;
812        }
813    }
814    let first_newton = surface_newton(
815        surface,
816        point,
817        best.u,
818        best.v,
819        [u0, u1, v0, v1],
820        closed_u,
821        closed_v,
822    )?;
823    if first_newton.distance_squared < best.distance_squared {
824        best = first_newton;
825    }
826    let (_, su, sv) = surface.deriv1(best.u, best.v)?;
827    let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
828    if degenerate || !first_newton.converged {
829        let mut grid = best;
830        for &(u, v, sample) in projection_dense_grid(surface)? {
831            let distance_squared = sample.sub(point).length_squared();
832            if distance_squared < grid.distance_squared {
833                grid.u = u;
834                grid.v = v;
835                grid.distance_squared = distance_squared;
836            }
837        }
838        if grid.distance_squared < best.distance_squared {
839            best = grid;
840        }
841        let polished = surface_newton(
842            surface,
843            point,
844            grid.u,
845            grid.v,
846            [u0, u1, v0, v1],
847            closed_u,
848            closed_v,
849        )?;
850        if polished.distance_squared < best.distance_squared {
851            best = polished;
852        }
853
854        let (_, su, sv) = surface.deriv1(best.u, best.v)?;
855        if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
856            let ring_grid = projection_ring_grid(surface)?;
857            let mut ring = best;
858            // Read the pre-evaluated ring points at the exact same parameter
859            // positions the live scans used (side/exponent/index → `ring_slot`),
860            // preserving scan order and argmin tie-breaking for bit-identity.
861            let scan = |side: usize,
862                        exponent_index: usize,
863                        along_u: bool,
864                        fixed: f64,
865                        candidate: &mut NewtonResult| {
866                for index in 0..=32 {
867                    let moving = if along_u {
868                        u0 + (u1 - u0) * index as f64 / 32.0
869                    } else {
870                        v0 + (v1 - v0) * index as f64 / 32.0
871                    };
872                    let (u, v) = if along_u {
873                        (moving, fixed)
874                    } else {
875                        (fixed, moving)
876                    };
877                    let distance_squared = ring_grid
878                        [ring_slot(side, exponent_index, index as usize)]
879                    .sub(point)
880                    .length_squared();
881                    if distance_squared < candidate.distance_squared {
882                        candidate.u = u;
883                        candidate.v = v;
884                        candidate.distance_squared = distance_squared;
885                    }
886                }
887            };
888            for exponent in 1..=22 {
889                let offset = 1.0 / 2f64.powi(exponent);
890                let e = exponent as usize - 1;
891                if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
892                    scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
893                }
894                if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
895                    scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
896                }
897                if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
898                    scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
899                }
900                if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
901                    scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
902                }
903            }
904            if ring.distance_squared < best.distance_squared {
905                best = ring;
906            }
907            let polished = surface_newton(
908                surface,
909                point,
910                ring.u,
911                ring.v,
912                [u0, u1, v0, v1],
913                closed_u,
914                closed_v,
915            )?;
916            if polished.distance_squared < best.distance_squared {
917                best = polished;
918            }
919        }
920    }
921    Ok(best)
922}
923
924/// Escape hatch for A/B measurement: `BREP_PROJECTION_GLOBAL=0` returns the
925/// local answer alone ([`nearest_seed_answer`], the pre-2026-09-14 projector),
926/// so a population run can be repeated with and without the global stage from
927/// one binary. Read once.
928fn global_stage_enabled() -> bool {
929    switch!("BREP_PROJECTION_GLOBAL", true)
930}
931
932/// `BREP_PROJECTION_GLOBAL_SITES=<a,b,…>`: the global stage's answer is kept
933/// only for call sites whose `file:line` contains one of the comma-separated
934/// substrings, and the local answer is returned everywhere else — so a result
935/// that moves can be attributed to the call site that moved it.
936/// `BREP_PROJECTION_GLOBAL_EXCEPT=<a,b,…>` is the complement: every site but
937/// those. Unset: every site. Read once.
938/// Whether a `BREP_PROJECTION_GLOBAL_SITES`/`_EXCEPT` filter is set, i.e.
939/// whether [`project_point_to_surface`]'s answer can depend on its call site.
940pub(crate) fn global_site_filter_active() -> bool {
941    // Read once per process, like the site lists `global_answer_admitted`
942    // reads: the common reader asks this for every probe (two `getenv` calls,
943    // ~3.7% of the elephant's B12 prefix on the production sampler,
944    // 2026-11-02), and nothing sets these variables after start-up.
945    static ACTIVE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
946    *ACTIVE.get_or_init(|| {
947        std::env::var_os("BREP_PROJECTION_GLOBAL_SITES").is_some() || std::env::var_os("BREP_PROJECTION_GLOBAL_EXCEPT").is_some()
948    })
949}
950
951fn global_answer_admitted(caller: &std::panic::Location<'_>) -> bool {
952    fn list(name: &str) -> Option<Vec<String>> {
953        std::env::var(name)
954            .ok()
955            .map(|value| value.split(',').map(str::to_string).filter(|site| !site.is_empty()).collect())
956    }
957    static SITES: std::sync::OnceLock<(Option<Vec<String>>, Option<Vec<String>>)> =
958        std::sync::OnceLock::new();
959    let (only, except) = SITES.get_or_init(|| {
960        (list("BREP_PROJECTION_GLOBAL_SITES"), list("BREP_PROJECTION_GLOBAL_EXCEPT"))
961    });
962    if only.is_none() && except.is_none() {
963        return true;
964    }
965    let location = format!("{}:{}", caller.file(), caller.line());
966    let named = |sites: &Vec<String>| sites.iter().any(|site| location.contains(site.as_str()));
967    only.as_ref().is_none_or(named) && !except.as_ref().is_some_and(named)
968}
969
970/// `BREP_PROJECTION_AUDIT=<dir>`: every query on which the global stage
971/// replaces the local answer appends one tab-separated line to
972/// `<dir>/projection-audit-<pid>.tsv` — the call site, the local and the
973/// global distance, both feet's `(u, v)` and the surface's shape. Per process,
974/// because the case gate forks a child per case and loses its stderr.
975fn audit_record(caller: &std::panic::Location<'_>, surface: &NurbsSurface, local: &NewtonResult, global: &NewtonResult) {
976    use std::io::Write;
977    static SINK: std::sync::OnceLock<Option<std::sync::Mutex<std::fs::File>>> =
978        std::sync::OnceLock::new();
979    let sink = SINK.get_or_init(|| {
980        let directory = std::env::var_os("BREP_PROJECTION_AUDIT")?;
981        let path = std::path::Path::new(&directory)
982            .join(format!("projection-audit-{}.tsv", std::process::id()));
983        std::fs::OpenOptions::new()
984            .create(true)
985            .append(true)
986            .open(path)
987            .ok()
988            .map(std::sync::Mutex::new)
989    });
990    if let Some(file) = sink {
991        if let Ok(mut file) = file.lock() {
992            let _ = writeln!(
993                file,
994                "{}:{}\t{:.6e}\t{:.6e}\t{:.9}\t{:.9}\t{:.9}\t{:.9}\t{}x{} cp {}x{}",
995                caller.file(),
996                caller.line(),
997                local.distance_squared.sqrt(),
998                global.distance_squared.sqrt(),
999                local.u,
1000                local.v,
1001                global.u,
1002                global.v,
1003                surface.degree_u,
1004                surface.degree_v,
1005                surface.control_points.len(),
1006                surface.control_points.first().map(Vec::len).unwrap_or(0),
1007            );
1008        }
1009    }
1010}
1011
1012/// The point of a general surface nearest `point`: the least local minimum of
1013/// the distance over the surface's whole domain.
1014///
1015/// **What it does.** First the local answer ([`nearest_seed_answer`]: Newton
1016/// from the nearest seed-grid sample, with its degenerate fallbacks — the whole
1017/// projector before 2026-09-14). Then the global stage ([`global_minimum`]):
1018/// the domain is cut into the cells of the projection lattice (every knot span
1019/// sampled `max(3, degree + 1)` times in an open direction and
1020/// `max(8, 4 × degree)` times in a closed one — the seed grid's own density),
1021/// and each cell is enclosed in a box built from the control points of the
1022/// surface REFINED to that cell. A NURBS patch with positive weights lies in
1023/// the convex hull of the control points that support it, so the distance from
1024/// the query to a cell's box is a rigorous LOWER bound on the distance to every
1025/// surface point of the cell. Every cell whose bound is below the best answer
1026/// so far is a candidate basin, and is refined by a descent confined to the
1027/// cell ([`confined_descent`]: Newton where the Hessian is positive definite,
1028/// the metric-scaled gradient where it is not, halved until the distance does
1029/// not rise, so it can end at a minimum but not at a saddle or a maximum),
1030/// seeded at the cell's nearest lattice node and continued over the domain
1031/// when the cell's least point is on its edge. The least answer wins. A closed
1032/// direction's seam is not special: the lattice spans the whole period, the
1033/// cells on either side of the seam are ordinary cells, and the continuation
1034/// wraps.
1035///
1036/// **What is guaranteed.**
1037/// * The answer is a point ON the surface, so its distance never under-reads.
1038/// * Every cell that could hold a point nearer than the answer by more than the
1039///   resolution has been descended in, unless it already held a verified
1040///   minimum: a cell left out was excluded by its hull bound, which cannot
1041///   over-read. [`GlobalStats::hull_lower_bound`] is that certificate: the
1042///   true distance lies in `[hull_lower_bound, answer]`, always.
1043/// * **The one step not certified in general: one basin in the cell that holds
1044///   the least minimum.** A descent finds the basin of its seed. A second basin
1045///   in any OTHER cell costs nothing — that cell's better basin is still no
1046///   nearer than the least minimum — but a second local minimum in the cell
1047///   holding the least minimum can catch the descent seeded at that cell's
1048///   nearest lattice node, and the least minimum is missed by at most
1049///   `answer − hull_lower_bound`. That takes a query past the focal distance of
1050///   part of the cell (farther than its radius of normal curvature on the
1051///   query's side, where the distance's Hessian `G − d·II` stops being positive
1052///   definite) or a cell whose image turns back toward the query. A descent that
1053///   runs out of iterations without a verified minimum fails the same way, with
1054///   the same bound. Seeding every corner of every candidate cell was measured
1055///   and changed no answer on the bench.
1056/// * **Certified on the analytic carriers** — the sphere, torus, cylinder and
1057///   cone, each a revolution of a circle or a line, and the plane. A lattice cell
1058///   there is an (azimuth × generatrix) rectangle under half a turn wide. In the
1059///   cell holding the least minimum the azimuthal derivative of the squared
1060///   distance, `2 ρ(v) ρ_P sin(λ − λ_P)`, vanishes only on the query's own
1061///   meridian, and moving toward that meridian lowers the distance from every
1062///   other point of the cell, so each local minimum over the cell is a local
1063///   minimum of the planar distance from the query's meridian image to the
1064///   generatrix over the cell's `v` range — exactly one on a circle arc under
1065///   half a turn or on a segment. At the focal sets (the axis, the sphere's
1066///   centre, the tube's centre circle) the minimum is a continuum of equal
1067///   distances and every foot is right. A general profile of revolution and a
1068///   general patch are not certified.
1069/// * Where the local answer is already the least minimum, it is returned
1070///   unchanged, bit for bit: the global stage replaces it only with a point
1071///   nearer by more than the resolution.
1072///
1073/// **The two numbers it reads.** The *on-surface floor* is `LINEAR_TOLERANCE`,
1074/// INHERITED unchanged: it is the local Newton's own stopping residual, and a
1075/// local answer within it of the query is on the surface and nothing is
1076/// searched. The *resolution* is the one new number, and it is the surface's
1077/// own: `(degree_u + 1)(degree_v + 1) · ε · scale`, `scale` the largest
1078/// control-point coordinate magnitude — the rounding of one evaluation, which
1079/// sums that many terms of that size. Every box is grown by it, and no
1080/// improvement finer than it is taken.
1081///
1082/// **What it costs.** A query settled by the floor costs what the local answer
1083/// always cost. Any other query builds the cell enclosure once per surface (one
1084/// knot refinement of the net to the lattice, cached on the surface beside the
1085/// seed grid), walks a bounding-sphere tree over the cells, and runs one
1086/// descent per candidate cell the hull bound does not exclude.
1087#[track_caller]
1088pub(crate) fn project_point_to_surface_general(
1089    surface: &NurbsSurface,
1090    point: Vec3,
1091) -> Result<SurfaceProjection, String> {
1092    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_GENERAL, surface);
1093    let [u0, u1] = surface.domain_u()?;
1094    let [v0, v1] = surface.domain_v()?;
1095    let (closed_u, closed_v) = surface.closed_directions()?;
1096    let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1097    let mut best = local;
1098    if global_stage_enabled() {
1099        global_minimum(
1100            surface,
1101            point,
1102            [u0, u1, v0, v1],
1103            (closed_u, closed_v),
1104            &mut best,
1105            &mut GlobalStats::default(),
1106        )?;
1107        if best.u != local.u || best.v != local.v {
1108            let caller = std::panic::Location::caller();
1109            if global_answer_admitted(caller) {
1110                audit_record(caller, surface, &local, &best);
1111            } else {
1112                best = local;
1113            }
1114        }
1115    }
1116    let projected = surface.evaluate(best.u, best.v)?;
1117    Ok(SurfaceProjection {
1118        u: best.u,
1119        v: best.v,
1120        point: projected,
1121        distance: projected.sub(point).length(),
1122    })
1123}
1124
1125/// The single-seed inversion by itself: the closed form on an analytic
1126/// carrier, otherwise [`nearest_seed_answer`] — Newton from the seed grid's
1127/// nearest sample, WITHOUT the global stage. Its distance is an upper bound and
1128/// its foot a local minimum. It is not a nearest-point query: it exists for a
1129/// caller that asks a question ABOUT that inversion, and there is one —
1130/// `joint_branch_pcurves` (`io/step_import/builder/rims.rs`) offers a loop the
1131/// joint branch only when this inversion alternates between branches along it,
1132/// which it does on a carrier that comes back within one seed-grid spacing of
1133/// itself (`abc_00000026`'s nearly closed thread flanks).
1134pub(crate) fn project_point_to_surface_from_nearest_seed(
1135    surface: &NurbsSurface,
1136    point: Vec3,
1137) -> Result<SurfaceProjection, String> {
1138    let _lap = crate::projection_profile::Lap::enter(crate::projection_profile::LANE_FROM_NEAREST_SEED, surface);
1139    if let Some(analytic) = surface.analytic() {
1140        if let Some(projection) = analytic.project(surface, point) {
1141            return Ok(projection);
1142        }
1143    }
1144    if let Some(ellipsoid) = surface.affine_sphere() {
1145        if let Some(projection) = ellipsoid.project(surface, point) {
1146            return Ok(projection);
1147        }
1148    }
1149    let [u0, u1] = surface.domain_u()?;
1150    let [v0, v1] = surface.domain_v()?;
1151    let (closed_u, closed_v) = surface.closed_directions()?;
1152    let local = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1153    let projected = surface.evaluate(local.u, local.v)?;
1154    Ok(SurfaceProjection {
1155        u: local.u,
1156        v: local.v,
1157        point: projected,
1158        distance: projected.sub(point).length(),
1159    })
1160}
1161
1162/// Both answers for one query, and what the global stage did — the bench's
1163/// seam (`examples/projector_bench.rs`). Not for kernel callers.
1164#[doc(hidden)]
1165pub fn project_point_to_surface_general_lanes(
1166    surface: &NurbsSurface,
1167    point: Vec3,
1168    run_global: bool,
1169) -> Result<(SurfaceProjection, GlobalStats), String> {
1170    let [u0, u1] = surface.domain_u()?;
1171    let [v0, v1] = surface.domain_v()?;
1172    let (closed_u, closed_v) = surface.closed_directions()?;
1173    let mut best = nearest_seed_answer(surface, point, [u0, u1, v0, v1], closed_u, closed_v)?;
1174    let mut stats = GlobalStats::default();
1175    if run_global {
1176        global_minimum(surface, point, [u0, u1, v0, v1], (closed_u, closed_v), &mut best, &mut stats)?;
1177    }
1178    let projected = surface.evaluate(best.u, best.v)?;
1179    Ok((
1180        SurfaceProjection {
1181            u: best.u,
1182            v: best.v,
1183            point: projected,
1184            distance: projected.sub(point).length(),
1185        },
1186        stats,
1187    ))
1188}
1189
1190/// How many basins the cells of one query hold — the bench's measure of the
1191/// global stage's one uncertified step (see [`project_point_to_surface_general`]).
1192#[doc(hidden)]
1193#[derive(Clone, Debug, Default)]
1194pub struct BasinCensus {
1195    /// Cells whose hull bound is below the answer's distance: every cell the
1196    /// global stage could have descended in.
1197    pub cells: usize,
1198    /// Descents run: one from each corner of those cells and one from the middle.
1199    pub descents: usize,
1200    /// Descents that stopped without a verified minimum.
1201    pub unconverged: usize,
1202    /// Cells where two verified minima differ in distance by more than `bar`.
1203    /// Distances within the on-surface floor count as equal: the descent stops
1204    /// there, so two feet under it are one foot polished to different depths.
1205    pub cells_with_two_basins: usize,
1206    /// Of those, cells whose rectangle holds the least foot any descent found:
1207    /// the only cells where a second basin can cost the answer anything.
1208    pub foot_cells_with_two_basins: usize,
1209    /// The least distance any descent reached, and where.
1210    pub least: f64,
1211    pub least_uv: (f64, f64),
1212    /// Every descent in a two-basin cell that holds the least foot: the cell,
1213    /// its rectangle, the seed, where it stopped, its distance, and whether that
1214    /// is a verified minimum.
1215    pub foot_cell_descents: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)>,
1216}
1217
1218/// Descend in every cell whose hull bound is below `answer_distance` from each
1219/// of its corners and its middle, and count the cells whose verified minima
1220/// disagree by more than `bar`. For the bench only: it costs five descents per
1221/// cell, which the projector itself was measured not to need.
1222#[doc(hidden)]
1223pub fn project_point_to_surface_basin_census(
1224    surface: &NurbsSurface,
1225    point: Vec3,
1226    answer_distance: f64,
1227    bar: f64,
1228) -> Result<BasinCensus, String> {
1229    let mut census = BasinCensus {
1230        least: f64::INFINITY,
1231        ..BasinCensus::default()
1232    };
1233    let Some(cells) = projection_cells(surface)? else {
1234        return Ok(census);
1235    };
1236    let columns = cells.vs.len() - 1;
1237    let mut spreads: Vec<(usize, f64)> = Vec::new();
1238    let mut runs: Vec<(usize, [f64; 4], (f64, f64), (f64, f64), f64, bool)> = Vec::new();
1239    for (cell, cell_box) in cells.boxes.iter().enumerate() {
1240        if cell_box.lower_bound(point) >= answer_distance {
1241            continue;
1242        }
1243        census.cells += 1;
1244        let (row, column) = (cell / columns, cell % columns);
1245        let rectangle = [cells.us[row], cells.us[row + 1], cells.vs[column], cells.vs[column + 1]];
1246        let middle = ((rectangle[0] + rectangle[1]) * 0.5, (rectangle[2] + rectangle[3]) * 0.5);
1247        let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
1248        for seed in [
1249            (rectangle[0], rectangle[2]),
1250            (rectangle[1], rectangle[2]),
1251            (rectangle[0], rectangle[3]),
1252            (rectangle[1], rectangle[3]),
1253            middle,
1254        ] {
1255            census.descents += 1;
1256            let (found, _) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1257            let distance = found.distance_squared.sqrt();
1258            runs.push((cell, rectangle, seed, (found.u, found.v), distance, found.converged));
1259            if distance < census.least {
1260                census.least = distance;
1261                census.least_uv = (found.u, found.v);
1262            }
1263            if !found.converged {
1264                census.unconverged += 1;
1265                continue;
1266            }
1267            low = low.min(distance.max(LINEAR_TOLERANCE));
1268            high = high.max(distance.max(LINEAR_TOLERANCE));
1269        }
1270        if high - low > bar {
1271            spreads.push((cell, high - low));
1272        }
1273    }
1274    census.cells_with_two_basins = spreads.len();
1275    let (u, v) = census.least_uv;
1276    let foot_cells: Vec<usize> = spreads
1277        .iter()
1278        .map(|&(cell, _)| cell)
1279        .filter(|&cell| {
1280            let (row, column) = (cell / columns, cell % columns);
1281            cells.us[row] <= u && u <= cells.us[row + 1] && cells.vs[column] <= v && v <= cells.vs[column + 1]
1282        })
1283        .collect();
1284    census.foot_cells_with_two_basins = foot_cells.len();
1285    census.foot_cell_descents = runs.into_iter().filter(|run| foot_cells.contains(&run.0)).collect();
1286    Ok(census)
1287}
1288
1289/// What the global stage did on one query.
1290#[doc(hidden)]
1291#[derive(Clone, Copy, Debug, Default)]
1292pub struct GlobalStats {
1293    /// The local answer was within the on-surface floor; nothing searched.
1294    pub settled_by_floor: bool,
1295    /// Cells whose hull bound admitted them.
1296    pub candidates: usize,
1297    /// Confined Newtons run (candidates not already holding a converged foot).
1298    pub refinements: usize,
1299    /// Continuations across the domain from a cell edge.
1300    pub continuations: usize,
1301    /// The global stage replaced the local answer.
1302    pub replaced: bool,
1303    /// The least hull bound over every cell — the true distance is never
1304    /// below it (0 when the floor settled the query).
1305    pub hull_lower_bound: f64,
1306}
1307
1308/// One lattice cell's enclosure: an oriented box around the control points
1309/// that support the cell, in an orthonormal frame fitted to the cell.
1310#[derive(Clone, Debug)]
1311struct CellBox {
1312    axes: [Vec3; 3],
1313    low: [f64; 3],
1314    high: [f64; 3],
1315}
1316
1317impl CellBox {
1318    /// Distance from `point` to the box: a lower bound on the distance to
1319    /// every surface point of the cell.
1320    fn lower_bound(&self, point: Vec3) -> f64 {
1321        let mut squared = 0.0;
1322        for axis in 0..3 {
1323            let coordinate = point.dot(self.axes[axis]);
1324            let excess = (self.low[axis] - coordinate).max(coordinate - self.high[axis]).max(0.0);
1325            squared += excess * excess;
1326        }
1327        squared.sqrt()
1328    }
1329}
1330
1331/// A node of the bounding-sphere tree over the cells. A leaf names its cell;
1332/// an internal node its two children.
1333#[derive(Clone, Copy, Debug)]
1334struct CellTreeNode {
1335    centre: Vec3,
1336    radius: f64,
1337    children: Option<(u32, u32)>,
1338    cell: u32,
1339}
1340
1341/// The cached enclosure of every projection lattice cell (see
1342/// [`project_point_to_surface_general`]).
1343#[derive(Clone, Debug)]
1344pub(crate) struct ProjectionCells {
1345    us: Vec<f64>,
1346    vs: Vec<f64>,
1347    /// `us.len() × vs.len()` lattice points, row-major in u.
1348    nodes: Vec<Vec3>,
1349    /// `(us.len() - 1) × (vs.len() - 1)` cells, row-major in u.
1350    boxes: Vec<CellBox>,
1351    /// The tree's root is the LAST node.
1352    tree: Vec<CellTreeNode>,
1353    resolution: f64,
1354}
1355
1356/// The projection lattice's parameters along one direction: every knot span
1357/// cut into `samples` equal pieces, the seed grid's density, shared span ends
1358/// listed once.
1359fn lattice_parameters(knots: &[f64], degree: usize, closed: bool) -> Vec<f64> {
1360    let [start, end] = crate::curve::knot_domain(knots, degree);
1361    let samples = if closed {
1362        8usize.max(degree * 4)
1363    } else {
1364        3usize.max(degree + 1)
1365    };
1366    let mut breaks = vec![start];
1367    breaks.extend(interior_knots(knots, degree));
1368    breaks.push(end);
1369    let mut parameters = Vec::with_capacity((breaks.len() - 1) * samples + 1);
1370    for pair in breaks.windows(2) {
1371        for index in 0..samples {
1372            parameters.push(pair[0] + (pair[1] - pair[0]) * index as f64 / samples as f64);
1373        }
1374    }
1375    parameters.push(end);
1376    parameters
1377}
1378
1379/// Raise every lattice parameter to full multiplicity in the knot vector, so
1380/// each lattice cell is one Bézier piece of the refined net.
1381///
1382/// `net[i]` is the i-th control point along the refined direction, held as a
1383/// row across the other direction, and every row takes the same combination.
1384/// Knot refinement in one pass (Piegl & Tiller, *The NURBS Book*, A5.4), in
1385/// homogeneous coordinates, so a rational net refines exactly and the cost is
1386/// linear in the refined net's size.
1387fn refine_to_lattice(net: &mut Vec<Vec<Vec4>>, knots: &mut Vec<f64>, degree: usize, parameters: &[f64]) {
1388    let p = degree;
1389    let n = net.len() - 1;
1390    let mut inserted = Vec::new();
1391    for &parameter in parameters {
1392        let multiplicity = knots.iter().filter(|&&knot| knot == parameter).count();
1393        for _ in multiplicity..p {
1394            inserted.push(parameter);
1395        }
1396    }
1397    if inserted.is_empty() || p == 0 {
1398        return;
1399    }
1400    inserted.sort_by(f64::total_cmp);
1401    let find_span = |parameter: f64| -> usize {
1402        if parameter >= knots[n + 1] {
1403            return n;
1404        }
1405        (knots.partition_point(|&knot| knot <= parameter) - 1).clamp(p, n)
1406    };
1407    let r = inserted.len() - 1;
1408    let m = n + p + 1;
1409    let a = find_span(inserted[0]);
1410    let b = find_span(inserted[r]) + 1;
1411    let columns = net[0].len();
1412    let blank = vec![
1413        Vec4 {
1414            x: 0.0,
1415            y: 0.0,
1416            z: 0.0,
1417            w: 0.0,
1418        };
1419        columns
1420    ];
1421    let mut refined = vec![blank; n + r + 2];
1422    let mut refined_knots = vec![0.0; m + r + 2];
1423    for j in 0..=a - p {
1424        refined[j] = net[j].clone();
1425    }
1426    for j in b - 1..=n {
1427        refined[j + r + 1] = net[j].clone();
1428    }
1429    refined_knots[..=a].copy_from_slice(&knots[..=a]);
1430    for j in b + p..=m {
1431        refined_knots[j + r + 1] = knots[j];
1432    }
1433    let mut i = b + p - 1;
1434    let mut k = b + p + r;
1435    for j in (0..=r).rev() {
1436        while inserted[j] <= knots[i] && i > a {
1437            refined[k - p - 1] = net[i - p - 1].clone();
1438            refined_knots[k] = knots[i];
1439            k -= 1;
1440            i -= 1;
1441        }
1442        refined[k - p - 1] = refined[k - p].clone();
1443        for l in 1..=p {
1444            let index = k - p + l;
1445            let numerator = refined_knots[k + l] - inserted[j];
1446            if numerator == 0.0 {
1447                refined[index - 1] = refined[index].clone();
1448            } else {
1449                let alpha = numerator / (refined_knots[k + l] - knots[i - p + l]);
1450                let (head, tail) = refined.split_at_mut(index);
1451                for (before, &here) in head[index - 1].iter_mut().zip(&tail[0]) {
1452                    *before = before.scale(alpha).add(here.scale(1.0 - alpha));
1453                }
1454            }
1455        }
1456        refined_knots[k] = inserted[j];
1457        k = k.saturating_sub(1);
1458    }
1459    *net = refined;
1460    *knots = refined_knots;
1461}
1462
1463/// The control-point index range supporting the parameter interval
1464/// `[low, high]`: every basis function non-zero anywhere on it.
1465fn supporting_range(knots: &[f64], degree: usize, count: usize, low: f64, high: f64) -> (usize, usize) {
1466    let first_span = knots.partition_point(|&knot| knot <= low).saturating_sub(1);
1467    let last_span = knots.partition_point(|&knot| knot < high).saturating_sub(1);
1468    (
1469        first_span.saturating_sub(degree).min(count - 1),
1470        last_span.max(first_span).min(count - 1),
1471    )
1472}
1473
1474fn unit_or(vector: Vec3, fallback: Vec3) -> Vec3 {
1475    let length = vector.length();
1476    if length > 0.0 && length.is_finite() {
1477        vector.scale(1.0 / length)
1478    } else {
1479        fallback
1480    }
1481}
1482
1483fn enclosing_sphere(a: (Vec3, f64), b: (Vec3, f64)) -> (Vec3, f64) {
1484    let offset = b.0.sub(a.0);
1485    let separation = offset.length();
1486    if separation + b.1 <= a.1 {
1487        return a;
1488    }
1489    if separation + a.1 <= b.1 {
1490        return b;
1491    }
1492    let radius = (separation + a.1 + b.1) * 0.5;
1493    let centre = a.0.add(offset.scale((radius - a.1) / separation));
1494    (centre, radius)
1495}
1496
1497fn build_cell_tree(
1498    tree: &mut Vec<CellTreeNode>,
1499    spheres: &[(Vec3, f64)],
1500    columns: usize,
1501    [row_low, row_high, column_low, column_high]: [usize; 4],
1502) -> u32 {
1503    if row_high - row_low == 1 && column_high - column_low == 1 {
1504        let cell = row_low * columns + column_low;
1505        tree.push(CellTreeNode {
1506            centre: spheres[cell].0,
1507            radius: spheres[cell].1,
1508            children: None,
1509            cell: cell as u32,
1510        });
1511        return (tree.len() - 1) as u32;
1512    }
1513    let (first, second) = if row_high - row_low >= column_high - column_low {
1514        let middle = (row_low + row_high) / 2;
1515        (
1516            [row_low, middle, column_low, column_high],
1517            [middle, row_high, column_low, column_high],
1518        )
1519    } else {
1520        let middle = (column_low + column_high) / 2;
1521        (
1522            [row_low, row_high, column_low, middle],
1523            [row_low, row_high, middle, column_high],
1524        )
1525    };
1526    let first = build_cell_tree(tree, spheres, columns, first);
1527    let second = build_cell_tree(tree, spheres, columns, second);
1528    let a = tree[first as usize];
1529    let b = tree[second as usize];
1530    // The resolution-inflated leaf spheres already absorb rounding; the merge
1531    // adds one more rounding of the same order, absorbed by growing the radius
1532    // by the larger child's own ulp.
1533    let (centre, radius) = enclosing_sphere((a.centre, a.radius), (b.centre, b.radius));
1534    tree.push(CellTreeNode {
1535        centre,
1536        radius: radius * (1.0 + 4.0 * f64::EPSILON),
1537        children: Some((first, second)),
1538        cell: u32::MAX,
1539    });
1540    (tree.len() - 1) as u32
1541}
1542
1543fn projection_cells(surface: &NurbsSurface) -> Result<Option<&ProjectionCells>, String> {
1544    if let Some(cells) = surface.projection_cells.get() {
1545        return Ok(cells.as_ref());
1546    }
1547    let cells = build_projection_cells(surface)?;
1548    Ok(surface.projection_cells.get_or_init(|| cells).as_ref())
1549}
1550
1551fn build_projection_cells(surface: &NurbsSurface) -> Result<Option<ProjectionCells>, String> {
1552    let (closed_u, closed_v) = surface.closed_directions()?;
1553    let us = lattice_parameters(&surface.knots_u, surface.degree_u, closed_u);
1554    let vs = lattice_parameters(&surface.knots_v, surface.degree_v, closed_v);
1555    if us.len() < 2 || vs.len() < 2 {
1556        return Ok(None);
1557    }
1558    let mut scale = 0.0f64;
1559    for control in surface.control_points.iter().flatten() {
1560        let point = control.point()?;
1561        scale = scale.max(point.x.abs()).max(point.y.abs()).max(point.z.abs());
1562    }
1563    let resolution =
1564        ((surface.degree_u + 1) * (surface.degree_v + 1)) as f64 * f64::EPSILON * scale;
1565
1566    // Refine u (the net's rows), then v (its columns, as rows of the transpose).
1567    let mut net = surface.control_points.clone();
1568    let mut knots_u = surface.knots_u.clone();
1569    refine_to_lattice(&mut net, &mut knots_u, surface.degree_u, &us);
1570    let columns = net.first().map(Vec::len).unwrap_or(0);
1571    let mut transposed: Vec<Vec<Vec4>> =
1572        (0..columns).map(|column| net.iter().map(|row| row[column]).collect()).collect();
1573    let mut knots_v = surface.knots_v.clone();
1574    refine_to_lattice(&mut transposed, &mut knots_v, surface.degree_v, &vs);
1575    let rows = transposed.first().map(Vec::len).unwrap_or(0);
1576    let columns = transposed.len();
1577    if rows == 0 || columns == 0 {
1578        return Ok(None);
1579    }
1580    let mut points = vec![Vec3::default(); rows * columns];
1581    for (column, fibre) in transposed.iter().enumerate() {
1582        for (row, control) in fibre.iter().enumerate() {
1583            points[row * columns + column] = control.point()?;
1584        }
1585    }
1586
1587    let mut nodes = Vec::with_capacity(us.len() * vs.len());
1588    for &u in &us {
1589        for &v in &vs {
1590            nodes.push(surface.evaluate(u, v)?);
1591        }
1592    }
1593
1594    let cell_rows = us.len() - 1;
1595    let cell_columns = vs.len() - 1;
1596    let mut boxes = Vec::with_capacity(cell_rows * cell_columns);
1597    let mut spheres = Vec::with_capacity(cell_rows * cell_columns);
1598    for i in 0..cell_rows {
1599        let (row_low, row_high) =
1600            supporting_range(&knots_u, surface.degree_u, rows, us[i], us[i + 1]);
1601        for j in 0..cell_columns {
1602            let (column_low, column_high) =
1603                supporting_range(&knots_v, surface.degree_v, columns, vs[j], vs[j + 1]);
1604            let at = |row: usize, column: usize| points[row * columns + column];
1605            let c00 = at(row_low, column_low);
1606            let c10 = at(row_high, column_low);
1607            let c01 = at(row_low, column_high);
1608            let c11 = at(row_high, column_high);
1609            let along_u = c10.sub(c00).add(c11.sub(c01));
1610            let along_v = c01.sub(c00).add(c11.sub(c10));
1611            let e1 = unit_or(along_u, unit_or(along_v, Vec3::new(1.0, 0.0, 0.0)));
1612            let normal = unit_or(e1.cross(along_v), e1.perpendicular()?);
1613            let e2 = unit_or(normal.cross(e1), e1.perpendicular()?);
1614            let e3 = e1.cross(e2);
1615            let axes = [e1, e2, e3];
1616            let mut low = [f64::INFINITY; 3];
1617            let mut high = [f64::NEG_INFINITY; 3];
1618            for row in row_low..=row_high {
1619                for column in column_low..=column_high {
1620                    let point = at(row, column);
1621                    for axis in 0..3 {
1622                        let coordinate = point.dot(axes[axis]);
1623                        low[axis] = low[axis].min(coordinate);
1624                        high[axis] = high[axis].max(coordinate);
1625                    }
1626                }
1627            }
1628            for axis in 0..3 {
1629                low[axis] -= resolution;
1630                high[axis] += resolution;
1631            }
1632            let mut centre = Vec3::default();
1633            let mut half_diagonal_squared = 0.0;
1634            for axis in 0..3 {
1635                centre = centre.add(axes[axis].scale((low[axis] + high[axis]) * 0.5));
1636                let half = (high[axis] - low[axis]) * 0.5;
1637                half_diagonal_squared += half * half;
1638            }
1639            if !low.iter().chain(high.iter()).all(|value| value.is_finite()) {
1640                return Ok(None);
1641            }
1642            spheres.push((centre, half_diagonal_squared.sqrt() + resolution));
1643            boxes.push(CellBox { axes, low, high });
1644        }
1645    }
1646    let mut tree = Vec::with_capacity(2 * boxes.len());
1647    build_cell_tree(&mut tree, &spheres, cell_columns, [0, cell_rows, 0, cell_columns]);
1648    Ok(Some(ProjectionCells {
1649        us,
1650        vs,
1651        nodes,
1652        boxes,
1653        tree,
1654        resolution,
1655    }))
1656}
1657
1658/// First- and second-order optimality of `(u, v)` for the distance over the
1659/// rectangle: stationary on the coordinates the rectangle leaves free (the
1660/// descent's own cosine test), with a positive definite Hessian there. A
1661/// `periodic` direction has no edge to be pinned against: its seam is interior.
1662fn optimality(
1663    derivatives: &[[Vec3; 3]; 3],
1664    point: Vec3,
1665    (u, v): (f64, f64),
1666    [ua, ub, va, vb]: [f64; 4],
1667    periodic: (bool, bool),
1668) -> Optimality {
1669    let residual = derivatives[0][0].sub(point);
1670    let residual_length = residual.length();
1671    let su = derivatives[1][0];
1672    let sv = derivatives[0][1];
1673    let f = su.dot(residual);
1674    let g = sv.dot(residual);
1675    let metric_u = su.length_squared();
1676    let metric_v = sv.length_squared();
1677    let stationary_u = metric_u.sqrt() * residual_length <= EPSILON
1678        || f.abs() <= 1e-10 * metric_u.sqrt() * residual_length;
1679    let stationary_v = metric_v.sqrt() * residual_length <= EPSILON
1680        || g.abs() <= 1e-10 * metric_v.sqrt() * residual_length;
1681    let pinned_u = !periodic.0 && ((u <= ua && f > 0.0) || (u >= ub && f < 0.0));
1682    let pinned_v = !periodic.1 && ((v <= va && g > 0.0) || (v >= vb && g < 0.0));
1683    let j00 = derivatives[2][0].dot(residual) + metric_u;
1684    let j01 = derivatives[1][1].dot(residual) + su.dot(sv);
1685    let j11 = derivatives[0][2].dot(residual) + metric_v;
1686    let determinant = j00 * j11 - j01 * j01;
1687    let convex = match (!pinned_u, !pinned_v) {
1688        (true, true) => j00 > 0.0 && determinant > 0.0,
1689        (true, false) => j00 > 0.0,
1690        (false, true) => j11 > 0.0,
1691        (false, false) => true,
1692    };
1693    Optimality {
1694        residual_length,
1695        gradient: (f, g),
1696        metric: (metric_u, metric_v),
1697        hessian: (j00, j01, j11),
1698        determinant,
1699        pinned: (pinned_u, pinned_v),
1700        convex,
1701        minimum: residual_length <= LINEAR_TOLERANCE
1702            || ((stationary_u || pinned_u) && (stationary_v || pinned_v) && convex),
1703    }
1704}
1705
1706struct Optimality {
1707    residual_length: f64,
1708    gradient: (f64, f64),
1709    metric: (f64, f64),
1710    hessian: (f64, f64, f64),
1711    determinant: f64,
1712    pinned: (bool, bool),
1713    convex: bool,
1714    minimum: bool,
1715}
1716
1717fn is_verified_minimum(
1718    surface: &NurbsSurface,
1719    point: Vec3,
1720    at: (f64, f64),
1721    rectangle: [f64; 4],
1722    periodic: (bool, bool),
1723) -> Result<bool, String> {
1724    let derivatives = surface.derivatives_small(at.0, at.1, 2)?;
1725    Ok(optimality(&derivatives, point, at, rectangle, periodic).minimum)
1726}
1727
1728/// A descent to a local minimum of the distance over the rectangle
1729/// `[ua, ub] × [va, vb]`, from a seed inside it.
1730///
1731/// Newton on the gradient converges to ANY critical point — a saddle or a
1732/// maximum as readily as a minimum — and reads convergence there, so every
1733/// step here must lower the distance: the Newton step where the Hessian is
1734/// positive definite on the free coordinates, the metric-scaled gradient step
1735/// where it is not, halved until the distance does not rise. A coordinate
1736/// pinned at the rectangle's edge by a gradient pointing out of it is held
1737/// there and the other takes the one-dimensional step, so a minimum ON the edge
1738/// converges as one (the Karush-Kuhn-Tucker point) instead of stalling.
1739///
1740/// Returns the best iterate, whether it is a verified minimum (stationary on
1741/// the free coordinates with a positive definite Hessian there, or within the
1742/// on-surface floor), and whether it lies on the rectangle's edge.
1743fn confined_descent(
1744    surface: &NurbsSurface,
1745    point: Vec3,
1746    seed: (f64, f64),
1747    rectangle: [f64; 4],
1748    periodic: (bool, bool),
1749) -> Result<(NewtonResult, bool), String> {
1750    let [ua, ub, va, vb] = rectangle;
1751    let mut u = fit_parameter(seed.0, ua, ub, periodic.0);
1752    let mut v = fit_parameter(seed.1, va, vb, periodic.1);
1753    let mut converged = false;
1754    let mut current = surface.evaluate(u, v)?.sub(point).length_squared();
1755    for _ in 0..MAX_NEWTON_ITERATIONS {
1756        let derivatives = surface.derivatives_small(u, v, 2)?;
1757        let state = optimality(&derivatives, point, (u, v), rectangle, periodic);
1758        current = state.residual_length * state.residual_length;
1759        if state.minimum {
1760            converged = true;
1761            break;
1762        }
1763        let (f, g) = state.gradient;
1764        let (metric_u, metric_v) = state.metric;
1765        let (j00, j01, j11) = state.hessian;
1766        let determinant = state.determinant;
1767        let (free_u, free_v) = (!state.pinned.0, !state.pinned.1);
1768        let (mut du, mut dv) = match (free_u, free_v) {
1769            (true, true) if state.convex => (
1770                (-f * j11 + g * j01) / determinant,
1771                (-g * j00 + f * j01) / determinant,
1772            ),
1773            (true, false) if state.convex => (-f / j00, 0.0),
1774            (false, true) if state.convex => (0.0, -g / j11),
1775            _ => (
1776                if free_u && metric_u > 0.0 { -f / metric_u } else { 0.0 },
1777                if free_v && metric_v > 0.0 { -g / metric_v } else { 0.0 },
1778            ),
1779        };
1780        if !(du.is_finite() && dv.is_finite()) || (du == 0.0 && dv == 0.0) {
1781            break;
1782        }
1783        let mut moved = false;
1784        // The step actually taken, a whole period removed across a seam.
1785        let taken = |next: f64, here: f64, low: f64, high: f64, wraps: bool| {
1786            let raw = next - here;
1787            if wraps {
1788                raw - (high - low) * (raw / (high - low)).round()
1789            } else {
1790                raw
1791            }
1792        };
1793        for _ in 0..40 {
1794            let next_u = fit_parameter(u + du, ua, ub, periodic.0);
1795            let next_v = fit_parameter(v + dv, va, vb, periodic.1);
1796            if next_u == u && next_v == v {
1797                break;
1798            }
1799            let trial = surface.evaluate(next_u, next_v)?.sub(point).length_squared();
1800            if trial <= current {
1801                moved = taken(next_u, u, ua, ub, periodic.0).abs() > 1e-15 * (ub - ua)
1802                    || taken(next_v, v, va, vb, periodic.1).abs() > 1e-15 * (vb - va);
1803                u = next_u;
1804                v = next_v;
1805                current = trial;
1806                break;
1807            }
1808            du *= 0.5;
1809            dv *= 0.5;
1810        }
1811        if !moved {
1812            break;
1813        }
1814    }
1815    let result = NewtonResult {
1816        u,
1817        v,
1818        distance_squared: current,
1819        converged,
1820    };
1821    let on_edge =
1822        (!periodic.0 && (u <= ua || u >= ub)) || (!periodic.1 && (v <= va || v >= vb));
1823    Ok((result, on_edge))
1824}
1825
1826/// The global stage of [`project_point_to_surface_general`]: replace `best`
1827/// with the least local minimum over every cell the hull bound admits.
1828#[allow(clippy::too_many_arguments)]
1829fn global_minimum(
1830    surface: &NurbsSurface,
1831    point: Vec3,
1832    domains: [f64; 4],
1833    periodic: (bool, bool),
1834    best: &mut NewtonResult,
1835    stats: &mut GlobalStats,
1836) -> Result<(), String> {
1837    if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
1838        stats.settled_by_floor = true;
1839        return Ok(());
1840    }
1841    let Some(cells) = projection_cells(surface)? else {
1842        return Ok(());
1843    };
1844    let resolution = cells.resolution;
1845    let columns = cells.vs.len() - 1;
1846    let columns_u = cells.us.len() - 1;
1847    let mut bound = best.distance_squared.sqrt() - resolution;
1848
1849    // Every cell whose box comes nearer than the answer; the least bound over
1850    // what the walk excluded is the certificate.
1851    let mut candidates: Vec<(f64, usize)> = Vec::new();
1852    let mut excluded = f64::INFINITY;
1853    let mut stack: Vec<u32> = Vec::with_capacity(64);
1854    stack.push((cells.tree.len() - 1) as u32);
1855    while let Some(index) = stack.pop() {
1856        let node = cells.tree[index as usize];
1857        let sphere_bound = (point.sub(node.centre).length() - node.radius).max(0.0);
1858        if sphere_bound >= bound {
1859            excluded = excluded.min(sphere_bound);
1860            continue;
1861        }
1862        match node.children {
1863            Some((first, second)) => {
1864                stack.push(first);
1865                stack.push(second);
1866            }
1867            None => {
1868                let cell = node.cell as usize;
1869                let box_bound = cells.boxes[cell].lower_bound(point);
1870                if box_bound < bound {
1871                    candidates.push((box_bound, cell));
1872                } else {
1873                    excluded = excluded.min(box_bound);
1874                }
1875            }
1876        }
1877    }
1878    stats.candidates = candidates.len();
1879    candidates.sort_by(|a, b| a.0.total_cmp(&b.0));
1880    stats.hull_lower_bound = candidates
1881        .first()
1882        .map_or(excluded, |&(least, _)| least.min(excluded));
1883
1884    // Cells already holding a verified minimum need no second descent. A foot
1885    // names ONE cell even when it sits on a lattice line shared by up to four:
1886    // a neighbour can hold a better basin of its own in its interior, and it
1887    // was admitted precisely because its bound beat this answer. The local
1888    // answer is one when it passes the same test the descent applies.
1889    let cell_of = |u: f64, v: f64| {
1890        let row = (cells.us.partition_point(|&value| value <= u).max(1) - 1).min(columns_u - 1);
1891        let column = (cells.vs.partition_point(|&value| value <= v).max(1) - 1).min(columns - 1);
1892        row * columns + column
1893    };
1894    let mut settled: Vec<usize> = Vec::new();
1895    if is_verified_minimum(surface, point, (best.u, best.v), domains, periodic)? {
1896        settled.push(cell_of(best.u, best.v));
1897    }
1898    for (cell_bound, cell) in candidates {
1899        if cell_bound >= bound {
1900            break;
1901        }
1902        let row = cell / columns;
1903        let column = cell % columns;
1904        let rectangle = [
1905            cells.us[row],
1906            cells.us[row + 1],
1907            cells.vs[column],
1908            cells.vs[column + 1],
1909        ];
1910        if settled.contains(&cell) {
1911            continue;
1912        }
1913        let mut seed = (rectangle[0], rectangle[2]);
1914        let mut seed_distance = f64::INFINITY;
1915        for (node_row, node_column) in
1916            [(row, column), (row + 1, column), (row, column + 1), (row + 1, column + 1)]
1917        {
1918            let distance = cells.nodes[node_row * cells.vs.len() + node_column]
1919                .sub(point)
1920                .length_squared();
1921            if distance < seed_distance {
1922                seed_distance = distance;
1923                seed = (cells.us[node_row], cells.vs[node_column]);
1924            }
1925        }
1926        stats.refinements += 1;
1927        let (confined, on_edge) = confined_descent(surface, point, seed, rectangle, (false, false))?;
1928        let mut found = confined;
1929        if on_edge && confined.distance_squared.sqrt() < bound {
1930            // The cell's least point is on its edge and beats the answer:
1931            // the basin continues past the cell.
1932            stats.continuations += 1;
1933            let (continued, _) =
1934                confined_descent(surface, point, (confined.u, confined.v), domains, periodic)?;
1935            if continued.distance_squared < found.distance_squared {
1936                found = continued;
1937            }
1938            if continued.converged {
1939                settled.push(cell_of(continued.u, continued.v));
1940            }
1941        }
1942        if found.distance_squared.sqrt() < bound {
1943            *best = found;
1944            bound = best.distance_squared.sqrt() - resolution;
1945            stats.replaced = true;
1946            if best.distance_squared.sqrt() <= LINEAR_TOLERANCE {
1947                return Ok(());
1948            }
1949        }
1950    }
1951    Ok(())
1952}
1953
1954
1955
1956/// Domain handling for a local foot-point Newton walk.
1957#[derive(Clone, Copy)]
1958pub(crate) enum FootDomain {
1959    Clamp,
1960    Wrap,
1961    Extended,
1962}
1963
1964#[derive(Clone, Copy)]
1965pub(crate) struct CurveFootPolicy {
1966    pub domain: FootDomain,
1967    pub iterations: usize,
1968    pub slope_guard: f64,
1969    pub step_limit: f64,
1970    pub parameter_tolerance: f64,
1971    pub mode: FootMode,
1972}
1973
1974/// Acceptance and convergence differ between a distance minimum and a stationary root.
1975#[derive(Clone, Copy)]
1976pub(crate) enum FootMode {
1977    Monotone,
1978    Witness,
1979    Stationary { positive_slope: bool, relative_step: bool },
1980    Backtracked,
1981    Planar,
1982}
1983
1984/// Local Newton foot. Seeding and enumeration of local minima belong to the caller.
1985/// Returns the last parameter, best witnessed parameter, and squared distance.
1986fn curve_foot_newton_state(
1987    curve: &NurbsCurve,
1988    point: Vec3,
1989    seed: f64,
1990    bracket: [f64; 2],
1991    policy: CurveFootPolicy,
1992) -> Result<(f64, f64, f64), String> {
1993    let fit = |u| match policy.domain {
1994        FootDomain::Clamp => fit_parameter(u, bracket[0], bracket[1], false),
1995        FootDomain::Wrap => fit_parameter(u, bracket[0], bracket[1], true),
1996        FootDomain::Extended => u,
1997    };
1998    let extended = matches!(policy.domain, FootDomain::Extended) || matches!(policy.mode, FootMode::Backtracked);
1999    let planar = matches!(policy.mode, FootMode::Planar);
2000    let project = |p: Vec3| if planar { Vec3::new(p.x, p.y, 0.0) } else { p };
2001    let point = project(point);
2002    let distance = |u| -> Result<f64, String> {
2003        let p = if extended { curve.derivatives_extended(u, 0)?[0] } else { curve.evaluate(u)? };
2004        Ok(project(p).sub(point).length())
2005    };
2006    let monotone = matches!(policy.mode, FootMode::Monotone | FootMode::Backtracked);
2007    let witness = matches!(policy.mode, FootMode::Witness);
2008    let mut u = if witness { seed } else { fit(seed) };
2009    let mut nearest_u = seed;
2010    let mut nearest_squared = f64::INFINITY;
2011    let mut best = if monotone { distance(u)? } else { f64::INFINITY };
2012    for _ in 0..policy.iterations {
2013        let derivatives = if extended {
2014            let values = curve.derivatives_extended(u, 2)?;
2015            [values[0], values[1], values[2]]
2016        } else if matches!(policy.mode, FootMode::Stationary { .. })
2017            || (planar && !crate::curve::small_curve_derivatives_enabled()) {
2018            let values = curve.derivatives(u, 2)?;
2019            [values[0], values[1], values[2]]
2020        } else {
2021            curve.derivatives_small(u, 2)?
2022        }.map(project);
2023        let offset = derivatives[0].sub(point);
2024        let numerator = offset.dot(derivatives[1]);
2025        let denominator = derivatives[1].dot(derivatives[1]) + offset.dot(derivatives[2]);
2026        if witness {
2027            let squared = offset.length_squared();
2028            if squared < nearest_squared {
2029                nearest_u = u;
2030                nearest_squared = squared;
2031            }
2032            let residual_length = squared.sqrt();
2033            if residual_length <= LINEAR_TOLERANCE
2034                || numerator.abs() <= EPSILON + 1e-10 * derivatives[1].length() * residual_length {
2035                break;
2036            }
2037        }
2038        let singular = if planar { denominator.abs() < policy.slope_guard }
2039            else { denominator.abs() <= policy.slope_guard };
2040        if singular || (matches!(policy.mode, FootMode::Stationary { positive_slope: true, .. }) && !(denominator > 0.0)) { break; }
2041        let mut step = if matches!(policy.mode, FootMode::Backtracked) && !(denominator > 0.0) {
2042            policy.step_limit.copysign(numerator)
2043        } else {
2044            (numerator / denominator).clamp(-policy.step_limit, policy.step_limit)
2045        };
2046        let mut next = fit(u - step);
2047        let mut next_distance = if monotone { distance(next)? } else { f64::INFINITY };
2048        if matches!(policy.mode, FootMode::Backtracked) {
2049            for _ in 0..30 {
2050                if next_distance < best { break; }
2051                step *= 0.5;
2052                next = fit(u - step);
2053                next_distance = distance(next)?;
2054            }
2055            if !(next_distance < best) { break; }
2056        } else if monotone && next_distance >= best { break; }
2057        let moved = if planar || matches!(policy.domain, FootDomain::Extended) { step }
2058            else if matches!(policy.domain, FootDomain::Wrap) {
2059                let period = bracket[1] - bracket[0];
2060                let raw = next - u;
2061                raw - period * (raw / period).round()
2062            } else { next - u };
2063        let tolerance = if matches!(policy.mode, FootMode::Stationary { relative_step: true, .. }) {
2064            policy.parameter_tolerance * (1.0 + u.abs())
2065        } else { policy.parameter_tolerance };
2066        let small = if planar || matches!(policy.mode, FootMode::Backtracked) { moved.abs() < tolerance }
2067            else { moved.abs() <= tolerance };
2068        if small && matches!(policy.mode, FootMode::Monotone) { break; }
2069        u = next;
2070        best = next_distance;
2071        if small { break; }
2072    }
2073    if witness { return Ok((u, nearest_u, nearest_squared)); }
2074    if !monotone { best = distance(u)?; }
2075    Ok((u, u, best * best))
2076}
2077
2078pub(crate) fn curve_foot_newton(
2079    curve: &NurbsCurve, point: Vec3, seed: f64, bracket: [f64; 2], policy: CurveFootPolicy,
2080) -> Result<(f64, f64), String> {
2081    let (_, nearest, squared) = curve_foot_newton_state(curve, point, seed, bracket, policy)?;
2082    Ok((nearest, squared))
2083}