Skip to main content

brep_kernel/geometry/
projection.rs

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