Skip to main content

brep_kernel/geometry/
projection.rs

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