Skip to main content

brep_kernel/geometry/
projection.rs

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