Skip to main content

brepkit_operations/
distance.rs

1//! Distance measurement between shapes.
2//!
3//! Computes minimum distance between solids and point-to-solid distance.
4//! Supports planar, NURBS, and analytic (cylinder, cone, sphere, torus) faces
5//! with BVH spatial acceleration.
6
7#![allow(
8    clippy::many_single_char_names,
9    clippy::similar_names,
10    clippy::suboptimal_flops,
11    clippy::needless_range_loop,
12    clippy::cast_precision_loss,
13    clippy::doc_markdown,
14    clippy::module_name_repetitions,
15    clippy::cast_sign_loss,
16    clippy::cast_possible_truncation,
17    clippy::manual_let_else,
18    clippy::needless_pass_by_value,
19    clippy::imprecise_flops
20)]
21
22use brepkit_geometry::extrema::{
23    point_to_cone as geo_point_to_cone, point_to_cylinder as geo_point_to_cylinder,
24    point_to_sphere as geo_point_to_sphere, point_to_torus as geo_point_to_torus,
25};
26use brepkit_math::aabb::Aabb3;
27use brepkit_math::bvh::Bvh;
28use brepkit_math::tolerance::Tolerance;
29use brepkit_math::vec::{Point3, Vec3};
30use brepkit_topology::Topology;
31use brepkit_topology::face::{FaceId, FaceSurface};
32use brepkit_topology::solid::SolidId;
33
34use crate::boolean::face_polygon;
35
36/// Result of a distance computation.
37#[derive(Debug, Clone)]
38pub struct DistanceResult {
39    /// The minimum distance found.
40    pub distance: f64,
41    /// The closest point on the first shape.
42    pub point_a: Point3,
43    /// The closest point on the second shape.
44    pub point_b: Point3,
45}
46
47/// Compute the minimum distance from a point to a solid.
48///
49/// Uses BVH over face AABBs for acceleration. Dispatches per face type:
50/// planar (point-to-polygon), NURBS (Newton projection), and analytic
51/// (closed-form for cylinder/cone/sphere/torus).
52///
53/// # Errors
54///
55/// Returns an error if the solid is invalid.
56#[allow(clippy::too_many_lines)]
57pub fn point_to_solid_distance(
58    topo: &Topology,
59    point: Point3,
60    solid: SolidId,
61) -> Result<DistanceResult, crate::OperationsError> {
62    let tol = Tolerance::new();
63
64    let solid_data = topo.solid(solid)?;
65    let shell = topo.shell(solid_data.outer_shell())?;
66    let face_ids: Vec<FaceId> = shell.faces().to_vec();
67
68    let face_aabbs = build_face_aabbs(topo, &face_ids)?;
69    let bvh = Bvh::build(&face_aabbs);
70
71    let mut best_dist = f64::INFINITY;
72    let mut best_point = point;
73
74    let candidates = bvh_distance_candidates(&bvh, &face_aabbs, point);
75
76    for idx in candidates {
77        let fid = face_ids[idx];
78        let aabb_dist_sq = face_aabbs[idx].1.distance_squared_to_point(point);
79        if aabb_dist_sq > best_dist * best_dist {
80            continue;
81        }
82
83        if let Some((dist, closest)) = point_to_face_distance(topo, point, fid, tol)?
84            && dist < best_dist
85        {
86            best_dist = dist;
87            best_point = closest;
88        }
89    }
90
91    Ok(DistanceResult {
92        distance: best_dist,
93        point_a: point,
94        point_b: best_point,
95    })
96}
97
98/// Compute the minimum distance between two solids.
99///
100/// Checks vertices of each solid against faces of the other, with
101/// BVH acceleration. Also checks edge-to-edge distances for the
102/// closest vertex pairs.
103///
104/// # Errors
105///
106/// Returns an error if either solid is invalid.
107#[allow(clippy::too_many_lines)]
108pub fn solid_to_solid_distance(
109    topo: &Topology,
110    solid_a: SolidId,
111    solid_b: SolidId,
112) -> Result<DistanceResult, crate::OperationsError> {
113    let tol = Tolerance::new();
114
115    let verts_a = collect_solid_points(topo, solid_a)?;
116    let verts_b = collect_solid_points(topo, solid_b)?;
117
118    let mut best_dist = f64::INFINITY;
119    let mut best_a = Point3::new(0.0, 0.0, 0.0);
120    let mut best_b = Point3::new(0.0, 0.0, 0.0);
121
122    for &pa in &verts_a {
123        for &pb in &verts_b {
124            let dist = (pa - pb).length();
125            if dist < best_dist {
126                best_dist = dist;
127                best_a = pa;
128                best_b = pb;
129            }
130        }
131    }
132
133    // Vertices of A against faces of B.
134    let data_b = topo.solid(solid_b)?;
135    let shell_b = topo.shell(data_b.outer_shell())?;
136    let faces_b: Vec<FaceId> = shell_b.faces().to_vec();
137    let aabbs_b = build_face_aabbs(topo, &faces_b)?;
138    let bvh_b = Bvh::build(&aabbs_b);
139
140    for &pa in &verts_a {
141        let candidates = bvh_distance_candidates(&bvh_b, &aabbs_b, pa);
142        for idx in candidates {
143            let aabb_dist_sq = aabbs_b[idx].1.distance_squared_to_point(pa);
144            if aabb_dist_sq > best_dist * best_dist {
145                continue;
146            }
147            if let Some((dist, closest)) = point_to_face_distance(topo, pa, faces_b[idx], tol)?
148                && dist < best_dist
149            {
150                best_dist = dist;
151                best_a = pa;
152                best_b = closest;
153            }
154        }
155    }
156
157    // Vertices of B against faces of A.
158    let data_a = topo.solid(solid_a)?;
159    let shell_a = topo.shell(data_a.outer_shell())?;
160    let faces_a: Vec<FaceId> = shell_a.faces().to_vec();
161    let aabbs_a = build_face_aabbs(topo, &faces_a)?;
162    let bvh_a = Bvh::build(&aabbs_a);
163
164    for &pb in &verts_b {
165        let candidates = bvh_distance_candidates(&bvh_a, &aabbs_a, pb);
166        for idx in candidates {
167            let aabb_dist_sq = aabbs_a[idx].1.distance_squared_to_point(pb);
168            if aabb_dist_sq > best_dist * best_dist {
169                continue;
170            }
171            if let Some((dist, closest)) = point_to_face_distance(topo, pb, faces_a[idx], tol)?
172                && dist < best_dist
173            {
174                best_dist = dist;
175                best_a = closest;
176                best_b = pb;
177            }
178        }
179    }
180
181    // Edge-to-edge pass for closest edge pairs.
182    let edges_a = collect_solid_edges(topo, solid_a)?;
183    let edges_b = collect_solid_edges(topo, solid_b)?;
184
185    for &(a1, a2) in &edges_a {
186        for &(b1, b2) in &edges_b {
187            let (dist, ca, cb) = segment_to_segment_distance(a1, a2, b1, b2);
188            if dist < best_dist {
189                best_dist = dist;
190                best_a = ca;
191                best_b = cb;
192            }
193        }
194    }
195
196    Ok(DistanceResult {
197        distance: best_dist,
198        point_a: best_a,
199        point_b: best_b,
200    })
201}
202
203/// Compute the minimum distance from a point to a face.
204///
205/// # Errors
206///
207/// Returns an error if the face lookup fails.
208pub fn point_to_face(
209    topo: &Topology,
210    point: Point3,
211    face_id: FaceId,
212) -> Result<DistanceResult, crate::OperationsError> {
213    let tol = Tolerance::new();
214    if let Some((dist, closest)) = point_to_face_distance(topo, point, face_id, tol)? {
215        Ok(DistanceResult {
216            distance: dist,
217            point_a: point,
218            point_b: closest,
219        })
220    } else {
221        // Fallback: distance to closest wire vertex
222        let face = topo.face(face_id)?;
223        let wire = topo.wire(face.outer_wire())?;
224        let mut best = f64::INFINITY;
225        let mut best_pt = point;
226        for oe in wire.edges() {
227            let edge = topo.edge(oe.edge())?;
228            let vp = topo.vertex(edge.start())?.point();
229            let d = (point - vp).length();
230            if d < best {
231                best = d;
232                best_pt = vp;
233            }
234        }
235        Ok(DistanceResult {
236            distance: best,
237            point_a: point,
238            point_b: best_pt,
239        })
240    }
241}
242
243/// Compute the minimum distance from a point to an edge.
244///
245/// For line edges, uses exact point-to-segment distance.
246/// For curved edges, samples the curve and returns the closest sample.
247///
248/// # Errors
249///
250/// Returns an error if the edge lookup fails.
251#[allow(clippy::cast_precision_loss)]
252pub fn point_to_edge(
253    topo: &Topology,
254    point: Point3,
255    edge_id: brepkit_topology::edge::EdgeId,
256) -> Result<DistanceResult, crate::OperationsError> {
257    let edge = topo.edge(edge_id)?;
258    let start = topo.vertex(edge.start())?.point();
259    let end = topo.vertex(edge.end())?.point();
260
261    if matches!(edge.curve(), brepkit_topology::edge::EdgeCurve::Line) {
262        let closest = closest_point_on_segment(point, start, end);
263        let dist = (point - closest).length();
264        Ok(DistanceResult {
265            distance: dist,
266            point_a: point,
267            point_b: closest,
268        })
269    } else {
270        let (t0, t1) = match edge.curve() {
271            brepkit_topology::edge::EdgeCurve::NurbsCurve(nc) => nc.domain(),
272            brepkit_topology::edge::EdgeCurve::Circle(c) => {
273                if edge.is_closed() {
274                    (0.0, std::f64::consts::TAU)
275                } else {
276                    // Project start/end vertices to get actual arc parameter range.
277                    let mut t0 = c.project(start);
278                    let mut t1 = c.project(end);
279                    if t0 < 0.0 {
280                        t0 += std::f64::consts::TAU;
281                    }
282                    if t1 <= t0 {
283                        t1 += std::f64::consts::TAU;
284                    }
285                    (t0, t1)
286                }
287            }
288            brepkit_topology::edge::EdgeCurve::Ellipse(e) => {
289                if edge.is_closed() {
290                    (0.0, std::f64::consts::TAU)
291                } else {
292                    let mut t0 = e.project(start);
293                    let mut t1 = e.project(end);
294                    if t0 < 0.0 {
295                        t0 += std::f64::consts::TAU;
296                    }
297                    if t1 <= t0 {
298                        t1 += std::f64::consts::TAU;
299                    }
300                    (t0, t1)
301                }
302            }
303            // Line was handled above (early return via `if` branch).
304            brepkit_topology::edge::EdgeCurve::Line => (0.0, 0.0),
305        };
306        let n_samples = 64;
307        let mut best_dist = f64::INFINITY;
308        let mut best_pt = start;
309        for i in 0..=n_samples {
310            let t = t0 + (t1 - t0) * (i as f64) / (n_samples as f64);
311            let pt = match edge.curve() {
312                brepkit_topology::edge::EdgeCurve::NurbsCurve(nc) => nc.evaluate(t),
313                brepkit_topology::edge::EdgeCurve::Circle(c) => c.evaluate(t),
314                brepkit_topology::edge::EdgeCurve::Ellipse(e) => e.evaluate(t),
315                // Line was handled above.
316                brepkit_topology::edge::EdgeCurve::Line => start,
317            };
318            let d = (point - pt).length();
319            if d < best_dist {
320                best_dist = d;
321                best_pt = pt;
322            }
323        }
324        Ok(DistanceResult {
325            distance: best_dist,
326            point_a: point,
327            point_b: best_pt,
328        })
329    }
330}
331
332/// Closest point on a line segment to a point.
333fn closest_point_on_segment(point: Point3, a: Point3, b: Point3) -> Point3 {
334    let ab = b - a;
335    let len_sq = ab.length_squared();
336    if len_sq < 1e-30 {
337        return a;
338    }
339    let ap = point - a;
340    let t = ap.dot(ab) / len_sq;
341    let t = t.clamp(0.0, 1.0);
342    a + ab * t
343}
344
345/// Compute the distance from a point to a single face, dispatching by type.
346pub(crate) fn point_to_face_distance(
347    topo: &Topology,
348    point: Point3,
349    face_id: FaceId,
350    tol: Tolerance,
351) -> Result<Option<(f64, Point3)>, crate::OperationsError> {
352    let face = topo.face(face_id)?;
353    match face.surface() {
354        FaceSurface::Plane { normal, d } => {
355            let verts = face_polygon(topo, face_id)?;
356            Ok(point_to_polygon_distance(point, &verts, *normal, *d, tol))
357        }
358        FaceSurface::Nurbs(surface) => {
359            let proj = brepkit_math::nurbs::projection::project_point_to_surface(
360                surface, point, tol.linear,
361            );
362            match proj {
363                Ok(p) => Ok(Some((p.distance, p.point))),
364                Err(_) => Ok(None),
365            }
366        }
367        FaceSurface::Cylinder(cyl) => Ok(Some(point_to_cylinder(point, cyl))),
368        FaceSurface::Cone(cone) => Ok(Some(point_to_cone(point, cone))),
369        FaceSurface::Sphere(sph) => Ok(Some(point_to_sphere(point, sph))),
370        FaceSurface::Torus(tor) => Ok(Some(point_to_torus(point, tor))),
371    }
372}
373
374// -- Analytic point-to-surface distance (delegating to brepkit_geometry) ------
375
376/// Closest point on a cylinder to a given point.
377fn point_to_cylinder(
378    point: Point3,
379    cyl: &brepkit_math::surfaces::CylindricalSurface,
380) -> (f64, Point3) {
381    let proj = geo_point_to_cylinder(point, cyl);
382    (proj.distance, proj.point)
383}
384
385/// Closest point on a cone to a given point.
386fn point_to_cone(point: Point3, cone: &brepkit_math::surfaces::ConicalSurface) -> (f64, Point3) {
387    let proj = geo_point_to_cone(point, cone);
388    (proj.distance, proj.point)
389}
390
391/// Closest point on a sphere to a given point.
392fn point_to_sphere(
393    point: Point3,
394    sphere: &brepkit_math::surfaces::SphericalSurface,
395) -> (f64, Point3) {
396    let proj = geo_point_to_sphere(point, sphere);
397    (proj.distance, proj.point)
398}
399
400/// Closest point on a torus to a given point.
401fn point_to_torus(point: Point3, torus: &brepkit_math::surfaces::ToroidalSurface) -> (f64, Point3) {
402    let proj = geo_point_to_torus(point, torus);
403    (proj.distance, proj.point)
404}
405
406// -- BVH helpers --------------------------------------------------------------
407
408/// Build AABBs for a set of faces (from vertex extents).
409fn build_face_aabbs(
410    topo: &Topology,
411    face_ids: &[FaceId],
412) -> Result<Vec<(usize, Aabb3)>, crate::OperationsError> {
413    let mut result = Vec::with_capacity(face_ids.len());
414    for (i, &fid) in face_ids.iter().enumerate() {
415        let face = topo.face(fid)?;
416        let wire = topo.wire(face.outer_wire())?;
417        let mut min = Point3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
418        let mut max = Point3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
419        for oe in wire.edges() {
420            let edge = topo.edge(oe.edge())?;
421            for vid in [edge.start(), edge.end()] {
422                let p = topo.vertex(vid)?.point();
423                min = Point3::new(min.x().min(p.x()), min.y().min(p.y()), min.z().min(p.z()));
424                max = Point3::new(max.x().max(p.x()), max.y().max(p.y()), max.z().max(p.z()));
425            }
426        }
427        // Expand AABB slightly for analytic surfaces (they may extend beyond vertices).
428        let margin = 0.01;
429        min = Point3::new(min.x() - margin, min.y() - margin, min.z() - margin);
430        max = Point3::new(max.x() + margin, max.y() + margin, max.z() + margin);
431        result.push((i, Aabb3 { min, max }));
432    }
433    Ok(result)
434}
435
436/// Get candidate face indices sorted by AABB distance to a point.
437fn bvh_distance_candidates(bvh: &Bvh, aabbs: &[(usize, Aabb3)], point: Point3) -> Vec<usize> {
438    // For simplicity, query all faces and sort by AABB distance.
439    let mut candidates: Vec<(usize, f64)> = aabbs
440        .iter()
441        .map(|(i, aabb)| (*i, aabb.distance_squared_to_point(point)))
442        .collect();
443    candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
444
445    if let Some(closest_idx) = bvh.query_closest(point)
446        && let Some(pos) = candidates.iter().position(|(i, _)| *i == closest_idx)
447    {
448        candidates.swap(0, pos);
449    }
450
451    candidates.into_iter().map(|(i, _)| i).collect()
452}
453
454// -- Segment-to-segment distance ----------------------------------------------
455
456/// Compute the minimum distance between two 3D line segments.
457///
458/// Delegates to [`brepkit_geometry::extrema::segment_segment_distance`].
459fn segment_to_segment_distance(
460    a1: Point3,
461    a2: Point3,
462    b1: Point3,
463    b2: Point3,
464) -> (f64, Point3, Point3) {
465    brepkit_geometry::extrema::segment_segment_distance(a1, a2, b1, b2)
466}
467
468/// Collect all edge segments from a solid.
469fn collect_solid_edges(
470    topo: &Topology,
471    solid: SolidId,
472) -> Result<Vec<(Point3, Point3)>, crate::OperationsError> {
473    let mut seen = std::collections::HashSet::new();
474    let mut edges = Vec::new();
475
476    let solid_data = topo.solid(solid)?;
477    let shell = topo.shell(solid_data.outer_shell())?;
478
479    for &fid in shell.faces() {
480        let face = topo.face(fid)?;
481        let wire = topo.wire(face.outer_wire())?;
482        for oe in wire.edges() {
483            if seen.insert(oe.edge().index()) {
484                let edge = topo.edge(oe.edge())?;
485                let p1 = topo.vertex(edge.start())?.point();
486                let p2 = topo.vertex(edge.end())?.point();
487                edges.push((p1, p2));
488            }
489        }
490    }
491
492    Ok(edges)
493}
494
495// -- Existing helpers (preserved) ---------------------------------------------
496
497/// Compute the distance from a point to a planar polygon.
498///
499/// Returns `(distance, closest_point)` or `None` if the polygon is degenerate.
500fn point_to_polygon_distance(
501    point: Point3,
502    verts: &[Point3],
503    normal: Vec3,
504    d: f64,
505    _tol: Tolerance,
506) -> Option<(f64, Point3)> {
507    if verts.len() < 3 {
508        return None;
509    }
510
511    let signed_dist = normal.dot(Vec3::new(point.x(), point.y(), point.z())) - d;
512    let projected = Point3::new(
513        (-normal.x()).mul_add(signed_dist, point.x()),
514        (-normal.y()).mul_add(signed_dist, point.y()),
515        (-normal.z()).mul_add(signed_dist, point.z()),
516    );
517
518    if point_in_polygon_3d(&projected, verts, &normal) {
519        return Some((signed_dist.abs(), projected));
520    }
521
522    let mut best_dist = f64::INFINITY;
523    let mut best_point = verts[0];
524    let n = verts.len();
525
526    for i in 0..n {
527        let j = (i + 1) % n;
528        let (dist, closest) = point_to_segment_distance(point, verts[i], verts[j]);
529        if dist < best_dist {
530            best_dist = dist;
531            best_point = closest;
532        }
533    }
534
535    Some((best_dist, best_point))
536}
537
538/// Point-in-polygon test for 3D (projecting to 2D).
539pub(crate) fn point_in_polygon_3d(point: &Point3, polygon: &[Point3], normal: &Vec3) -> bool {
540    use brepkit_math::predicates::point_in_polygon;
541    use brepkit_math::vec::Point2;
542
543    let ax = normal.x().abs();
544    let ay = normal.y().abs();
545    let az = normal.z().abs();
546
547    let (proj_pt, proj_poly): (Point2, Vec<Point2>) = if az >= ax && az >= ay {
548        (
549            Point2::new(point.x(), point.y()),
550            polygon.iter().map(|p| Point2::new(p.x(), p.y())).collect(),
551        )
552    } else if ay >= ax {
553        (
554            Point2::new(point.x(), point.z()),
555            polygon.iter().map(|p| Point2::new(p.x(), p.z())).collect(),
556        )
557    } else {
558        (
559            Point2::new(point.y(), point.z()),
560            polygon.iter().map(|p| Point2::new(p.y(), p.z())).collect(),
561        )
562    };
563
564    point_in_polygon(proj_pt, &proj_poly)
565}
566
567/// Distance from a point to a line segment.
568fn point_to_segment_distance(point: Point3, a: Point3, b: Point3) -> (f64, Point3) {
569    let ab = b - a;
570    let ap = point - a;
571    let len_sq = ab.length_squared();
572
573    if len_sq < 1e-30 {
574        return ((point - a).length(), a);
575    }
576
577    let t = (ap.dot(ab) / len_sq).clamp(0.0, 1.0);
578    let closest = Point3::new(
579        ab.x().mul_add(t, a.x()),
580        ab.y().mul_add(t, a.y()),
581        ab.z().mul_add(t, a.z()),
582    );
583    ((point - closest).length(), closest)
584}
585
586/// Collect all unique vertex positions from a solid.
587fn collect_solid_points(
588    topo: &Topology,
589    solid: SolidId,
590) -> Result<Vec<Point3>, crate::OperationsError> {
591    let mut seen = std::collections::HashSet::new();
592    let mut points = Vec::new();
593
594    let solid_data = topo.solid(solid)?;
595    let shell = topo.shell(solid_data.outer_shell())?;
596
597    for &fid in shell.faces() {
598        let face = topo.face(fid)?;
599        let wire = topo.wire(face.outer_wire())?;
600        for oe in wire.edges() {
601            let edge = topo.edge(oe.edge())?;
602            for vid in [edge.start(), edge.end()] {
603                if seen.insert(vid.index()) {
604                    points.push(topo.vertex(vid)?.point());
605                }
606            }
607        }
608    }
609
610    Ok(points)
611}
612
613#[cfg(test)]
614mod tests {
615    #![allow(clippy::unwrap_used)]
616
617    use brepkit_math::tolerance::Tolerance;
618    use brepkit_math::vec::Point3;
619    use brepkit_topology::Topology;
620    use brepkit_topology::test_utils::make_unit_cube_manifold_at;
621
622    use super::*;
623
624    #[test]
625    fn point_inside_cube_distance_is_half() {
626        let mut topo = Topology::new();
627        let cube = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
628
629        // Point at center of cube — closest face is 0.5 away.
630        let result = point_to_solid_distance(&topo, Point3::new(0.5, 0.5, 0.5), cube).unwrap();
631        let tol = Tolerance::loose();
632        assert!(
633            tol.approx_eq(result.distance, 0.5),
634            "center-to-face distance should be ~0.5, got {}",
635            result.distance
636        );
637    }
638
639    #[test]
640    fn point_outside_cube_distance() {
641        let mut topo = Topology::new();
642        let cube = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
643
644        // Point above the cube.
645        let result = point_to_solid_distance(&topo, Point3::new(0.5, 0.5, 3.0), cube).unwrap();
646        let tol = Tolerance::loose();
647        assert!(
648            tol.approx_eq(result.distance, 2.0),
649            "point 2 above cube top should be distance ~2.0, got {}",
650            result.distance
651        );
652    }
653
654    #[test]
655    fn disjoint_cubes_distance() {
656        let mut topo = Topology::new();
657        let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
658        let b = make_unit_cube_manifold_at(&mut topo, 5.0, 0.0, 0.0);
659
660        let result = solid_to_solid_distance(&topo, a, b).unwrap();
661        let tol = Tolerance::loose();
662        // Cubes are [0,1] and [5,6], gap is 4.0.
663        assert!(
664            tol.approx_eq(result.distance, 4.0),
665            "disjoint cubes should be ~4.0 apart, got {}",
666            result.distance
667        );
668    }
669
670    #[test]
671    fn adjacent_cubes_distance_is_zero() {
672        let mut topo = Topology::new();
673        let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
674        let b = make_unit_cube_manifold_at(&mut topo, 1.0, 0.0, 0.0);
675
676        let result = solid_to_solid_distance(&topo, a, b).unwrap();
677        let tol = Tolerance::loose();
678        assert!(
679            tol.approx_eq(result.distance, 0.0),
680            "touching cubes should have distance ~0, got {}",
681            result.distance
682        );
683    }
684
685    #[test]
686    fn same_solid_distance_is_zero() {
687        let mut topo = Topology::new();
688        let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
689
690        let result = solid_to_solid_distance(&topo, a, a).unwrap();
691        let tol = Tolerance::loose();
692        assert!(
693            tol.approx_eq(result.distance, 0.0),
694            "distance to self should be 0, got {}",
695            result.distance
696        );
697    }
698
699    #[test]
700    fn point_to_sphere_distance() {
701        let sphere =
702            brepkit_math::surfaces::SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 5.0).unwrap();
703        let (dist, closest) = point_to_sphere(Point3::new(10.0, 0.0, 0.0), &sphere);
704        let tol = Tolerance::loose();
705        assert!(
706            tol.approx_eq(dist, 5.0),
707            "distance to sphere should be ~5.0, got {dist}"
708        );
709        assert!(
710            tol.approx_eq(closest.x(), 5.0),
711            "closest x should be ~5.0, got {}",
712            closest.x()
713        );
714    }
715
716    #[test]
717    fn point_to_cylinder_distance() {
718        let cyl = brepkit_math::surfaces::CylindricalSurface::new(
719            Point3::new(0.0, 0.0, 0.0),
720            Vec3::new(0.0, 0.0, 1.0),
721            3.0,
722        )
723        .unwrap();
724        let (dist, _closest) = point_to_cylinder(Point3::new(5.0, 0.0, 1.0), &cyl);
725        let tol = Tolerance::loose();
726        assert!(
727            tol.approx_eq(dist, 2.0),
728            "distance to cylinder should be ~2.0, got {dist}"
729        );
730    }
731
732    #[test]
733    fn segment_to_segment_parallel() {
734        let (dist, _, _) = segment_to_segment_distance(
735            Point3::new(0.0, 0.0, 0.0),
736            Point3::new(1.0, 0.0, 0.0),
737            Point3::new(0.0, 3.0, 0.0),
738            Point3::new(1.0, 3.0, 0.0),
739        );
740        let tol = Tolerance::loose();
741        assert!(
742            tol.approx_eq(dist, 3.0),
743            "parallel segments 3 apart should have distance ~3.0, got {dist}"
744        );
745    }
746
747    #[test]
748    fn segment_to_segment_crossing() {
749        let (dist, _, _) = segment_to_segment_distance(
750            Point3::new(0.0, 0.0, 0.0),
751            Point3::new(1.0, 0.0, 0.0),
752            Point3::new(0.5, 0.0, -1.0),
753            Point3::new(0.5, 0.0, 1.0),
754        );
755        let tol = Tolerance::loose();
756        assert!(
757            tol.approx_eq(dist, 0.0),
758            "crossing segments should have distance ~0, got {dist}"
759        );
760    }
761}