1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
35use ogeom_geom::{Curve, SurfaceGeometry, Transformable, TrimmedCurve};
36use ogeom_math::{Point, Point2, Transform};
37use ogeom_mesh::{Deflection, face_boundary, inside_boundary};
38use ogeom_topo::{EdgeRepr, Model, NodeData, Shape, ShapeType, explore_unique};
39
40use crate::classify::{distance_to_rings, parametric_band};
41use crate::measure::{project_on_curve, project_on_surface};
42use ogeom_intersect::ExtremaOptions;
43
44#[derive(Debug, Clone)]
46pub struct ClosestPair {
47 pub point_a: Point,
49 pub point_b: Point,
51 pub support_a: Shape,
53 pub support_b: Shape,
55}
56
57#[derive(Debug, Clone)]
59pub struct ShapeDistance {
60 pub distance: f64,
62 pub pairs: Vec<ClosestPair>,
66}
67
68enum Element {
70 Vertex(Shape, Point),
71 Edge(Shape, Box<Curve>),
72 Face(Shape, Box<Prepared>),
73}
74
75struct Prepared {
80 world: SurfaceGeometry,
81 local: SurfaceGeometry,
82 to_local: Transform,
83 rings: Vec<Vec<Point2>>,
84}
85
86pub fn distance_between_shapes(
95 model: &Model,
96 a: &Shape,
97 b: &Shape,
98 options: ExtremaOptions,
99 tol: Tolerances,
100) -> OgeomResult<ShapeDistance> {
101 let ea = elements(model, a, tol)?;
102 let eb = elements(model, b, tol)?;
103 if ea.is_empty() || eb.is_empty() {
104 ogeom_bail!(Construction, "a shape with no elements has no distance");
105 }
106
107 let mut candidates: Vec<(f64, ClosestPair)> = Vec::new();
108 for element_a in &ea {
109 for element_b in &eb {
110 approach(element_a, element_b, options, tol, &mut candidates)?;
111 }
112 }
113 let Some(least) = candidates
114 .iter()
115 .map(|(d, _)| *d)
116 .min_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal))
117 else {
118 ogeom_bail!(
119 NotDone,
120 "no candidate approach was found between these shapes"
121 );
122 };
123
124 let mut pairs: Vec<ClosestPair> = Vec::new();
125 for (d, pair) in candidates {
126 if d - least > tol.confusion() {
127 continue;
128 }
129 if pairs.iter().any(|known| {
133 known.point_a.distance(pair.point_a) <= tol.confusion() * 1e2
134 && known.point_b.distance(pair.point_b) <= tol.confusion() * 1e2
135 }) {
136 continue;
137 }
138 pairs.push(pair);
139 }
140 Ok(ShapeDistance {
141 distance: least,
142 pairs,
143 })
144}
145
146fn approach(
148 a: &Element,
149 b: &Element,
150 options: ExtremaOptions,
151 tol: Tolerances,
152 out: &mut Vec<(f64, ClosestPair)>,
153) -> OgeomResult<()> {
154 let mut push = |distance: f64, pa: Point, pb: Point, sa: &Shape, sb: &Shape| {
155 out.push((
156 distance,
157 ClosestPair {
158 point_a: pa,
159 point_b: pb,
160 support_a: sa.clone(),
161 support_b: sb.clone(),
162 },
163 ));
164 };
165 match (a, b) {
166 (Element::Vertex(sa, pa), Element::Vertex(sb, pb)) => {
167 push(pa.distance(*pb), *pa, *pb, sa, sb);
168 }
169 (Element::Vertex(sa, pa), Element::Edge(sb, curve)) => {
170 let foot = project_on_curve(curve, *pa, 64, tol)?;
171 push(foot.distance, *pa, foot.point, sa, sb);
172 }
173 (Element::Edge(sa, curve), Element::Vertex(sb, pb)) => {
174 let foot = project_on_curve(curve, *pb, 64, tol)?;
175 push(foot.distance, foot.point, *pb, sa, sb);
176 }
177 (Element::Vertex(sa, pa), Element::Face(sb, face)) => {
178 let foot = project_on_surface(&face.world, *pa, 32, tol)?;
179 if inside_trim(face, foot.point, tol)? {
180 push(foot.distance, *pa, foot.point, sa, sb);
181 }
182 }
183 (Element::Face(sa, face), Element::Vertex(sb, pb)) => {
184 let foot = project_on_surface(&face.world, *pb, 32, tol)?;
185 if inside_trim(face, foot.point, tol)? {
186 push(foot.distance, foot.point, *pb, sa, sb);
187 }
188 }
189 (Element::Edge(sa, ca), Element::Edge(sb, cb)) => {
190 let found = ogeom_intersect::extrema_curve_curve(ca, cb, options, tol)?;
191 for near in &found.approaches {
192 push(near.distance, near.point_a, near.point_b, sa, sb);
193 }
194 }
195 (Element::Edge(sa, curve), Element::Face(sb, face)) => {
196 let found = ogeom_intersect::extrema_curve_surface(curve, &face.world, options, tol)?;
197 for near in &found.approaches {
198 if inside_trim(face, near.point_b, tol)? {
199 push(near.distance, near.point_a, near.point_b, sa, sb);
200 }
201 }
202 }
203 (Element::Face(sa, face), Element::Edge(sb, curve)) => {
204 let found = ogeom_intersect::extrema_curve_surface(curve, &face.world, options, tol)?;
205 for near in &found.approaches {
206 if inside_trim(face, near.point_b, tol)? {
207 push(near.distance, near.point_b, near.point_a, sa, sb);
208 }
209 }
210 }
211 (Element::Face(sa, fa), Element::Face(sb, fb)) => {
212 let found =
213 ogeom_intersect::extrema_surface_surface(&fa.world, &fb.world, options, tol)?;
214 for near in &found.approaches {
215 if inside_trim(fa, near.point_a, tol)? && inside_trim(fb, near.point_b, tol)? {
216 push(near.distance, near.point_a, near.point_b, sa, sb);
217 }
218 }
219 }
220 }
221 Ok(())
222}
223
224fn inside_trim(face: &Prepared, world_point: Point, tol: Tolerances) -> OgeomResult<bool> {
232 let local = face.to_local.apply(world_point);
233 let projection = project_on_surface(&face.local, local, 32, tol)?;
234 let (u, v) = projection.parameters;
235 let at = Point2::new(u, v);
236 let band = parametric_band(&face.local, (u, v), tol.confusion() + RING_CHORD, tol);
237 if distance_to_rings(&face.rings, at) <= band {
238 return Ok(false);
239 }
240 Ok(inside_boundary(&face.rings, at))
241}
242
243const RING_CHORD: f64 = 1e-3;
245
246fn elements(model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<Vec<Element>> {
248 let mut out = Vec::new();
249 for vertex in explore_unique(model, shape, ShapeType::Vertex)? {
250 let Some(node) = model.node(&vertex) else {
251 ogeom_bail!(Dangling, "vertex is not in this model");
252 };
253 let Some(data) = node.data().as_vertex() else {
254 ogeom_bail!(Construction, "vertex node holds no vertex data");
255 };
256 let placed = vertex.transform(model.datums())?.apply(data.point);
257 out.push(Element::Vertex(vertex, placed));
258 }
259 for edge in explore_unique(model, shape, ShapeType::Edge)? {
260 let Some(node) = model.node(&edge) else {
261 ogeom_bail!(Dangling, "edge is not in this model");
262 };
263 let NodeData::Edge(data) = node.data() else {
264 ogeom_bail!(Construction, "edge node holds no edge data");
265 };
266 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
267 continue;
270 };
271 let Some(geometry) = model.geometry().curve(*curve) else {
272 ogeom_bail!(Dangling, "curve is not in this model");
273 };
274 let placement = edge.transform(model.datums())?;
275 let trimmed: Curve = if (range.0, range.1) == {
276 use ogeom_geom::Curve3d as _;
277 geometry.domain()
278 } {
279 geometry.clone()
280 } else {
281 TrimmedCurve::new(geometry.clone(), range.0, range.1, tol)?.into()
282 };
283 out.push(Element::Edge(
284 edge,
285 Box::new(trimmed.transformed(&placement, tol)?),
286 ));
287 }
288 let ring_deflection = Deflection {
289 chord: RING_CHORD,
290 angular: 0.05,
291 ..Deflection::default()
292 };
293 for face in explore_unique(model, shape, ShapeType::Face)? {
294 let Some(node) = model.node(&face) else {
295 ogeom_bail!(Dangling, "face is not in this model");
296 };
297 let NodeData::Face(data) = node.data() else {
298 ogeom_bail!(Construction, "face node holds no face data");
299 };
300 let Some(surface) = model.geometry().surface(data.surface) else {
301 ogeom_bail!(Dangling, "face refers to a surface not in this model");
302 };
303 let placement = face.transform(model.datums())?;
304 let rings = face_boundary(model, &face, ring_deflection, tol)?;
305 let restricted = restrict_to_rings(surface, &rings, tol)?;
311 out.push(Element::Face(
312 face,
313 Box::new(Prepared {
314 world: restricted.transformed(&placement, tol)?,
315 local: surface.clone(),
316 to_local: placement.inverse()?,
317 rings,
318 }),
319 ));
320 }
321 Ok(out)
322}
323
324fn restrict_to_rings(
332 surface: &SurfaceGeometry,
333 rings: &[Vec<Point2>],
334 tol: Tolerances,
335) -> OgeomResult<SurfaceGeometry> {
336 use ogeom_geom::Surface as _;
337 let ((ua, ub), (va, vb)) = surface.domain();
338 let mut u = (f64::INFINITY, f64::NEG_INFINITY);
339 let mut v = (f64::INFINITY, f64::NEG_INFINITY);
340 for ring in rings {
341 for p in ring {
342 u = (u.0.min(p.x), u.1.max(p.x));
343 v = (v.0.min(p.y), v.1.max(p.y));
344 }
345 }
346 if u.0 > u.1 || v.0 > v.1 {
347 return Ok(surface.clone());
349 }
350 let margin_u = (u.1 - u.0).mul_add(0.05, tol.parametric());
351 let margin_v = (v.1 - v.0).mul_add(0.05, tol.parametric());
352 let lo_u = (u.0 - margin_u).max(ua);
353 let hi_u = (u.1 + margin_u).min(ub);
354 let lo_v = (v.0 - margin_v).max(va);
355 let hi_v = (v.1 + margin_v).min(vb);
356 if lo_u >= hi_u || lo_v >= hi_v {
357 return Ok(surface.clone());
358 }
359 Ok(ogeom_geom::TrimmedSurface::new(surface.clone(), (lo_u, hi_u), (lo_v, hi_v), tol)?.into())
360}
361
362#[cfg(test)]
363#[allow(clippy::unwrap_used)]
364mod tests {
365 use super::*;
366 use crate::{make_box, make_cylinder, make_sphere};
367 use ogeom_math::{Direction, Frame, Vector};
368
369 const T: Tolerances = Tolerances::millimetres();
370
371 fn frame_at(origin: Point) -> Frame {
372 Frame::new(origin, Direction::Z, Direction::X, T).unwrap()
373 }
374
375 #[test]
376 fn parallel_box_walls_meet_at_the_gap_between_them() {
377 let mut model = Model::new();
378 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
379 let b = make_box(
380 &mut model,
381 frame_at(Point::new(5.0, 0.0, 0.0)),
382 (2.0, 2.0, 2.0),
383 T,
384 )
385 .unwrap();
386 let found =
387 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
388 .unwrap();
389 assert!((found.distance - 3.0).abs() < 1e-9, "{}", found.distance);
390 assert!(!found.pairs.is_empty());
391 for pair in &found.pairs {
392 assert!((pair.point_a.distance(pair.point_b) - found.distance).abs() < 1e-9);
393 }
394 }
395
396 #[test]
397 fn diagonal_boxes_meet_corner_to_corner() {
398 let mut model = Model::new();
401 let a = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
402 let b = make_box(
403 &mut model,
404 frame_at(Point::new(3.0, 3.0, 3.0)),
405 (1.0, 1.0, 1.0),
406 T,
407 )
408 .unwrap();
409 let found =
410 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
411 .unwrap();
412 let exact = (3.0_f64 * 4.0).sqrt(); assert!((found.distance - exact).abs() < 1e-9);
414 let pair = &found.pairs[0];
415 assert!(pair.point_a.is_equal(Point::new(1.0, 1.0, 1.0), T));
416 assert!(pair.point_b.is_equal(Point::new(3.0, 3.0, 3.0), T));
417 assert_eq!(model.kind_of(&pair.support_a).unwrap(), ShapeType::Vertex);
418 assert_eq!(model.kind_of(&pair.support_b).unwrap(), ShapeType::Vertex);
419 }
420
421 #[test]
422 fn a_sphere_over_a_box_measures_to_the_top_face() {
423 let mut model = Model::new();
424 let block = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
425 let ball = make_sphere(&mut model, frame_at(Point::new(2.0, 2.0, 4.0)), 1.0, T).unwrap();
426 let found = distance_between_shapes(
427 &model,
428 &block.shape,
429 &ball.shape,
430 ExtremaOptions::default(),
431 T,
432 )
433 .unwrap();
434 assert!((found.distance - 2.0).abs() < 1e-7, "{}", found.distance);
435 let pair = &found.pairs[0];
436 assert!(pair.point_a.is_equal(Point::new(2.0, 2.0, 1.0), T));
437 assert!(pair.point_b.is_equal(Point::new(2.0, 2.0, 3.0), T));
438 }
439
440 #[test]
441 fn parallel_cylinders_meet_wall_to_wall() {
442 let mut model = Model::new();
445 let a = make_cylinder(&mut model, Frame::WORLD, 1.0, 4.0, T).unwrap();
446 let b =
447 make_cylinder(&mut model, frame_at(Point::new(5.0, 0.0, 0.0)), 1.0, 4.0, T).unwrap();
448 let found =
449 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
450 .unwrap();
451 assert!((found.distance - 3.0).abs() < 1e-7, "{}", found.distance);
452 }
453
454 #[test]
455 fn touching_boxes_report_zero() {
456 let mut model = Model::new();
457 let a = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
458 let b = make_box(
459 &mut model,
460 frame_at(Point::new(2.0, 0.0, 0.0)),
461 (2.0, 2.0, 2.0),
462 T,
463 )
464 .unwrap();
465 let found =
466 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
467 .unwrap();
468 assert!(found.distance < 1e-9, "{}", found.distance);
469 }
470
471 #[test]
472 fn a_box_inside_a_box_measures_boundary_to_boundary() {
473 let mut model = Model::new();
476 let outer = make_box(&mut model, Frame::WORLD, (6.0, 6.0, 6.0), T).unwrap();
477 let inner = make_box(
478 &mut model,
479 frame_at(Point::new(2.0, 2.0, 2.0)),
480 (2.0, 2.0, 2.0),
481 T,
482 )
483 .unwrap();
484 let found = distance_between_shapes(
485 &model,
486 &outer.shape,
487 &inner.shape,
488 ExtremaOptions::default(),
489 T,
490 )
491 .unwrap();
492 assert!((found.distance - 2.0).abs() < 1e-9, "{}", found.distance);
493 }
494
495 #[test]
496 fn a_rotated_box_measures_edge_to_edge() {
497 let mut model = Model::new();
501 let a = make_box(&mut model, Frame::WORLD, (4.0, 4.0, 1.0), T).unwrap();
502 let tilted = Frame::new(
503 Point::new(2.0, 2.0, 3.0),
504 Direction::new(Vector::new(0.0, 1.0, 1.0), T).unwrap(),
505 Direction::X,
506 T,
507 )
508 .unwrap();
509 let b = make_box(&mut model, tilted, (1.0, 1.0, 1.0), T).unwrap();
510 let found =
511 distance_between_shapes(&model, &a.shape, &b.shape, ExtremaOptions::default(), T)
512 .unwrap();
513 let exact = 2.0 - core::f64::consts::FRAC_1_SQRT_2;
516 assert!((found.distance - exact).abs() < 1e-7, "{}", found.distance);
517 }
518}