1#![allow(
4 clippy::many_single_char_names,
5 clippy::similar_names,
6 clippy::suboptimal_flops
7)]
8
9pub(crate) mod analytic;
10pub(crate) mod edge;
11
12use std::collections::HashSet;
13
14use brepkit_math::aabb::Aabb3;
15use brepkit_math::bvh::Bvh;
16use brepkit_math::vec::{Point3, Vec3};
17use brepkit_topology::Topology;
18use brepkit_topology::face::{FaceId, FaceSurface};
19use brepkit_topology::solid::SolidId;
20
21use crate::CheckError;
22
23#[derive(Debug, Clone, Copy)]
25pub enum SupportElement {
26 Face(FaceId, f64, f64),
28}
29
30#[derive(Debug, Clone)]
32pub struct DistanceResult {
33 pub distance: f64,
35 pub point_a: Point3,
37 pub point_b: Point3,
39}
40
41#[allow(clippy::too_many_lines)]
50pub fn point_to_solid(
51 topo: &Topology,
52 point: Point3,
53 solid: SolidId,
54) -> Result<DistanceResult, CheckError> {
55 let face_ids = collect_solid_faces(topo, solid)?;
56
57 let mut face_aabbs: Vec<(usize, Aabb3)> = Vec::with_capacity(face_ids.len());
58 for (i, &fid) in face_ids.iter().enumerate() {
59 let aabb = crate::util::face_aabb(topo, fid)?;
60 face_aabbs.push((i, aabb));
61 }
62 let bvh = Bvh::build(&face_aabbs);
63
64 let mut best_dist = f64::INFINITY;
65 let mut best_point = point;
66
67 let mut candidates: Vec<usize> = (0..face_aabbs.len()).collect();
69 candidates.sort_by(|&a, &b| {
70 let da = face_aabbs[a].1.distance_squared_to_point(point);
71 let db = face_aabbs[b].1.distance_squared_to_point(point);
72 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
73 });
74
75 if let Some(closest_idx) = bvh.query_closest(point)
76 && let Some(pos) = candidates
77 .iter()
78 .position(|&i| face_aabbs[i].0 == closest_idx)
79 {
80 candidates.swap(0, pos);
81 }
82
83 for idx in candidates {
84 let aabb_dist_sq = face_aabbs[idx].1.distance_squared_to_point(point);
85 if aabb_dist_sq > best_dist * best_dist {
86 continue; }
88 let face_idx = face_aabbs[idx].0;
89 let fid = face_ids[face_idx];
90 if let Ok(Some((dist, closest))) = point_to_face(topo, point, fid)
91 && dist < best_dist
92 {
93 best_dist = dist;
94 best_point = closest;
95 }
96 }
97
98 Ok(DistanceResult {
99 distance: best_dist,
100 point_a: point,
101 point_b: best_point,
102 })
103}
104
105pub fn point_to_face(
111 topo: &Topology,
112 point: Point3,
113 face_id: FaceId,
114) -> Result<Option<(f64, Point3)>, CheckError> {
115 let face = topo.face(face_id)?;
116 match face.surface() {
117 FaceSurface::Plane { normal, d } => {
118 let polygon = crate::util::face_polygon(topo, face_id)?;
119 Ok(point_to_polygon_distance(point, &polygon, *normal, *d))
120 }
121 FaceSurface::Cylinder(cyl) => {
122 let (dist, closest) = analytic::point_to_cylinder(point, cyl);
123 if is_point_in_face_boundary(topo, face_id, closest)? {
124 Ok(Some((dist, closest)))
125 } else {
126 Ok(closest_point_on_wire_edges(topo, face_id, point)?)
127 }
128 }
129 FaceSurface::Cone(cone) => {
130 let (dist, closest) = analytic::point_to_cone(point, cone);
131 if is_point_in_face_boundary(topo, face_id, closest)? {
132 Ok(Some((dist, closest)))
133 } else {
134 Ok(closest_point_on_wire_edges(topo, face_id, point)?)
135 }
136 }
137 FaceSurface::Sphere(sph) => {
138 let (dist, closest) = analytic::point_to_sphere(point, sph);
139 if is_point_in_face_boundary(topo, face_id, closest)? {
140 Ok(Some((dist, closest)))
141 } else {
142 Ok(closest_point_on_wire_edges(topo, face_id, point)?)
143 }
144 }
145 FaceSurface::Torus(tor) => {
146 let (dist, closest) = analytic::point_to_torus(point, tor);
147 if is_point_in_face_boundary(topo, face_id, closest)? {
148 Ok(Some((dist, closest)))
149 } else {
150 Ok(closest_point_on_wire_edges(topo, face_id, point)?)
151 }
152 }
153 FaceSurface::Nurbs(nurbs) => {
154 match brepkit_math::nurbs::projection::project_point_to_surface(nurbs, point, 1e-7) {
155 Ok(proj) => {
156 if is_point_in_face_boundary(topo, face_id, proj.point)? {
157 Ok(Some((proj.distance, proj.point)))
158 } else {
159 closest_point_on_wire_edges(topo, face_id, point)
160 }
161 }
162 Err(_) => Ok(None),
163 }
164 }
165 }
166}
167
168#[allow(clippy::too_many_lines)]
177pub fn solid_to_solid(
178 topo: &Topology,
179 solid_a: SolidId,
180 solid_b: SolidId,
181) -> Result<DistanceResult, CheckError> {
182 let verts_a = collect_solid_vertices(topo, solid_a)?;
183 let verts_b = collect_solid_vertices(topo, solid_b)?;
184
185 let mut best_dist = f64::INFINITY;
186 let mut best_a = Point3::new(0.0, 0.0, 0.0);
187 let mut best_b = Point3::new(0.0, 0.0, 0.0);
188
189 for &pa in &verts_a {
191 for &pb in &verts_b {
192 let dist = (pa - pb).length();
193 if dist < best_dist {
194 best_dist = dist;
195 best_a = pa;
196 best_b = pb;
197 }
198 }
199 }
200
201 let faces_b = collect_solid_faces(topo, solid_b)?;
203 let mut aabbs_b: Vec<(usize, Aabb3)> = Vec::with_capacity(faces_b.len());
204 for (i, &fid) in faces_b.iter().enumerate() {
205 let aabb = crate::util::face_aabb(topo, fid)?;
206 aabbs_b.push((i, aabb));
207 }
208
209 for &pa in &verts_a {
210 for &(idx, ref aabb) in &aabbs_b {
211 if aabb.distance_squared_to_point(pa) > best_dist * best_dist {
212 continue;
213 }
214 if let Ok(Some((dist, closest))) = point_to_face(topo, pa, faces_b[idx])
215 && dist < best_dist
216 {
217 best_dist = dist;
218 best_a = pa;
219 best_b = closest;
220 }
221 }
222 }
223
224 let faces_a = collect_solid_faces(topo, solid_a)?;
226 let mut aabbs_a: Vec<(usize, Aabb3)> = Vec::with_capacity(faces_a.len());
227 for (i, &fid) in faces_a.iter().enumerate() {
228 let aabb = crate::util::face_aabb(topo, fid)?;
229 aabbs_a.push((i, aabb));
230 }
231
232 for &pb in &verts_b {
233 for &(idx, ref aabb) in &aabbs_a {
234 if aabb.distance_squared_to_point(pb) > best_dist * best_dist {
235 continue;
236 }
237 if let Ok(Some((dist, closest))) = point_to_face(topo, pb, faces_a[idx])
238 && dist < best_dist
239 {
240 best_dist = dist;
241 best_b = pb;
242 best_a = closest;
243 }
244 }
245 }
246
247 let edges_a = collect_solid_edge_segments(topo, solid_a)?;
249 let edges_b = collect_solid_edge_segments(topo, solid_b)?;
250
251 for &(p0a, p1a) in &edges_a {
252 let aabb_a = Aabb3::try_from_points([p0a, p1a].iter().copied())
253 .unwrap_or(Aabb3 { min: p0a, max: p0a });
254 for &(p0b, p1b) in &edges_b {
255 let aabb_b = Aabb3::try_from_points([p0b, p1b].iter().copied())
256 .unwrap_or(Aabb3 { min: p0b, max: p0b });
257 if aabb_distance(&aabb_a, &aabb_b) > best_dist {
258 continue;
259 }
260 let (dist, ca, cb) = edge::segment_segment_distance(p0a, p1a, p0b, p1b);
261 if dist < best_dist {
262 best_dist = dist;
263 best_a = ca;
264 best_b = cb;
265 }
266 }
267 }
268
269 Ok(DistanceResult {
270 distance: best_dist,
271 point_a: best_a,
272 point_b: best_b,
273 })
274}
275
276fn collect_solid_vertices(topo: &Topology, solid: SolidId) -> Result<Vec<Point3>, CheckError> {
278 let solid_data = topo.solid(solid)?;
279 let mut seen = HashSet::new();
280 let mut points = Vec::new();
281 let shell_ids: Vec<_> = std::iter::once(solid_data.outer_shell())
282 .chain(solid_data.inner_shells().iter().copied())
283 .collect();
284 for sid in shell_ids {
285 let shell = topo.shell(sid)?;
286 for &fid in shell.faces() {
287 let face = topo.face(fid)?;
288 let mut wire_ids = vec![face.outer_wire()];
289 wire_ids.extend(face.inner_wires().iter().copied());
290 for wid in wire_ids {
291 let wire = topo.wire(wid)?;
292 for oe in wire.edges() {
293 let edge_data = topo.edge(oe.edge())?;
294 for vid in [edge_data.start(), edge_data.end()] {
295 if seen.insert(vid) {
296 points.push(topo.vertex(vid)?.point());
297 }
298 }
299 }
300 }
301 }
302 }
303 Ok(points)
304}
305
306fn collect_solid_faces(topo: &Topology, solid: SolidId) -> Result<Vec<FaceId>, CheckError> {
308 let solid_data = topo.solid(solid)?;
309 let mut faces = Vec::new();
310 let shell_ids: Vec<_> = std::iter::once(solid_data.outer_shell())
311 .chain(solid_data.inner_shells().iter().copied())
312 .collect();
313 for sid in shell_ids {
314 let shell = topo.shell(sid)?;
315 faces.extend(shell.faces().iter().copied());
316 }
317 Ok(faces)
318}
319
320#[allow(clippy::cast_precision_loss)]
325fn collect_solid_edge_segments(
326 topo: &Topology,
327 solid: SolidId,
328) -> Result<Vec<(Point3, Point3)>, CheckError> {
329 use brepkit_topology::edge::EdgeCurve;
330
331 let solid_data = topo.solid(solid)?;
332 let mut seen = HashSet::new();
333 let mut segments = Vec::new();
334
335 let n_samples = 8usize;
336
337 let shell_ids: Vec<_> = std::iter::once(solid_data.outer_shell())
338 .chain(solid_data.inner_shells().iter().copied())
339 .collect();
340 for sid in shell_ids {
341 let shell = topo.shell(sid)?;
342 for &fid in shell.faces() {
343 let face = topo.face(fid)?;
344 let mut wire_ids = vec![face.outer_wire()];
345 wire_ids.extend(face.inner_wires().iter().copied());
346 for wid in wire_ids {
347 let wire = topo.wire(wid)?;
348 for oe in wire.edges() {
349 let eid = oe.edge();
350 if !seen.insert(eid) {
351 continue;
352 }
353 let edge_data = topo.edge(eid)?;
354 let start_pt = topo.vertex(edge_data.start())?.point();
355 let end_pt = topo.vertex(edge_data.end())?.point();
356
357 match edge_data.curve() {
358 EdgeCurve::Line => {
359 segments.push((start_pt, end_pt));
360 }
361 EdgeCurve::Circle(c) => {
362 let is_closed = edge_data.start() == edge_data.end();
363 let (t0, t1) = if is_closed {
364 (0.0, std::f64::consts::TAU)
365 } else {
366 let t0 = c.project(start_pt);
367 let mut t1 = c.project(end_pt);
368 if t1 <= t0 {
369 t1 += std::f64::consts::TAU;
370 }
371 (t0, t1)
372 };
373 let mut prev = c.evaluate(t0);
374 for i in 1..=n_samples {
375 let t = t0 + (t1 - t0) * (i as f64) / (n_samples as f64);
376 let curr = c.evaluate(t);
377 segments.push((prev, curr));
378 prev = curr;
379 }
380 }
381 EdgeCurve::Ellipse(e) => {
382 let is_closed = edge_data.start() == edge_data.end();
383 let (t0, t1) = if is_closed {
384 (0.0, std::f64::consts::TAU)
385 } else {
386 let t0 = e.project(start_pt);
387 let mut t1 = e.project(end_pt);
388 if t1 <= t0 {
389 t1 += std::f64::consts::TAU;
390 }
391 (t0, t1)
392 };
393 let mut prev = e.evaluate(t0);
394 for i in 1..=n_samples {
395 let t = t0 + (t1 - t0) * (i as f64) / (n_samples as f64);
396 let curr = e.evaluate(t);
397 segments.push((prev, curr));
398 prev = curr;
399 }
400 }
401 EdgeCurve::NurbsCurve(nc) => {
402 let (t0, t1) = nc.domain();
403 let mut prev = nc.evaluate(t0);
404 for i in 1..=n_samples {
405 let t = t0 + (t1 - t0) * (i as f64) / (n_samples as f64);
406 let curr = nc.evaluate(t);
407 segments.push((prev, curr));
408 prev = curr;
409 }
410 }
411 }
412 }
413 }
414 }
415 }
416
417 Ok(segments)
418}
419
420fn is_point_in_face_boundary(
422 topo: &Topology,
423 face_id: FaceId,
424 point: Point3,
425) -> Result<bool, CheckError> {
426 let polygon = crate::util::face_polygon(topo, face_id)?;
427 if polygon.len() < 3 {
428 return Ok(true); }
430 let normal = crate::util::polygon_normal(&polygon);
431 Ok(crate::util::point_in_polygon_3d(&point, &polygon, &normal))
432}
433
434fn closest_point_on_wire_edges(
438 topo: &Topology,
439 face_id: FaceId,
440 point: Point3,
441) -> Result<Option<(f64, Point3)>, CheckError> {
442 let face = topo.face(face_id)?;
443 let mut best_dist = f64::INFINITY;
444 let mut best_pt = point;
445
446 let mut wire_ids = vec![face.outer_wire()];
447 wire_ids.extend(face.inner_wires().iter().copied());
448
449 for wid in wire_ids {
450 let wire = topo.wire(wid)?;
451 for oe in wire.edges() {
452 let edge_data = topo.edge(oe.edge())?;
453 let p0 = topo.vertex(edge_data.start())?.point();
454 let p1 = topo.vertex(edge_data.end())?.point();
455 let (dist, closest) = point_to_segment(point, p0, p1);
456 if dist < best_dist {
457 best_dist = dist;
458 best_pt = closest;
459 }
460 }
461 }
462 if best_dist < f64::INFINITY {
463 Ok(Some((best_dist, best_pt)))
464 } else {
465 Ok(None)
466 }
467}
468
469fn point_to_polygon_distance(
474 point: Point3,
475 polygon: &[Point3],
476 normal: Vec3,
477 d: f64,
478) -> Option<(f64, Point3)> {
479 if polygon.len() < 3 {
480 return None;
481 }
482
483 let (_, projected) = analytic::point_to_plane(point, normal, d);
484
485 if crate::util::point_in_polygon_3d(&projected, polygon, &normal) {
486 let dist = (point - projected).length();
487 return Some((dist, projected));
488 }
489
490 let mut best_dist = f64::INFINITY;
491 let mut best_pt = polygon[0];
492 let n = polygon.len();
493 for i in 0..n {
494 let j = (i + 1) % n;
495 let (dist, closest) = point_to_segment(point, polygon[i], polygon[j]);
496 if dist < best_dist {
497 best_dist = dist;
498 best_pt = closest;
499 }
500 }
501 Some((best_dist, best_pt))
502}
503
504fn point_to_segment(point: Point3, a: Point3, b: Point3) -> (f64, Point3) {
506 let ab = b - a;
507 let ap = point - a;
508 let len_sq = ab.length_squared();
509 if len_sq < 1e-30 {
510 return ((point - a).length(), a);
511 }
512 let t = (ap.dot(ab) / len_sq).clamp(0.0, 1.0);
513 let closest = Point3::new(
514 ab.x().mul_add(t, a.x()),
515 ab.y().mul_add(t, a.y()),
516 ab.z().mul_add(t, a.z()),
517 );
518 ((point - closest).length(), closest)
519}
520
521fn aabb_distance(a: &Aabb3, b: &Aabb3) -> f64 {
523 let dx = (a.min.x() - b.max.x()).max(b.min.x() - a.max.x()).max(0.0);
524 let dy = (a.min.y() - b.max.y()).max(b.min.y() - a.max.y()).max(0.0);
525 let dz = (a.min.z() - b.max.z()).max(b.min.z() - a.max.z()).max(0.0);
526 (dx.mul_add(dx, dy.mul_add(dy, dz * dz))).sqrt()
527}
528
529#[cfg(test)]
530mod tests {
531 #![allow(clippy::unwrap_used, clippy::expect_used)]
532
533 use super::*;
534 use brepkit_math::surfaces::{CylindricalSurface, SphericalSurface, ToroidalSurface};
535
536 #[test]
537 fn point_to_sphere_outside() {
538 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
539 let (dist, closest) = analytic::point_to_sphere(Point3::new(3.0, 0.0, 0.0), &sphere);
540 assert!(
541 (dist - 2.0).abs() < 1e-10,
542 "distance should be 2.0, got {dist}"
543 );
544 assert!((closest.x() - 1.0).abs() < 1e-10);
545 assert!(closest.y().abs() < 1e-10);
546 assert!(closest.z().abs() < 1e-10);
547 }
548
549 #[test]
550 fn point_to_sphere_inside() {
551 let sphere = SphericalSurface::new(Point3::new(0.0, 0.0, 0.0), 1.0).unwrap();
552 let (dist, closest) = analytic::point_to_sphere(Point3::new(0.5, 0.0, 0.0), &sphere);
553 assert!(
554 (dist - 0.5).abs() < 1e-10,
555 "distance should be 0.5, got {dist}"
556 );
557 assert!((closest.x() - 1.0).abs() < 1e-10);
558 }
559
560 #[test]
561 fn point_to_cylinder_outside() {
562 let cyl =
563 CylindricalSurface::new(Point3::new(0.0, 0.0, 0.0), Vec3::new(0.0, 0.0, 1.0), 1.0)
564 .unwrap();
565 let (dist, closest) = analytic::point_to_cylinder(Point3::new(2.0, 0.0, 0.0), &cyl);
566 assert!(
567 (dist - 1.0).abs() < 1e-10,
568 "distance should be 1.0, got {dist}"
569 );
570 assert!((closest.x() - 1.0).abs() < 1e-10);
571 assert!(closest.y().abs() < 1e-10);
572 assert!(closest.z().abs() < 1e-10);
573 }
574
575 #[test]
576 fn point_to_plane_above() {
577 let normal = Vec3::new(0.0, 0.0, 1.0);
578 let d = 0.0;
579 let (dist, closest) = analytic::point_to_plane(Point3::new(0.0, 0.0, 5.0), normal, d);
580 assert!(
581 (dist - 5.0).abs() < 1e-10,
582 "distance should be 5.0, got {dist}"
583 );
584 assert!(closest.x().abs() < 1e-10);
585 assert!(closest.y().abs() < 1e-10);
586 assert!(closest.z().abs() < 1e-10);
587 }
588
589 #[test]
590 fn segment_segment_parallel() {
591 let (dist, _, _) = edge::segment_segment_distance(
593 Point3::new(0.0, 0.0, 0.0),
594 Point3::new(1.0, 0.0, 0.0),
595 Point3::new(0.0, 2.0, 0.0),
596 Point3::new(1.0, 2.0, 0.0),
597 );
598 assert!(
599 (dist - 2.0).abs() < 1e-10,
600 "parallel segment distance should be 2.0, got {dist}"
601 );
602 }
603
604 #[test]
605 fn segment_segment_crossing() {
606 let (dist, _, _) = edge::segment_segment_distance(
608 Point3::new(-1.0, 0.0, 0.0),
609 Point3::new(1.0, 0.0, 0.0),
610 Point3::new(0.0, -1.0, 0.0),
611 Point3::new(0.0, 1.0, 0.0),
612 );
613 assert!(
614 dist < 1e-10,
615 "crossing segment distance should be ~0, got {dist}"
616 );
617 }
618
619 #[test]
620 fn segment_segment_skew() {
621 let (dist, ca, cb) = edge::segment_segment_distance(
623 Point3::new(0.0, 0.0, 0.0),
624 Point3::new(1.0, 0.0, 0.0),
625 Point3::new(0.0, 0.0, 3.0),
626 Point3::new(0.0, 1.0, 3.0),
627 );
628 assert!(
629 (dist - 3.0).abs() < 1e-10,
630 "skew segment distance should be 3.0, got {dist}"
631 );
632 assert!(ca.z().abs() < 1e-10);
633 assert!((cb.z() - 3.0).abs() < 1e-10);
634 }
635
636 #[test]
637 fn solid_to_solid_separated() {
638 use brepkit_topology::test_utils::make_unit_cube_manifold_at;
639 let mut topo = Topology::new();
640 let a = make_unit_cube_manifold_at(&mut topo, 0.0, 0.0, 0.0);
642 let b = make_unit_cube_manifold_at(&mut topo, 3.0, 0.0, 0.0);
643 let result = solid_to_solid(&topo, a, b).unwrap();
644 assert!(
645 (result.distance - 2.0).abs() < 1e-10,
646 "distance should be 2.0, got {}",
647 result.distance
648 );
649 }
650
651 #[test]
652 fn point_to_torus_outside() {
653 let torus = ToroidalSurface::new(Point3::new(0.0, 0.0, 0.0), 3.0, 1.0).unwrap();
655 let (dist, closest) = analytic::point_to_torus(Point3::new(6.0, 0.0, 0.0), &torus);
657 assert!(
658 (dist - 2.0).abs() < 1e-10,
659 "distance should be 2.0, got {dist}"
660 );
661 assert!((closest.x() - 4.0).abs() < 1e-10);
662 assert!(closest.y().abs() < 1e-10);
663 assert!(closest.z().abs() < 1e-10);
664 }
665}