Skip to main content

brep_kernel/geometry/
projection.rs

1use crate::{NurbsCurve, NurbsSurface, Vec3};
2use serde::Serialize;
3
4const EPSILON: f64 = 1e-12;
5const LINEAR_TOLERANCE: f64 = 1e-7;
6const MAX_NEWTON_ITERATIONS: usize = 50;
7
8#[derive(Clone, Copy, Debug, Serialize)]
9pub struct CurveProjection {
10    pub u: f64,
11    pub point: Vec3,
12    pub distance: f64,
13}
14
15#[derive(Clone, Copy, Debug, Serialize)]
16pub struct SurfaceProjection {
17    pub u: f64,
18    pub v: f64,
19    pub point: Vec3,
20    pub distance: f64,
21}
22
23fn interior_knots(knots: &[f64], degree: usize) -> Vec<f64> {
24    let start = knots[degree];
25    let end = knots[knots.len() - 1 - degree];
26    let mut result = Vec::new();
27    for &knot in knots {
28        if knot <= start + 1e-12 || knot >= end - 1e-12 {
29            continue;
30        }
31        if result
32            .last()
33            .is_none_or(|previous: &f64| (*previous - knot).abs() > 1e-12)
34        {
35            result.push(knot);
36        }
37    }
38    result
39}
40
41fn fit_parameter(value: f64, minimum: f64, maximum: f64, closed: bool) -> f64 {
42    if closed {
43        let period = maximum - minimum;
44        (value - minimum).rem_euclid(period) + minimum
45    } else {
46        value.clamp(minimum, maximum)
47    }
48}
49
50pub fn project_point_to_curve(curve: &NurbsCurve, point: Vec3) -> Result<CurveProjection, String> {
51    let [start, end] = curve.domain()?;
52    let mut breaks = vec![start];
53    breaks.extend(interior_knots(&curve.knots, curve.degree));
54    breaks.push(end);
55    let samples_per_span = 4usize.max(curve.degree + 2);
56    let mut best_u = start;
57    let mut best_distance_squared = f64::INFINITY;
58    for pair in breaks.windows(2) {
59        for index in 0..=samples_per_span {
60            let parameter = pair[0] + (pair[1] - pair[0]) * index as f64 / samples_per_span as f64;
61            let distance_squared = curve.evaluate(parameter)?.sub(point).length_squared();
62            if distance_squared < best_distance_squared {
63                best_distance_squared = distance_squared;
64                best_u = parameter;
65            }
66        }
67    }
68
69    let mut parameter = best_u;
70    for _ in 0..MAX_NEWTON_ITERATIONS {
71        let derivatives = curve.derivatives_small(parameter, 2)?;
72        let residual = derivatives[0].sub(point);
73        let f = derivatives[1].dot(residual);
74        let derivative = derivatives[2].dot(residual) + derivatives[1].length_squared();
75        let distance_squared = residual.length_squared();
76        if distance_squared < best_distance_squared {
77            best_distance_squared = distance_squared;
78            best_u = parameter;
79        }
80        let residual_length = distance_squared.sqrt();
81        let tangent_length = derivatives[1].length();
82        if residual_length <= LINEAR_TOLERANCE
83            || f.abs() <= EPSILON + 1e-10 * tangent_length * residual_length
84            || derivative.abs() <= EPSILON
85        {
86            break;
87        }
88        let mut step = -f / derivative;
89        let maximum_step = (end - start) / 4.0;
90        if step.abs() > maximum_step {
91            step = step.signum() * maximum_step;
92        }
93        let next = (parameter + step).clamp(start, end);
94        if (next - parameter).abs() <= 1e-15 * (end - start) {
95            parameter = next;
96            break;
97        }
98        parameter = next;
99    }
100    for candidate in [start, end, parameter] {
101        let distance_squared = curve.evaluate(candidate)?.sub(point).length_squared();
102        if distance_squared < best_distance_squared {
103            best_distance_squared = distance_squared;
104            best_u = candidate;
105        }
106    }
107    let projected = curve.evaluate(best_u)?;
108    Ok(CurveProjection {
109        u: best_u,
110        point: projected,
111        distance: projected.sub(point).length(),
112    })
113}
114
115fn project_linear_revolution(
116    surface: &NurbsSurface,
117    point: Vec3,
118) -> Result<Option<SurfaceProjection>, String> {
119    let rows = &surface.control_points;
120    if surface.degree_u != 2
121        || surface.degree_v != 1
122        || rows.len() < 5
123        || rows[0].len() != 2
124        || !surface.closed_directions()?.0
125    {
126        return Ok(None);
127    }
128    let base = rows[0][0].point()?.add(rows[4][0].point()?).scale(0.5);
129    let top = rows[0][1].point()?.add(rows[4][1].point()?).scale(0.5);
130    let axis_vector = top.sub(base);
131    let height = axis_vector.length();
132    if height <= EPSILON {
133        return Ok(None);
134    }
135    let axis = axis_vector.normalized()?;
136    let [v0, v1] = surface.domain_v()?;
137    let axial = point.sub(base).dot(axis).clamp(0.0, height);
138    let v = v0 + (v1 - v0) * axial / height;
139    let circle = surface.iso_curve_v(v)?;
140    let projection = project_point_to_curve(&circle, point)?;
141    let [u0, u1] = surface.domain_u()?;
142    let u = projection.u.clamp(u0, u1);
143    let projected = surface.evaluate(u, v)?;
144    Ok(Some(SurfaceProjection {
145        u,
146        v,
147        point: projected,
148        distance: projected.sub(point).length(),
149    }))
150}
151
152#[derive(Clone, Copy)]
153struct NewtonResult {
154    u: f64,
155    v: f64,
156    distance_squared: f64,
157    converged: bool,
158}
159
160fn surface_newton(
161    surface: &NurbsSurface,
162    point: Vec3,
163    seed_u: f64,
164    seed_v: f64,
165    domains: [f64; 4],
166    closed_u: bool,
167    closed_v: bool,
168) -> Result<NewtonResult, String> {
169    let [u0, u1, v0, v1] = domains;
170    let mut u = seed_u;
171    let mut v = seed_v;
172    let mut converged = false;
173    let mut best = NewtonResult {
174        u,
175        v,
176        distance_squared: surface.evaluate(u, v)?.sub(point).length_squared(),
177        converged,
178    };
179    for _ in 0..MAX_NEWTON_ITERATIONS {
180        let derivatives = surface.derivatives_small(u, v, 2)?;
181        let residual = derivatives[0][0].sub(point);
182        let distance_squared = residual.length_squared();
183        if distance_squared < best.distance_squared {
184            best.u = u;
185            best.v = v;
186            best.distance_squared = distance_squared;
187        }
188        let f = derivatives[1][0].dot(residual);
189        let g = derivatives[0][1].dot(residual);
190        let residual_length = distance_squared.sqrt();
191        if residual_length <= LINEAR_TOLERANCE {
192            converged = true;
193            break;
194        }
195        let tangent_u_length = derivatives[1][0].length();
196        let tangent_v_length = derivatives[0][1].length();
197        let cosine_u = if tangent_u_length * residual_length <= EPSILON {
198            0.0
199        } else {
200            f.abs() / (tangent_u_length * residual_length)
201        };
202        let cosine_v = if tangent_v_length * residual_length <= EPSILON {
203            0.0
204        } else {
205            g.abs() / (tangent_v_length * residual_length)
206        };
207        if cosine_u <= 1e-10 && cosine_v <= 1e-10 {
208            converged = true;
209            break;
210        }
211        let j00 = derivatives[2][0].dot(residual) + derivatives[1][0].length_squared();
212        let j01 = derivatives[1][1].dot(residual) + derivatives[1][0].dot(derivatives[0][1]);
213        let j11 = derivatives[0][2].dot(residual) + derivatives[0][1].length_squared();
214        let determinant = j00 * j11 - j01 * j01;
215        if determinant.abs() <= EPSILON {
216            break;
217        }
218        let mut du = (-f * j11 + g * j01) / determinant;
219        let mut dv = (-g * j00 + f * j01) / determinant;
220        du = du.clamp(-(u1 - u0) / 4.0, (u1 - u0) / 4.0);
221        dv = dv.clamp(-(v1 - v0) / 4.0, (v1 - v0) / 4.0);
222        let next_u = fit_parameter(u + du, u0, u1, closed_u);
223        let next_v = fit_parameter(v + dv, v0, v1, closed_v);
224        let stalled =
225            (next_u - u).abs() <= 1e-15 * (u1 - u0) && (next_v - v).abs() <= 1e-15 * (v1 - v0);
226        u = next_u;
227        v = next_v;
228        if stalled {
229            break;
230        }
231    }
232    let final_distance_squared = surface.evaluate(u, v)?.sub(point).length_squared();
233    if final_distance_squared < best.distance_squared {
234        best.u = u;
235        best.v = v;
236        best.distance_squared = final_distance_squared;
237    }
238    best.converged = converged;
239    Ok(best)
240}
241
242pub fn project_point_to_surface(
243    surface: &NurbsSurface,
244    point: Vec3,
245) -> Result<SurfaceProjection, String> {
246    // Recognized analytic carriers (plane / cylinder / cone / sphere /
247    // torus) have exact closed-form projections in the surface's own
248    // rational parameterization — skip grid seeding and Newton entirely.
249    if let Some(analytic) = surface.analytic() {
250        if let Some(projection) = analytic.project(surface, point) {
251            return Ok(projection);
252        }
253    }
254    project_point_to_surface_general(surface, point)
255}
256
257/// Project a point onto a surface starting Newton from an explicit `(u, v)`
258/// guess, WITHOUT the global grid seed. This is a footpoint refiner for
259/// continuity-preserving curve-on-surface tracing: seeding each edge sample
260/// from its neighbour's parameters keeps the fit on ONE branch of a surface
261/// that folds back over the small trimmed patch, where an independent global
262/// search would snap to whichever fold is momentarily closest and tear the
263/// pcurve into a self-crossing zig-zag. The caller compares the returned
264/// `distance` against the global answer and only adopts this result when it is
265/// geometrically just as valid, so a bad seed can never make a fit worse.
266pub fn project_point_to_surface_seeded(
267    surface: &NurbsSurface,
268    point: Vec3,
269    seed_u: f64,
270    seed_v: f64,
271) -> Result<SurfaceProjection, String> {
272    let [u0, u1] = surface.domain_u()?;
273    let [v0, v1] = surface.domain_v()?;
274    let (closed_u, closed_v) = surface.closed_directions()?;
275    let result = surface_newton(
276        surface,
277        point,
278        seed_u,
279        seed_v,
280        [u0, u1, v0, v1],
281        closed_u,
282        closed_v,
283    )?;
284    let projected = surface.evaluate(result.u, result.v)?;
285    Ok(SurfaceProjection {
286        u: result.u,
287        v: result.v,
288        point: projected,
289        distance: projected.sub(point).length(),
290    })
291}
292
293/// The Newton seed grid for general projection: (u, v, point) samples over
294/// every knot span. A pure function of the surface, cached on it — building
295/// the grid costs hundreds of evaluations and projection is the hottest
296/// entry point in the kernel.
297fn projection_seed_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
298    if let Some(grid) = surface.projection_grid.get() {
299        return Ok(grid);
300    }
301    let [u0, u1] = surface.domain_u()?;
302    let [v0, v1] = surface.domain_v()?;
303    let (closed_u, closed_v) = surface.closed_directions()?;
304    let mut breaks_u = vec![u0];
305    breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
306    breaks_u.push(u1);
307    let mut breaks_v = vec![v0];
308    breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
309    breaks_v.push(v1);
310    let samples_u = if closed_u {
311        8usize.max(surface.degree_u * 4)
312    } else {
313        3usize.max(surface.degree_u + 1)
314    };
315    let samples_v = if closed_v {
316        8usize.max(surface.degree_v * 4)
317    } else {
318        3usize.max(surface.degree_v + 1)
319    };
320    let mut grid = Vec::new();
321    for u_pair in breaks_u.windows(2) {
322        for v_pair in breaks_v.windows(2) {
323            for i in 0..=samples_u {
324                for j in 0..=samples_v {
325                    let u = u_pair[0] + (u_pair[1] - u_pair[0]) * i as f64 / samples_u as f64;
326                    let v = v_pair[0] + (v_pair[1] - v_pair[0]) * j as f64 / samples_v as f64;
327                    grid.push((u, v, surface.evaluate(u, v)?));
328                }
329            }
330        }
331    }
332    Ok(surface.projection_grid.get_or_init(|| grid))
333}
334
335/// The dense fallback grid for degenerate general projection: (u, v, point)
336/// over a `(count_u+1) × (count_v+1)` lattice with `count_u/count_v` derived
337/// from the surface's knot spans. A pure function of the surface, cached on
338/// it — on metre-unit-mm parts the degenerate fallback fires ~1.7M times and
339/// each re-evaluated all ~561 lattice points; caching them makes the argmin a
340/// pure distance scan over reused points (bit-identical seed).
341fn projection_dense_grid(surface: &NurbsSurface) -> Result<&[(f64, f64, Vec3)], String> {
342    if let Some(grid) = surface.projection_dense_grid.get() {
343        return Ok(grid);
344    }
345    let [u0, u1] = surface.domain_u()?;
346    let [v0, v1] = surface.domain_v()?;
347    let mut breaks_u = vec![u0];
348    breaks_u.extend(interior_knots(&surface.knots_u, surface.degree_u));
349    breaks_u.push(u1);
350    let mut breaks_v = vec![v0];
351    breaks_v.extend(interior_knots(&surface.knots_v, surface.degree_v));
352    breaks_v.push(v1);
353    let count_u = 32usize.max((breaks_u.len() - 1) * 8);
354    let count_v = 16usize.max((breaks_v.len() - 1) * 8);
355    let mut grid = Vec::with_capacity((count_u + 1) * (count_v + 1));
356    for i in 0..=count_u {
357        for j in 0..=count_v {
358            let u = u0 + (u1 - u0) * i as f64 / count_u as f64;
359            let v = v0 + (v1 - v0) * j as f64 / count_v as f64;
360            grid.push((u, v, surface.evaluate(u, v)?));
361        }
362    }
363    Ok(surface.projection_dense_grid.get_or_init(|| grid))
364}
365
366// Boundary-ring cache geometry: 4 sides × 22 exponents × 33 indices. The scan
367// positions depend only on the surface domain and these fixed integers, so the
368// evaluated points are a pure function of the surface and cacheable.
369const RING_EXPONENTS: usize = 22;
370const RING_INDICES: usize = 33; // index 0..=32
371
372#[inline]
373fn ring_slot(side: usize, exponent_index: usize, index: usize) -> usize {
374    (side * RING_EXPONENTS + exponent_index) * RING_INDICES + index
375}
376
377/// The boundary-ring sample points for the degenerate projection fallback,
378/// flattened by `ring_slot(side, exponent-1, index)`. Sides: 0 = v-low
379/// (`v = v0 + (v1-v0)·offset`), 1 = v-high (`v1 - (v1-v0)·offset`), 2 = u-low
380/// (`u0 + (u1-u0)·offset`), 3 = u-high (`u1 - (u1-u0)·offset`), where
381/// `offset = 1/2^exponent`. A pure function of the surface, cached on it — the
382/// fallback's boundary ring re-evaluated these on every one of ~1.7M calls.
383fn projection_ring_grid(surface: &NurbsSurface) -> Result<&[Vec3], String> {
384    if let Some(grid) = surface.projection_ring_grid.get() {
385        return Ok(grid);
386    }
387    let [u0, u1] = surface.domain_u()?;
388    let [v0, v1] = surface.domain_v()?;
389    let mut grid = vec![Vec3::default(); 4 * RING_EXPONENTS * RING_INDICES];
390    for exponent in 1..=RING_EXPONENTS as i32 {
391        let offset = 1.0 / 2f64.powi(exponent);
392        let e = exponent as usize - 1;
393        let v_lo = v0 + (v1 - v0) * offset;
394        let v_hi = v1 - (v1 - v0) * offset;
395        let u_lo = u0 + (u1 - u0) * offset;
396        let u_hi = u1 - (u1 - u0) * offset;
397        for index in 0..=32 {
398            let u = u0 + (u1 - u0) * index as f64 / 32.0;
399            grid[ring_slot(0, e, index as usize)] = surface.evaluate(u, v_lo)?;
400            grid[ring_slot(1, e, index as usize)] = surface.evaluate(u, v_hi)?;
401        }
402        for index in 0..=32 {
403            let v = v0 + (v1 - v0) * index as f64 / 32.0;
404            grid[ring_slot(2, e, index as usize)] = surface.evaluate(u_lo, v)?;
405            grid[ring_slot(3, e, index as usize)] = surface.evaluate(u_hi, v)?;
406        }
407    }
408    Ok(surface.projection_ring_grid.get_or_init(|| grid))
409}
410
411pub(crate) fn project_point_to_surface_general(
412    surface: &NurbsSurface,
413    point: Vec3,
414) -> Result<SurfaceProjection, String> {
415    let [u0, u1] = surface.domain_u()?;
416    let [v0, v1] = surface.domain_v()?;
417    let (closed_u, closed_v) = surface.closed_directions()?;
418    let mut best = NewtonResult {
419        u: u0,
420        v: v0,
421        distance_squared: f64::INFINITY,
422        converged: false,
423    };
424    for &(u, v, sample) in projection_seed_grid(surface)? {
425        let distance_squared = sample.sub(point).length_squared();
426        if distance_squared < best.distance_squared {
427            best.u = u;
428            best.v = v;
429            best.distance_squared = distance_squared;
430        }
431    }
432    if let Some(revolution) = project_linear_revolution(surface, point)? {
433        if revolution.distance * revolution.distance < best.distance_squared {
434            best.u = revolution.u;
435            best.v = revolution.v;
436            best.distance_squared = revolution.distance * revolution.distance;
437        }
438    }
439    let first_newton = surface_newton(
440        surface,
441        point,
442        best.u,
443        best.v,
444        [u0, u1, v0, v1],
445        closed_u,
446        closed_v,
447    )?;
448    if first_newton.distance_squared < best.distance_squared {
449        best = first_newton;
450    }
451    let (_, su, sv) = surface.deriv1(best.u, best.v)?;
452    let degenerate = su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length());
453    if degenerate || !first_newton.converged {
454        let mut grid = best;
455        for &(u, v, sample) in projection_dense_grid(surface)? {
456            let distance_squared = sample.sub(point).length_squared();
457            if distance_squared < grid.distance_squared {
458                grid.u = u;
459                grid.v = v;
460                grid.distance_squared = distance_squared;
461            }
462        }
463        if grid.distance_squared < best.distance_squared {
464            best = grid;
465        }
466        let polished = surface_newton(
467            surface,
468            point,
469            grid.u,
470            grid.v,
471            [u0, u1, v0, v1],
472            closed_u,
473            closed_v,
474        )?;
475        if polished.distance_squared < best.distance_squared {
476            best = polished;
477        }
478
479        let (_, su, sv) = surface.deriv1(best.u, best.v)?;
480        if su.cross(sv).length() <= 1e-5 * (1.0 + su.length() + sv.length()) {
481            let ring_grid = projection_ring_grid(surface)?;
482            let mut ring = best;
483            // Read the pre-evaluated ring points at the exact same parameter
484            // positions the live scans used (side/exponent/index → `ring_slot`),
485            // preserving scan order and argmin tie-breaking for bit-identity.
486            let scan = |side: usize,
487                        exponent_index: usize,
488                        along_u: bool,
489                        fixed: f64,
490                        candidate: &mut NewtonResult| {
491                for index in 0..=32 {
492                    let moving = if along_u {
493                        u0 + (u1 - u0) * index as f64 / 32.0
494                    } else {
495                        v0 + (v1 - v0) * index as f64 / 32.0
496                    };
497                    let (u, v) = if along_u {
498                        (moving, fixed)
499                    } else {
500                        (fixed, moving)
501                    };
502                    let distance_squared = ring_grid
503                        [ring_slot(side, exponent_index, index as usize)]
504                    .sub(point)
505                    .length_squared();
506                    if distance_squared < candidate.distance_squared {
507                        candidate.u = u;
508                        candidate.v = v;
509                        candidate.distance_squared = distance_squared;
510                    }
511                }
512            };
513            for exponent in 1..=22 {
514                let offset = 1.0 / 2f64.powi(exponent);
515                let e = exponent as usize - 1;
516                if (best.v - v0).abs() <= (v1 - v0) * 0.02 {
517                    scan(0, e, true, v0 + (v1 - v0) * offset, &mut ring);
518                }
519                if (best.v - v1).abs() <= (v1 - v0) * 0.02 {
520                    scan(1, e, true, v1 - (v1 - v0) * offset, &mut ring);
521                }
522                if (best.u - u0).abs() <= (u1 - u0) * 0.02 {
523                    scan(2, e, false, u0 + (u1 - u0) * offset, &mut ring);
524                }
525                if (best.u - u1).abs() <= (u1 - u0) * 0.02 {
526                    scan(3, e, false, u1 - (u1 - u0) * offset, &mut ring);
527                }
528            }
529            if ring.distance_squared < best.distance_squared {
530                best = ring;
531            }
532            let polished = surface_newton(
533                surface,
534                point,
535                ring.u,
536                ring.v,
537                [u0, u1, v0, v1],
538                closed_u,
539                closed_v,
540            )?;
541            if polished.distance_squared < best.distance_squared {
542                best = polished;
543            }
544        }
545    }
546    let projected = surface.evaluate(best.u, best.v)?;
547    Ok(SurfaceProjection {
548        u: best.u,
549        v: best.v,
550        point: projected,
551        distance: projected.sub(point).length(),
552    })
553}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use crate::{make_arc, make_sphere_surface};
559
560    #[test]
561    fn curve_projection_finds_arc_footpoint() {
562        let arc = make_arc(
563            Vec3::default(),
564            Vec3::new(1.0, 0.0, 0.0),
565            Vec3::new(0.0, 1.0, 0.0),
566            5.0,
567            0.0,
568            std::f64::consts::PI,
569        )
570        .unwrap();
571        let projection = project_point_to_curve(&arc, Vec3::new(3.0, 4.0, 2.0)).unwrap();
572        assert!(projection.point.sub(Vec3::new(3.0, 4.0, 0.0)).length() < 1e-8);
573        assert!((projection.distance - 2.0).abs() < 1e-8);
574    }
575
576    #[test]
577    fn surface_projection_handles_sphere_near_pole() {
578        let sphere = make_sphere_surface(Vec3::default(), 5.0, Vec3::new(0.0, 1.0, 0.0)).unwrap();
579        let on_surface = Vec3::new(0.15, (25.0_f64 - 0.15 * 0.15).sqrt(), 0.0);
580        let projection = project_point_to_surface(&sphere, on_surface).unwrap();
581        assert!(projection.distance < 1e-7);
582    }
583}