1#![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#[derive(Debug, Clone)]
38pub struct DistanceResult {
39 pub distance: f64,
41 pub point_a: Point3,
43 pub point_b: Point3,
45}
46
47#[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#[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 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 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 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
203pub 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 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#[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 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 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 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
332fn 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
345pub(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
374fn 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
385fn 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
391fn 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
400fn 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
406fn 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 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
436fn bvh_distance_candidates(bvh: &Bvh, aabbs: &[(usize, Aabb3)], point: Point3) -> Vec<usize> {
438 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
454fn 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
468fn 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
495fn 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
538pub(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
567fn 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
586fn 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 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 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 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}