1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
34use ogeom_geom::{Curve, SurfaceGeometry, Transformable, TrimmedCurve};
35use ogeom_math::{Point, Point2, Transform, Vector};
36use ogeom_mesh::{Deflection, face_boundary, inside_boundary};
37use ogeom_topo::{EdgeRepr, Model, NodeData, Shape, ShapeType, explore_unique};
38
39use crate::classify::{fold_toward_rings, parametric_band};
40use crate::measure::{project_on_curve, project_on_surface};
41use ogeom_intersect::ExtremaOptions;
42
43#[derive(Debug, Clone)]
45pub struct ClosestPair {
46 pub point_a: Point,
48 pub point_b: Point,
50 pub support_a: Shape,
52 pub support_b: Shape,
54}
55
56#[derive(Debug, Clone)]
58pub struct ShapeDistance {
59 pub distance: f64,
61 pub pairs: Vec<ClosestPair>,
65}
66
67pub(crate) enum Element {
69 Vertex(Shape, Point),
70 Edge(Shape, Box<Curve>),
71 Face(Shape, Box<Prepared>),
72}
73
74pub(crate) struct Prepared {
79 pub(crate) world: SurfaceGeometry,
80 local: SurfaceGeometry,
81 to_local: Transform,
82 rings: Vec<Vec<Point2>>,
83 placement: Transform,
85 span: Option<((f64, f64), (f64, f64))>,
87}
88
89pub fn distance_between_shapes(
98 model: &Model,
99 a: &Shape,
100 b: &Shape,
101 options: ExtremaOptions,
102 tol: Tolerances,
103) -> OgeomResult<ShapeDistance> {
104 let ea = elements(model, a, tol)?;
105 let eb = elements(model, b, tol)?;
106 distance_between_elements(&ea, &eb, options, tol)
107}
108
109pub struct PointDistance(Vec<Element>);
111
112impl core::fmt::Debug for PointDistance {
113 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
114 f.debug_struct("PointDistance")
115 .field("elements", &self.0.len())
116 .finish()
117 }
118}
119
120impl PointDistance {
121 pub fn of(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Self> {
128 Ok(Self(elements(model, shape, tol)?))
129 }
130
131 pub(crate) fn elements(&self) -> &[Element] {
133 &self.0
134 }
135
136 pub fn nearest(&self, p: Point, tol: Tolerances) -> OgeomResult<(f64, Point)> {
148 let mut best: Option<(f64, Point)> = None;
149 let mut keep = |d: f64, q: Point| {
150 if d.is_finite() && best.is_none_or(|(held, _)| d < held) {
151 best = Some((d, q));
152 }
153 };
154 for element in &self.0 {
155 match element {
156 Element::Vertex(_, q) => keep(p.distance(*q), *q),
157 Element::Edge(_, curve) => {
158 let foot = project_on_curve(curve, p, 64, tol)?;
159 keep(foot.distance, foot.point);
160 }
161 Element::Face(_, face) => {
162 let foot = project_on_surface(&face.world, p, 32, tol)?;
163 if inside_trim(face, foot.point, tol)? {
164 keep(foot.distance, foot.point);
165 } else if !matches!(face.local, SurfaceGeometry::Plane(_)) {
166 for (d, q) in feet_inside(face, p, tol)? {
167 keep(d, q);
168 }
169 }
170 }
171 }
172 }
173 best.ok_or_else(|| {
174 ogeom_core::ogeom_err!(Construction, "a shape with no elements has no distance")
175 })
176 }
177}
178
179const LOCAL_SEEDS: u32 = 8;
182
183fn feet_inside(face: &Prepared, p: Point, tol: Tolerances) -> OgeomResult<Vec<(f64, Point)>> {
193 use ogeom_geom::Surface as _;
194 let ((u0, u1), (v0, v1)) = face.world.domain();
195 if ![u0, u1, v0, v1].iter().all(|x| x.is_finite()) {
196 return Ok(Vec::new());
197 }
198 let mut found = Vec::new();
199 for i in 0..LOCAL_SEEDS {
200 for j in 0..LOCAL_SEEDS {
201 let at = |k: u32, lo: f64, hi: f64| {
202 lo + (hi - lo) * (f64::from(k) + 0.5) / f64::from(LOCAL_SEEDS)
203 };
204 let (u, v) = (at(i, u0, u1), at(j, v0, v1));
205 if !inside_trim(face, face.world.point_at(u, v, tol)?, tol)? {
206 continue;
207 }
208 let foot = ogeom_geom::project_on_surface_from(&face.world, p, (u, v), tol)?;
209 if inside_trim(face, foot.point, tol)? {
210 found.push((foot.distance, foot.point));
211 }
212 }
213 }
214 Ok(found)
215}
216
217pub(crate) struct Elements(Vec<Element>);
220
221impl Elements {
222 pub(crate) fn of(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Self> {
223 Ok(Self(elements(model, shape, tol)?))
224 }
225
226 pub(crate) fn apart_by_more_than(&self, other: &Self, gap: f64, tol: Tolerances) -> bool {
243 let has_vertex = |e: &Self| e.0.iter().any(|e| matches!(e, Element::Vertex(..)));
246 if !has_vertex(self) || !has_vertex(other) {
247 return false;
248 }
249 let mut arena = Arena::default();
250 let (Some(mine), Some(theirs)) = (arena.roots(&self.0, tol), arena.roots(&other.0, tol))
251 else {
252 return false;
253 };
254 let mut stack: Vec<(usize, usize)> = mine
255 .iter()
256 .flat_map(|&i| theirs.iter().map(move |&j| (i, j)))
257 .collect();
258 let mut visits = 0_usize;
259 while let Some((i, j)) = stack.pop() {
260 visits += 1;
261 if visits > COVER_VISITS {
262 return false;
263 }
264 if arena.nodes[i].apart_from(&arena.nodes[j], gap) {
265 continue;
266 }
267 let (big, small) = if arena.nodes[i].size >= arena.nodes[j].size {
268 (i, j)
269 } else {
270 (j, i)
271 };
272 let halves = match arena.halves(big, gap, tol) {
273 Some(h) => Some((h, small)),
274 None => arena.halves(small, gap, tol).map(|h| (h, big)),
275 };
276 let Some(([a, b], with)) = halves else {
277 return false;
278 };
279 stack.push((a, with));
280 stack.push((b, with));
281 }
282 true
283 }
284}
285
286const COVER_VISITS: usize = 20_000;
289
290enum Piece<'a> {
292 Fixed(Vec<Point>),
294 Spline(Box<ogeom_geom::BSplineCurve>),
296 Arc(&'a Curve, (f64, f64)),
298 Patch(Box<ogeom_geom::BSplineSurface>, &'a Transform),
300 Flat(
302 &'a ogeom_geom::PlaneSurface,
303 (f64, f64),
304 (f64, f64),
305 &'a Transform,
306 ),
307}
308
309struct Node<'a> {
310 piece: Piece<'a>,
311 hull: Vec<Point>,
313 bound: ogeom_math::Aabb,
314 size: f64,
316 normal: Option<Vector>,
318 chord: Option<Vector>,
320 halves: Option<Option<[usize; 2]>>,
322}
323
324impl Node<'_> {
325 fn apart_from(&self, other: &Self, gap: f64) -> bool {
327 if self.bound.distance_to_box(&other.bound) > gap {
328 return true;
329 }
330 let (Some(here), Some(there)) = (self.bound.centre(), other.bound.centre()) else {
331 return false;
332 };
333 let between = there - here;
334 let square = |v: Vector| {
335 let length = v.magnitude();
336 (length > 0.0 && length.is_finite()).then(|| v / length)
337 };
338 let across = |chord: Option<Vector>| {
341 let chord = square(chord?)?;
342 square(between - chord * between.dot(chord))
343 };
344 [
345 self.normal.and_then(square),
346 other.normal.and_then(square),
347 square(between),
348 across(self.chord),
349 across(other.chord),
350 ]
351 .into_iter()
352 .flatten()
353 .any(|axis| {
354 let shadow = |hull: &[Point]| {
355 hull.iter()
356 .map(|p| (*p - Point::ORIGIN).dot(axis))
357 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| {
358 (lo.min(d), hi.max(d))
359 })
360 };
361 let (a, b) = (shadow(&self.hull), shadow(&other.hull));
362 b.0 - a.1 > gap || a.0 - b.1 > gap
363 })
364 }
365}
366
367#[derive(Default)]
368struct Arena<'a> {
369 nodes: Vec<Node<'a>>,
370}
371
372impl<'a> Arena<'a> {
373 fn roots(&mut self, elements: &'a [Element], tol: Tolerances) -> Option<Vec<usize>> {
376 let mut out = Vec::with_capacity(elements.len());
377 for element in elements {
378 let piece = match element {
379 Element::Vertex(_, p) => Piece::Fixed(vec![*p]),
380 Element::Edge(_, curve) => curve_piece(curve, tol)?,
381 Element::Face(_, face) => face_piece(face, tol)?,
382 };
383 out.push(self.add(piece, tol)?);
384 }
385 Some(out)
386 }
387
388 fn add(&mut self, piece: Piece<'a>, tol: Tolerances) -> Option<usize> {
389 let (hull, normal, chord) = hull_of(&piece, tol)?;
390 if hull.is_empty()
393 || !hull
394 .iter()
395 .all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite())
396 {
397 return None;
398 }
399 let bound = ogeom_math::Aabb::of_points(&hull);
400 self.nodes.push(Node {
401 piece,
402 size: bound.diagonal(),
403 hull,
404 bound,
405 normal,
406 chord,
407 halves: None,
408 });
409 Some(self.nodes.len() - 1)
410 }
411
412 fn halves(&mut self, i: usize, gap: f64, tol: Tolerances) -> Option<[usize; 2]> {
415 if let Some(known) = self.nodes[i].halves {
416 return known;
417 }
418 let found = if self.nodes[i].size <= gap {
419 None
420 } else {
421 split(&self.nodes[i].piece, tol).and_then(|(a, b)| {
422 let a = self.add(a, tol)?;
423 let b = self.add(b, tol)?;
424 Some([a, b])
425 })
426 };
427 self.nodes[i].halves = Some(found);
428 found
429 }
430}
431
432fn curve_piece(curve: &Curve, tol: Tolerances) -> Option<Piece<'_>> {
434 use ogeom_geom::Curve3d as _;
435 let range = curve.domain();
436 let whole = || {
437 crate::measure::curve_bounds(curve, tol)
438 .ok()
439 .map(|bound| Piece::Fixed(bound.corners()))
440 };
441 match curve {
442 Curve::BSpline(spline) if !spline.is_periodic() => {
443 Some(Piece::Spline(Box::new(spline.clone())))
444 }
445 Curve::Line(_) | Curve::Circle(_) | Curve::Ellipse(_) => Some(Piece::Arc(curve, range)),
446 Curve::Trimmed(trimmed) if !trimmed.is_reversed() => match trimmed.basis() {
447 Curve::BSpline(spline) => spline
448 .segment(range, tol)
449 .map_or_else(|_| whole(), |piece| Some(Piece::Spline(Box::new(piece)))),
450 basis @ (Curve::Line(_) | Curve::Circle(_) | Curve::Ellipse(_)) => {
451 Some(Piece::Arc(basis, range))
452 }
453 _ => whole(),
454 },
455 _ => whole(),
456 }
457}
458
459fn face_piece(face: &Prepared, tol: Tolerances) -> Option<Piece<'_>> {
462 match &face.local {
463 SurfaceGeometry::BSpline(spline) => {
464 let patch = match face.span {
465 Some((u, v)) => spline.segment(u, v, tol).ok()?,
466 None => spline.clone(),
467 };
468 Some(Piece::Patch(Box::new(patch), &face.placement))
469 }
470 SurfaceGeometry::Plane(plane) => {
471 let (u, v) = face.span?;
472 Some(Piece::Flat(plane, u, v, &face.placement))
473 }
474 _ => None,
475 }
476}
477
478type Hull = (Vec<Point>, Option<Vector>, Option<Vector>);
481
482fn hull_of(piece: &Piece<'_>, tol: Tolerances) -> Option<Hull> {
483 use ogeom_geom::{Curve3d as _, Surface as _};
484 let normal = |[a, b, c, d]: [Point; 4]| (d - a).cross(c - b);
486 Some(match piece {
487 Piece::Fixed(points) => (points.clone(), None, None),
488 Piece::Spline(spline) => {
489 let hull: Vec<Point> = spline.control_points().iter().map(|w| w.point()).collect();
490 let chord = *hull.last()? - *hull.first()?;
491 (hull, None, Some(chord))
492 }
493 Piece::Arc(curve, range) => {
494 let (start, end) = (
495 curve.point_at(range.0, tol).ok()?,
496 curve.point_at(range.1, tol).ok()?,
497 );
498 let hull = match curve {
499 Curve::Line(_) => vec![start, end],
500 _ => crate::measure::curve_bounds_over(curve, *range, tol)
501 .ok()?
502 .corners(),
503 };
504 (hull, None, Some(end - start))
505 }
506 Piece::Patch(patch, placement) => {
507 let grid = patch.grid();
508 let (nu, nv) = (grid.u_count(), grid.v_count());
509 let at = |i, j| grid.get(i, j).map(|w| placement.apply(w.point()));
510 let corners = [
511 at(0, 0)?,
512 at(nu - 1, 0)?,
513 at(0, nv - 1)?,
514 at(nu - 1, nv - 1)?,
515 ];
516 let hull = grid
517 .points()
518 .iter()
519 .map(|w| placement.apply(w.point()))
520 .collect();
521 (hull, Some(normal(corners)), None)
522 }
523 Piece::Flat(plane, u, v, placement) => {
524 let mut corners = [Point::ORIGIN; 4];
525 for (corner, (a, b)) in
526 corners
527 .iter_mut()
528 .zip([(u.0, v.0), (u.1, v.0), (u.0, v.1), (u.1, v.1)])
529 {
530 *corner = placement.apply(plane.point_at(a, b, tol).ok()?);
531 }
532 (corners.to_vec(), Some(normal(corners)), None)
533 }
534 })
535}
536
537fn split<'a>(piece: &Piece<'a>, tol: Tolerances) -> Option<(Piece<'a>, Piece<'a>)> {
539 use ogeom_geom::{Curve3d as _, Surface as _};
540 let mid = |(a, b): (f64, f64)| 0.5 * (a + b);
541 match piece {
542 Piece::Fixed(_) => None,
543 Piece::Spline(spline) => {
544 let (a, b) = spline.domain();
545 let m = mid((a, b));
546 Some((
547 Piece::Spline(Box::new(spline.segment((a, m), tol).ok()?)),
548 Piece::Spline(Box::new(spline.segment((m, b), tol).ok()?)),
549 ))
550 }
551 Piece::Arc(curve, (a, b)) => {
552 let m = mid((*a, *b));
553 Some((Piece::Arc(curve, (*a, m)), Piece::Arc(curve, (m, *b))))
554 }
555 Piece::Patch(patch, placement) => {
556 let (u, v) = patch.domain();
557 let grid = patch.grid();
559 let length = |along_u: bool| {
560 let (across, along) = if along_u {
561 (grid.v_count(), grid.u_count())
562 } else {
563 (grid.u_count(), grid.v_count())
564 };
565 let at = |k: usize, o: usize| {
566 if along_u {
567 grid.get(k, o)
568 } else {
569 grid.get(o, k)
570 }
571 };
572 let mut longest = 0.0_f64;
573 for o in 0..across {
574 let mut run = 0.0;
575 for k in 1..along {
576 if let (Some(p), Some(q)) = (at(k - 1, o), at(k, o)) {
577 run += p.point().distance(q.point());
578 }
579 }
580 longest = longest.max(run);
581 }
582 longest
583 };
584 let pieces = if length(true) >= length(false) {
585 let m = mid(u);
586 (
587 patch.segment((u.0, m), v, tol),
588 patch.segment((m, u.1), v, tol),
589 )
590 } else {
591 let m = mid(v);
592 (
593 patch.segment(u, (v.0, m), tol),
594 patch.segment(u, (m, v.1), tol),
595 )
596 };
597 Some((
598 Piece::Patch(Box::new(pieces.0.ok()?), placement),
599 Piece::Patch(Box::new(pieces.1.ok()?), placement),
600 ))
601 }
602 Piece::Flat(plane, u, v, placement) => {
603 if u.1 - u.0 >= v.1 - v.0 {
604 let m = mid(*u);
605 Some((
606 Piece::Flat(plane, (u.0, m), *v, placement),
607 Piece::Flat(plane, (m, u.1), *v, placement),
608 ))
609 } else {
610 let m = mid(*v);
611 Some((
612 Piece::Flat(plane, *u, (v.0, m), placement),
613 Piece::Flat(plane, *u, (m, v.1), placement),
614 ))
615 }
616 }
617 }
618}
619
620pub(crate) fn distance_between_prepared(
622 a: &Elements,
623 b: &Elements,
624 options: ExtremaOptions,
625 tol: Tolerances,
626) -> OgeomResult<ShapeDistance> {
627 distance_between_elements(&a.0, &b.0, options, tol)
628}
629
630fn distance_between_elements(
631 ea: &[Element],
632 eb: &[Element],
633 options: ExtremaOptions,
634 tol: Tolerances,
635) -> OgeomResult<ShapeDistance> {
636 if ea.is_empty() || eb.is_empty() {
637 ogeom_bail!(Construction, "a shape with no elements has no distance");
638 }
639
640 let mut candidates: Vec<(f64, ClosestPair)> = Vec::new();
641 for element_a in ea {
642 for element_b in eb {
643 approach(element_a, element_b, options, tol, &mut candidates)?;
644 }
645 }
646 candidates.retain(|(d, _)| d.is_finite());
649 let Some(least) = candidates.iter().map(|(d, _)| *d).min_by(f64::total_cmp) else {
650 ogeom_bail!(
651 NotDone,
652 "no candidate approach was found between these shapes"
653 );
654 };
655
656 let mut pairs: Vec<ClosestPair> = Vec::new();
657 for (d, pair) in candidates {
658 if d - least > tol.confusion() {
659 continue;
660 }
661 if pairs.iter().any(|known| {
665 known.point_a.distance(pair.point_a) <= tol.confusion() * 1e2
666 && known.point_b.distance(pair.point_b) <= tol.confusion() * 1e2
667 }) {
668 continue;
669 }
670 pairs.push(pair);
671 }
672 Ok(ShapeDistance {
673 distance: least,
674 pairs,
675 })
676}
677
678fn approach(
680 a: &Element,
681 b: &Element,
682 options: ExtremaOptions,
683 tol: Tolerances,
684 out: &mut Vec<(f64, ClosestPair)>,
685) -> OgeomResult<()> {
686 let mut push = |distance: f64, pa: Point, pb: Point, sa: &Shape, sb: &Shape| {
687 out.push((
688 distance,
689 ClosestPair {
690 point_a: pa,
691 point_b: pb,
692 support_a: sa.clone(),
693 support_b: sb.clone(),
694 },
695 ));
696 };
697 match (a, b) {
698 (Element::Vertex(sa, pa), Element::Vertex(sb, pb)) => {
699 push(pa.distance(*pb), *pa, *pb, sa, sb);
700 }
701 (Element::Vertex(sa, pa), Element::Edge(sb, curve)) => {
702 let foot = project_on_curve(curve, *pa, 64, tol)?;
703 push(foot.distance, *pa, foot.point, sa, sb);
704 }
705 (Element::Edge(sa, curve), Element::Vertex(sb, pb)) => {
706 let foot = project_on_curve(curve, *pb, 64, tol)?;
707 push(foot.distance, foot.point, *pb, sa, sb);
708 }
709 (Element::Vertex(sa, pa), Element::Face(sb, face)) => {
710 let foot = project_on_surface(&face.world, *pa, 32, tol)?;
711 if inside_trim(face, foot.point, tol)? {
712 push(foot.distance, *pa, foot.point, sa, sb);
713 }
714 }
715 (Element::Face(sa, face), Element::Vertex(sb, pb)) => {
716 let foot = project_on_surface(&face.world, *pb, 32, tol)?;
717 if inside_trim(face, foot.point, tol)? {
718 push(foot.distance, foot.point, *pb, sa, sb);
719 }
720 }
721 (Element::Edge(sa, ca), Element::Edge(sb, cb)) => {
722 let found = ogeom_intersect::extrema_curve_curve(ca, cb, options, tol)?;
723 for near in &found.approaches {
724 push(near.distance, near.point_a, near.point_b, sa, sb);
725 }
726 }
727 (Element::Edge(sa, curve), Element::Face(sb, face)) => {
728 let found = ogeom_intersect::extrema_curve_surface(curve, &face.world, options, tol)?;
729 for near in &found.approaches {
730 if inside_trim(face, near.point_b, tol)? {
731 push(near.distance, near.point_a, near.point_b, sa, sb);
732 }
733 }
734 }
735 (Element::Face(sa, face), Element::Edge(sb, curve)) => {
736 let found = ogeom_intersect::extrema_curve_surface(curve, &face.world, options, tol)?;
737 for near in &found.approaches {
738 if inside_trim(face, near.point_b, tol)? {
739 push(near.distance, near.point_b, near.point_a, sa, sb);
740 }
741 }
742 }
743 (Element::Face(sa, fa), Element::Face(sb, fb)) => {
744 let found =
745 ogeom_intersect::extrema_surface_surface(&fa.world, &fb.world, options, tol)?;
746 for near in &found.approaches {
747 if inside_trim(fa, near.point_a, tol)? && inside_trim(fb, near.point_b, tol)? {
748 push(near.distance, near.point_a, near.point_b, sa, sb);
749 }
750 }
751 }
752 }
753 Ok(())
754}
755
756fn inside_trim(face: &Prepared, world_point: Point, tol: Tolerances) -> OgeomResult<bool> {
764 Ok(trim_side(face, None, RING_CHORD, world_point, tol)? == Trim::Inside)
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub(crate) enum Trim {
770 Inside,
772 Outside,
774 Band,
776}
777
778pub(crate) fn trim_side(
783 face: &Prepared,
784 rings: Option<&[Vec<Point2>]>,
785 chord: f64,
786 world_point: Point,
787 tol: Tolerances,
788) -> OgeomResult<Trim> {
789 let rings = rings.unwrap_or(&face.rings);
790 let local = face.to_local.apply(world_point);
791 let projection = project_on_surface(&face.local, local, 32, tol)?;
792 let (u, v) = projection.parameters;
793 let at = fold_toward_rings(&face.local, rings, Point2::new(u, v));
794 let band = parametric_band(&face.local, (at.x, at.y), tol.confusion() + chord, tol);
795 if band.meets_rings(rings, at) {
796 return Ok(Trim::Band);
797 }
798 Ok(if inside_boundary(rings, at) {
799 Trim::Inside
800 } else {
801 Trim::Outside
802 })
803}
804
805pub(crate) const RING_CHORD: f64 = 1e-3;
807
808pub(crate) const RING_ANGULAR: f64 = 0.05;
810
811fn elements(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Vec<Element>> {
813 let mut out = Vec::new();
814 for vertex in explore_unique(model, shape, ShapeType::Vertex)? {
815 let Some(node) = model.node(&vertex) else {
816 ogeom_bail!(Dangling, "vertex is not in this model");
817 };
818 let Some(data) = node.data().as_vertex() else {
819 ogeom_bail!(Construction, "vertex node holds no vertex data");
820 };
821 let placed = vertex.transform(model.datums())?.apply(data.point);
822 out.push(Element::Vertex(vertex, placed));
823 }
824 for edge in explore_unique(model, shape, ShapeType::Edge)? {
825 let Some(node) = model.node(&edge) else {
826 ogeom_bail!(Dangling, "edge is not in this model");
827 };
828 let NodeData::Edge(data) = node.data() else {
829 ogeom_bail!(Construction, "edge node holds no edge data");
830 };
831 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
832 continue;
835 };
836 let Some(geometry) = model.geometry().curve(*curve) else {
837 ogeom_bail!(Dangling, "curve is not in this model");
838 };
839 let placement = edge.transform(model.datums())?;
840 let trimmed: Curve = if (range.0, range.1) == {
841 use ogeom_geom::Curve3d as _;
842 geometry.domain()
843 } {
844 geometry.clone()
845 } else {
846 TrimmedCurve::new(geometry.clone(), range.0, range.1, tol)?.into()
847 };
848 out.push(Element::Edge(
849 edge,
850 Box::new(trimmed.transformed(&placement, tol)?),
851 ));
852 }
853 let ring_deflection = Deflection {
854 chord: RING_CHORD,
855 angular: RING_ANGULAR,
856 ..Deflection::default()
857 };
858 for face in explore_unique(model, shape, ShapeType::Face)? {
859 let Some(node) = model.node(&face) else {
860 ogeom_bail!(Dangling, "face is not in this model");
861 };
862 let NodeData::Face(data) = node.data() else {
863 ogeom_bail!(Construction, "face node holds no face data");
864 };
865 let Some(surface) = model.geometry().surface(data.surface) else {
866 ogeom_bail!(Dangling, "face refers to a surface not in this model");
867 };
868 let placement = face.transform(model.datums())?;
869 let rings = face_boundary(model, &face, ring_deflection, tol)?;
870 let span = ring_span(surface, &rings, tol);
876 let restricted = match span {
877 Some((u, v)) => ogeom_geom::TrimmedSurface::new(surface.clone(), u, v, tol)?.into(),
878 None => surface.clone(),
879 };
880 out.push(Element::Face(
881 face,
882 Box::new(Prepared {
883 world: restricted.transformed(&placement, tol)?,
884 local: surface.clone(),
885 to_local: placement.inverse()?,
886 rings,
887 placement,
888 span,
889 }),
890 ));
891 }
892 Ok(out)
893}
894
895fn ring_span(
905 surface: &SurfaceGeometry,
906 rings: &[Vec<Point2>],
907 tol: Tolerances,
908) -> Option<((f64, f64), (f64, f64))> {
909 use ogeom_geom::Surface as _;
910 let ((ua, ub), (va, vb)) = surface.domain();
911 let mut u = (f64::INFINITY, f64::NEG_INFINITY);
912 let mut v = (f64::INFINITY, f64::NEG_INFINITY);
913 for ring in rings {
914 for p in ring {
915 u = (u.0.min(p.x), u.1.max(p.x));
916 v = (v.0.min(p.y), v.1.max(p.y));
917 }
918 }
919 if u.0 > u.1 || v.0 > v.1 {
920 return None;
921 }
922 let margin_u = (u.1 - u.0).mul_add(0.05, tol.parametric());
923 let margin_v = (v.1 - v.0).mul_add(0.05, tol.parametric());
924 let window = |range: (f64, f64), margin: f64, domain: (f64, f64), periodic| {
927 let (lo, hi) = (range.0 - margin, range.1 + margin);
928 if periodic {
929 (lo, hi)
930 } else {
931 (lo.max(domain.0), hi.min(domain.1))
932 }
933 };
934 let (lo_u, hi_u) = window(u, margin_u, (ua, ub), surface.is_periodic_u());
935 let (lo_v, hi_v) = window(v, margin_v, (va, vb), surface.is_periodic_v());
936 (lo_u < hi_u && lo_v < hi_v).then_some(((lo_u, hi_u), (lo_v, hi_v)))
937}
938
939#[cfg(test)]
940#[allow(clippy::unwrap_used)]
941mod tests {
942 use super::*;
943 use crate::{make_box, make_cylinder, make_sphere};
944 use ogeom_math::{Direction, Frame, Vector};
945
946 const T: Tolerances = Tolerances::millimetres();
947
948 fn frame_at(origin: Point) -> Frame {
949 Frame::new(origin, Direction::Z, Direction::X, T).unwrap()
950 }
951
952 #[test]
953 fn a_torus_distance_is_independent_of_its_seam_window() {
954 use core::f64::consts::{FRAC_PI_2, PI, TAU};
955 use ogeom_topo::VertexData;
956
957 let (major, minor) = (5.0, 2.0);
961 let u = 0.7_f64;
962 for seam in [-2.0 * TAU, -PI, 0.0, PI, TAU + FRAC_PI_2] {
963 let mut model = Model::new();
964 let face = crate::torus_face_seamed_at(&mut model, Frame::WORLD, major, minor, seam, T)
965 .unwrap();
966 let distance = PointDistance::of(&model, &face, T).unwrap();
967 for v in [
970 -FRAC_PI_2,
971 0.0,
972 FRAC_PI_2,
973 PI,
974 3.0 * FRAC_PI_2,
975 TAU - 0.01,
976 TAU,
977 TAU + 0.01,
978 2.0 * TAU + FRAC_PI_2,
979 ] {
980 for offset in [-0.125_f64, 0.0, 0.125] {
981 let point = |radius: f64| {
982 let reach = major + radius * v.cos();
983 Point::new(reach * u.cos(), reach * u.sin(), radius * v.sin())
984 };
985 let query = point(minor + offset);
986 let exact = point(minor);
987 let (found, foot) = distance.nearest(query, T).unwrap();
988 assert!(
989 (found - offset.abs()).abs() <= T.confusion(),
990 "seam={seam}, v={v}, offset={offset}: {found}"
991 );
992 assert!(
993 foot.distance(exact) <= T.confusion(),
994 "foot: seam={seam}, v={v}, offset={offset}: {foot:?} vs {exact:?}, error={}",
995 foot.distance(exact)
996 );
997 let vertex = model.add_vertex(VertexData::new(query));
998 let found = distance_between_shapes(
999 &model,
1000 &vertex,
1001 &face,
1002 ExtremaOptions::default(),
1003 T,
1004 )
1005 .unwrap();
1006 assert!(
1007 (found.distance - offset.abs()).abs() <= T.confusion(),
1008 "shape distance: seam={seam}, v={v}, offset={offset}: {}",
1009 found.distance
1010 );
1011 assert!(found.pairs.iter().any(|pair| {
1012 pair.point_a.distance(query) <= T.confusion()
1013 && pair.point_b.distance(exact) <= T.confusion()
1014 }));
1015 }
1016 }
1017 }
1018 }
1019
1020 #[test]
1021 fn parallel_box_walls_meet_at_the_gap_between_them() {
1022 let mut model = Model::new();
1023 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
1024 let b = make_box(
1025 &mut model,
1026 frame_at(Point::new(5.0, 0.0, 0.0)),
1027 (2.0, 2.0, 2.0),
1028 T,
1029 )
1030 .unwrap();
1031 let found =
1032 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1033 .unwrap();
1034 assert!((found.distance - 3.0).abs() < 1e-9, "{}", found.distance);
1035 assert!(!found.pairs.is_empty());
1036 for pair in &found.pairs {
1037 assert!((pair.point_a.distance(pair.point_b) - found.distance).abs() < 1e-9);
1038 }
1039 }
1040
1041 #[test]
1042 fn diagonal_boxes_meet_corner_to_corner() {
1043 let mut model = Model::new();
1046 let a = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
1047 let b = make_box(
1048 &mut model,
1049 frame_at(Point::new(3.0, 3.0, 3.0)),
1050 (1.0, 1.0, 1.0),
1051 T,
1052 )
1053 .unwrap();
1054 let found =
1055 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1056 .unwrap();
1057 let exact = (3.0_f64 * 4.0).sqrt(); assert!((found.distance - exact).abs() < 1e-9);
1059 let pair = &found.pairs[0];
1060 assert!(pair.point_a.is_equal(Point::new(1.0, 1.0, 1.0), T));
1061 assert!(pair.point_b.is_equal(Point::new(3.0, 3.0, 3.0), T));
1062 assert_eq!(model.kind_of(&pair.support_a).unwrap(), ShapeType::Vertex);
1063 assert_eq!(model.kind_of(&pair.support_b).unwrap(), ShapeType::Vertex);
1064 }
1065
1066 #[test]
1067 fn a_sphere_over_a_box_measures_to_the_top_face() {
1068 let mut model = Model::new();
1069 let block = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
1070 let ball = make_sphere(&mut model, frame_at(Point::new(2.0, 2.0, 4.0)), 1.0, T).unwrap();
1071 let found = distance_between_shapes(
1072 &model,
1073 &block.shape,
1074 &ball.shape,
1075 ExtremaOptions::default(),
1076 T,
1077 )
1078 .unwrap();
1079 assert!((found.distance - 2.0).abs() < 1e-7, "{}", found.distance);
1080 let pair = &found.pairs[0];
1081 assert!(pair.point_a.is_equal(Point::new(2.0, 2.0, 1.0), T));
1082 assert!(pair.point_b.is_equal(Point::new(2.0, 2.0, 3.0), T));
1083 }
1084
1085 #[test]
1086 fn parallel_cylinders_meet_wall_to_wall() {
1087 let mut model = Model::new();
1090 let a = make_cylinder(&mut model, Frame::WORLD, 1.0, 4.0, T).unwrap();
1091 let b =
1092 make_cylinder(&mut model, frame_at(Point::new(5.0, 0.0, 0.0)), 1.0, 4.0, T).unwrap();
1093 let found =
1094 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1095 .unwrap();
1096 assert!((found.distance - 3.0).abs() < 1e-7, "{}", found.distance);
1097 }
1098
1099 #[test]
1100 fn touching_boxes_report_zero() {
1101 let mut model = Model::new();
1102 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
1103 let b = make_box(
1104 &mut model,
1105 frame_at(Point::new(2.0, 0.0, 0.0)),
1106 (2.0, 2.0, 2.0),
1107 T,
1108 )
1109 .unwrap();
1110 let found =
1111 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1112 .unwrap();
1113 assert!(found.distance < 1e-9, "{}", found.distance);
1114 }
1115
1116 #[test]
1117 fn a_box_inside_a_box_measures_boundary_to_boundary() {
1118 let mut model = Model::new();
1121 let outer = make_box(&mut model, Frame::WORLD, (6.0, 6.0, 6.0), T).unwrap();
1122 let inner = make_box(
1123 &mut model,
1124 frame_at(Point::new(2.0, 2.0, 2.0)),
1125 (2.0, 2.0, 2.0),
1126 T,
1127 )
1128 .unwrap();
1129 let found = distance_between_shapes(
1130 &model,
1131 &outer.shape,
1132 &inner.shape,
1133 ExtremaOptions::default(),
1134 T,
1135 )
1136 .unwrap();
1137 assert!((found.distance - 2.0).abs() < 1e-9, "{}", found.distance);
1138 }
1139
1140 #[test]
1141 fn a_rotated_box_measures_edge_to_edge() {
1142 let mut model = Model::new();
1146 let a = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
1147 let tilted = Frame::new(
1148 Point::new(2.0, 2.0, 3.0),
1149 Direction::new(Vector::new(0.0, 1.0, 1.0), T).unwrap(),
1150 Direction::X,
1151 T,
1152 )
1153 .unwrap();
1154 let b = make_box(&mut model, tilted, (1.0, 1.0, 1.0), T).unwrap();
1155 let found =
1156 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
1157 .unwrap();
1158 let exact = 2.0 - core::f64::consts::FRAC_1_SQRT_2;
1161 assert!((found.distance - exact).abs() < 1e-7, "{}", found.distance);
1162 }
1163}