1use std::cmp::{Ordering, Reverse};
95use std::collections::BinaryHeap;
96
97use axiolid_brep::ExactBRep;
98use axiolid_core::{Point2, Point3, Scalar, Tolerance, Vec3};
99use axiolid_surface::Surface;
100use axiolid_topology::Face;
101
102use crate::exact::ExactMeasureError;
103use crate::exact_distance::witness;
104use crate::exact_distance::Key as DistanceKey;
105use crate::exact_distance::{
106 closed, critical_toward, floor, point_lower_bound, surface_of, Element, Metric, Shape, Side,
107 POINT_EPSILONS, RATIO_EPSILONS,
108};
109use crate::mesh_hausdorff::HausdorffBounds;
110
111mod cut;
112mod translate;
113
114pub(crate) use cut::Cut;
115use cut::CutBounds;
116
117use axiolid_evaluate::surface::evaluate;
118use translate::{support_point, translate, Shifted};
119
120pub(crate) const MAX_SPLITS: usize = 200_000;
123
124const POINT_STEPS: usize = 20_000;
126
127const MAX_DISPLACEMENTS: usize = 16;
130
131#[derive(Debug, Clone, Copy, PartialEq)]
134pub struct BoundaryHausdorff {
135 pub distance: HausdorffBounds,
138 pub forward: HausdorffBounds,
141 pub backward: HausdorffBounds,
144}
145
146pub fn one_sided_boundary_hausdorff(
161 from: &ExactBRep,
162 to: &ExactBRep,
163 accuracy: Scalar,
164 tolerance: Tolerance,
165) -> Result<HausdorffBounds, ExactMeasureError> {
166 one_sided_boundary_hausdorff_with_budget(from, to, accuracy, tolerance, MAX_SPLITS)
167}
168
169pub fn one_sided_boundary_hausdorff_with_budget(
181 from: &ExactBRep,
182 to: &ExactBRep,
183 accuracy: Scalar,
184 tolerance: Tolerance,
185 max_splits: usize,
186) -> Result<HausdorffBounds, ExactMeasureError> {
187 let every = 0..from.topology().edges().len();
189 let edges = core::slice::from_ref(&every);
190 Ok(witnessed(from, to, accuracy, tolerance, max_splits, edges, None)?.bounds)
191}
192
193pub(crate) struct Witnessed {
195 pub(crate) bounds: HausdorffBounds,
196 pub(crate) from: Shape,
198 pub(crate) to: Shape,
200}
201
202#[allow(clippy::too_many_arguments)]
209pub(crate) fn witnessed(
210 from: &ExactBRep,
211 to: &ExactBRep,
212 accuracy: Scalar,
213 tolerance: Tolerance,
214 max_splits: usize,
215 items: &[core::ops::Range<usize>],
216 cuts: Option<(&Cut<'_>, &Cut<'_>)>,
217) -> Result<Witnessed, ExactMeasureError> {
218 let accuracy = accuracy.max(0.0);
219 let linear = tolerance.linear().max(1e-12);
220 let source = Side::new(from, linear, Metric::Space)?;
221 let mut target = Nearest::new(Side::new(to, linear, Metric::Space)?);
222 let mut cuts = cuts.map(|(from, to)| CutBounds::new(from, to));
223 search(
224 &source,
225 &mut target,
226 accuracy,
227 max_splits,
228 items,
229 cuts.as_mut(),
230 )
231}
232
233pub fn boundary_hausdorff_distance(
243 a: &ExactBRep,
244 b: &ExactBRep,
245 accuracy: Scalar,
246 tolerance: Tolerance,
247) -> Result<BoundaryHausdorff, ExactMeasureError> {
248 let forward = one_sided_boundary_hausdorff(a, b, accuracy, tolerance)?;
249 let backward = one_sided_boundary_hausdorff(b, a, accuracy, tolerance)?;
250 let mut distance = if backward.lower > forward.lower {
251 backward
252 } else {
253 forward
254 };
255 distance.upper = forward.upper.max(backward.upper);
256 Ok(BoundaryHausdorff {
257 distance,
258 forward,
259 backward,
260 })
261}
262
263#[derive(Debug, Clone, Copy, PartialEq)]
265struct Key(Scalar, Reverse<usize>);
266
267impl Eq for Key {}
268
269impl PartialOrd for Key {
270 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
271 Some(self.cmp(other))
272 }
273}
274
275impl Ord for Key {
276 fn cmp(&self, other: &Self) -> Ordering {
277 self.0.total_cmp(&other.0).then(self.1.cmp(&other.1))
278 }
279}
280
281struct Nearest<'a> {
284 side: Side<'a>,
285 nodes: Vec<Element>,
286 children: Vec<Option<Vec<usize>>>,
287 roots: Vec<usize>,
288}
289
290#[derive(Debug, Clone, Copy)]
293struct PointDistance {
294 lower: Scalar,
295 upper: Scalar,
296 nearest: Point3,
297 on: Shape,
299}
300
301impl<'a> Nearest<'a> {
302 fn new(side: Side<'a>) -> Self {
303 let nodes = side.elements.clone();
304 let roots = (0..nodes.len()).collect();
305 let children = vec![None; nodes.len()];
306 Self {
307 side,
308 nodes,
309 children,
310 roots,
311 }
312 }
313
314 fn children(&mut self, node: usize) -> Result<Vec<usize>, ExactMeasureError> {
315 if let Some(known) = &self.children[node] {
316 return Ok(known.clone());
317 }
318 let parent = self.side.size(&self.nodes[node]);
319 let split = self.side.split(&self.nodes[node])?;
320 let ids = if split.iter().any(|child| self.side.size(child) >= parent) {
322 Vec::new()
323 } else {
324 split
325 .into_iter()
326 .map(|child| {
327 self.nodes.push(child);
328 self.children.push(None);
329 self.nodes.len() - 1
330 })
331 .collect()
332 };
333 self.children[node] = Some(ids.clone());
334 Ok(ids)
335 }
336
337 fn query(
340 &mut self,
341 p: Point3,
342 accuracy: Scalar,
343 ) -> Result<Option<PointDistance>, ExactMeasureError> {
344 let scale = p.length() + self.side.scale;
345 let error = self.side.witness_error();
346 let excess = self.side.witness_excess();
347 let mut heap = BinaryHeap::new();
349 let mut order = 0;
350 let mut best: Option<(Scalar, Point3, Shape)> = None;
351 for &root in &self.roots {
352 let node = &self.nodes[root];
353 if critical_toward(node, p) {
354 let bound = point_lower_bound(p, &self.side, node)?;
355 heap.push(Reverse((DistanceKey(bound), order, root)));
356 order += 1;
357 offer(&mut best, p, node);
358 }
359 }
360 let mut lower = 0.0;
361 let mut steps = 0;
362 while let Some(Reverse((DistanceKey(bound), popped, node))) = heap.pop() {
363 lower = bound;
364 let upper = best.map_or(Scalar::INFINITY, |(d, ..)| d);
365 if bound < upper {
368 let element = self.nodes[node];
369 if let Some((q, on)) = witness::toward(p, &self.side, &element)? {
370 let d = (q - p).length();
371 if best.is_none_or(|(current, ..)| d < current) {
372 best = Some((d, q, on));
373 }
374 }
375 }
376 let upper = best.map_or(Scalar::INFINITY, |(d, ..)| {
377 super::exact_distance::witnessed_upper(d, error)
378 });
379 if upper.is_finite()
380 && (bound >= upper || closed(bound, upper, accuracy, floor(scale, upper) + excess))
381 {
382 break;
383 }
384 steps += 1;
385 let children = self.children(node)?;
386 if steps > POINT_STEPS || children.is_empty() {
387 heap.push(Reverse((DistanceKey(bound), popped, node)));
388 break;
389 }
390 for child in children {
391 let element = &self.nodes[child];
392 if critical_toward(element, p) {
393 let bound = point_lower_bound(p, &self.side, element)?;
394 heap.push(Reverse((DistanceKey(bound), order, child)));
395 order += 1;
396 offer(&mut best, p, element);
397 }
398 }
399 }
400 if let Some(Reverse((DistanceKey(bound), ..))) = heap.peek() {
401 lower = Scalar::min(lower, *bound);
402 }
403 Ok(best.map(|(d, nearest, on)| {
404 let upper = super::exact_distance::witnessed_upper(d, error);
405 PointDistance {
406 lower: lower.min(upper),
407 upper,
408 nearest,
409 on,
410 }
411 }))
412 }
413}
414
415fn offer(best: &mut Option<(Scalar, Point3, Shape)>, p: Point3, element: &Element) {
420 if let Some(w) = element.witness {
421 let d = (w - p).length();
422 if best.is_none_or(|(current, ..)| d < current) {
423 *best = Some((d, w, element.shape));
424 }
425 }
426}
427
428#[derive(Debug, Clone)]
431struct Match {
432 face: usize,
433 shifted: Option<Shifted>,
434}
435
436fn matches(a: &ExactBRep, b: &ExactBRep) -> Vec<Vec<Match>> {
438 let fa = a.topology().faces();
439 let fb = b.topology().faces();
440 fa.iter()
441 .map(|face_a| {
442 fb.iter()
443 .enumerate()
444 .filter_map(|(face, face_b)| {
445 if same_face(a, face_a, b, face_b) {
446 Some(Match {
447 face,
448 shifted: None,
449 })
450 } else {
451 translate(a, face_a, b, face_b).map(|shifted| Match {
452 face,
453 shifted: Some(shifted),
454 })
455 }
456 })
457 .collect()
458 })
459 .collect()
460}
461
462fn same_face(
465 a: &ExactBRep,
466 fa: &Face<axiolid_brep::SurfaceId>,
467 b: &ExactBRep,
468 fb: &Face<axiolid_brep::SurfaceId>,
469) -> bool {
470 let (Ok(sa), Ok(sb)) = (surface_of(a, fa.surface), surface_of(b, fb.surface)) else {
471 return false;
472 };
473 if !same_shape(sa, sb) || fa.bounds.len() != fb.bounds.len() {
474 return false;
475 }
476 let (ta, tb) = (a.topology(), b.topology());
477 fa.bounds.iter().zip(&fb.bounds).all(|(ba, bb)| {
478 if ba.orientation != bb.orientation || ba.outer != bb.outer {
479 return false;
480 }
481 let (Some(la), Some(lb)) = (
482 ta.loops().get(ba.loop_id.index()),
483 tb.loops().get(bb.loop_id.index()),
484 ) else {
485 return false;
486 };
487 la.edges.len() == lb.edges.len()
488 && la
489 .edges
490 .iter()
491 .zip(&lb.edges)
492 .enumerate()
493 .all(|(i, (ua, ub))| {
494 let (Some(ca), Some(cb)) = (pcurve(a, ua.pcurve), pcurve(b, ub.pcurve)) else {
495 return false;
496 };
497 let ia = a.pcurve_interval(ba.loop_id, i);
498 let ib = b.pcurve_interval(bb.loop_id, i);
499 ia.is_some() && ia == ib && ca == cb
500 })
501 })
502}
503
504fn pcurve(brep: &ExactBRep, id: Option<axiolid_brep::Curve2Id>) -> Option<&axiolid_curve::Curve2> {
505 id.and_then(|id| brep.curves2().get(id.index()))
506}
507
508fn same_shape(a: &Surface, b: &Surface) -> bool {
516 match (a, b) {
517 (Surface::Plane(_), Surface::Plane(_))
518 | (Surface::Cylinder(_), Surface::Cylinder(_))
519 | (Surface::EllipticalCylinder(_), Surface::EllipticalCylinder(_))
520 | (Surface::Sphere(_), Surface::Sphere(_))
521 | (Surface::Torus(_), Surface::Torus(_)) => true,
522 (Surface::Cone(p), Surface::Cone(q)) => {
523 p.radius == q.radius && p.semi_angle == q.semi_angle
524 }
525 (Surface::BSpline(p), Surface::BSpline(q)) => {
526 let shape = |s: &axiolid_surface::BSplineSurface| {
527 s.control_points.iter().map(Vec::len).collect::<Vec<_>>()
528 };
529 p.u_degree == q.u_degree
530 && p.v_degree == q.v_degree
531 && p.u_knots == q.u_knots
532 && p.v_knots == q.v_knots
533 && p.u_multiplicities == q.u_multiplicities
534 && p.v_multiplicities == q.v_multiplicities
535 && p.weights == q.weights
536 && shape(p) == shape(q)
537 && p.weights
538 .as_ref()
539 .is_none_or(|w| w.iter().flatten().all(|w| w.is_finite() && *w > 0.0))
540 }
541 _ => false,
542 }
543}
544
545fn terms(surface: &Surface) -> Option<(Point3, Vec<Vec3>)> {
548 Some(match surface {
549 Surface::Plane(p) => (p.frame.origin, vec![p.frame.x, p.frame.y]),
550 Surface::Cylinder(c) => (
551 c.frame.origin,
552 vec![c.frame.x * c.radius, c.frame.y * c.radius, c.frame.z],
553 ),
554 Surface::EllipticalCylinder(c) => (
555 c.frame.origin,
556 vec![
557 c.frame.x * c.semi_axis_x,
558 c.frame.y * c.semi_axis_y,
559 c.frame.z,
560 ],
561 ),
562 Surface::Cone(c) => {
563 let slope = c.semi_angle.tan();
564 (
565 c.frame.origin,
566 vec![
567 c.frame.x * c.radius,
568 c.frame.y * c.radius,
569 c.frame.x * slope,
570 c.frame.y * slope,
571 c.frame.z,
572 ],
573 )
574 }
575 Surface::Sphere(s) => (
576 s.frame.origin,
577 vec![
578 s.frame.x * s.radius,
579 s.frame.y * s.radius,
580 s.frame.z * s.radius,
581 ],
582 ),
583 Surface::Torus(t) => (
584 t.frame.origin,
585 vec![
586 t.frame.x * t.major_radius,
587 t.frame.y * t.major_radius,
588 t.frame.x * t.minor_radius,
589 t.frame.y * t.minor_radius,
590 t.frame.z * t.minor_radius,
591 ],
592 ),
593 _ => return None,
594 })
595}
596
597fn basis(
601 surface: &Surface,
602 lo: Point2,
603 hi: Point2,
604) -> Option<(Vec<Scalar>, Vec<Scalar>, Vec<Scalar>)> {
605 let m = (lo + hi) * 0.5;
606 let (hu, hv) = (0.5 * (hi.x - lo.x).abs(), 0.5 * (hi.y - lo.y).abs());
607 let (tu, tv) = (hu.min(2.0), hv.min(2.0));
609 let reach_u = lo.x.abs().max(hi.x.abs());
610 let reach_v = lo.y.abs().max(hi.y.abs());
611 let (su, cu) = m.x.sin_cos();
612 let (sv, cv) = m.y.sin_cos();
613 Some(match surface {
614 Surface::Plane(_) => (vec![m.x, m.y], vec![hu, hv], vec![reach_u, reach_v]),
615 Surface::Cylinder(_) | Surface::EllipticalCylinder(_) => {
616 (vec![cu, su, m.y], vec![tu, tu, hv], vec![1.0, 1.0, reach_v])
617 }
618 Surface::Cone(_) => {
619 let mixed = hv + m.y.abs() * tu;
621 (
622 vec![cu, su, m.y * cu, m.y * su, m.y],
623 vec![tu, tu, mixed, mixed, hv],
624 vec![1.0, 1.0, reach_v, reach_v, reach_v],
625 )
626 }
627 Surface::Sphere(_) => (
628 vec![cv * cu, cv * su, sv],
629 vec![tv + tu, tv + tu, tv],
630 vec![1.0, 1.0, 1.0],
631 ),
632 Surface::Torus(_) => (
633 vec![cu, su, cv * cu, cv * su, sv],
634 vec![tu, tu, tv + tu, tv + tu, tv],
635 vec![1.0, 1.0, 1.0, 1.0, 1.0],
636 ),
637 _ => return None,
638 })
639}
640
641fn matched_bound(a: &Surface, b: &Surface, lo: Point2, hi: Point2) -> Option<Scalar> {
644 if let (Surface::BSpline(p), Surface::BSpline(q)) = (a, b) {
645 let mut reach: Scalar = 0.0;
648 let mut scale: Scalar = 0.0;
649 for (row_a, row_b) in p.control_points.iter().zip(&q.control_points) {
650 for (pa, pb) in row_a.iter().zip(row_b) {
651 reach = reach.max((*pa - *pb).length());
652 scale = scale.max(pa.length()).max(pb.length());
653 }
654 }
655 return Some(
656 reach * (1.0 + RATIO_EPSILONS * Scalar::EPSILON)
657 + POINT_EPSILONS * Scalar::EPSILON * scale
658 + Scalar::MIN_POSITIVE,
659 );
660 }
661 let (oa, ka) = terms(a)?;
662 let (ob, kb) = terms(b)?;
663 let (phi, wobble, reach) = basis(a, lo, hi)?;
664 let mut centre = oa - ob;
665 let mut spread = 0.0;
666 let mut scale = oa.length() + ob.length();
667 for k in 0..phi.len() {
668 let delta = ka[k] - kb[k];
669 centre += delta * phi[k];
670 spread += wobble[k] * delta.length();
671 scale += reach[k] * (ka[k].length() + kb[k].length());
672 }
673 let bound = (centre.length() + spread) * (1.0 + RATIO_EPSILONS * Scalar::EPSILON)
674 + POINT_EPSILONS * Scalar::EPSILON * scale
675 + Scalar::MIN_POSITIVE;
676 bound.is_finite().then_some(bound)
677}
678
679fn displacements(
684 source: &Side<'_>,
685 target: &Side<'_>,
686 matched: &[Vec<Match>],
687) -> Result<Vec<Vec3>, ExactMeasureError> {
688 let rounding = 4.0 * POINT_EPSILONS * Scalar::EPSILON * (source.scale + target.scale);
691 let mut found: Vec<Vec3> = Vec::new();
692 for element in &source.elements {
693 let Shape::Face { face, lo, hi, .. } = element.shape else {
694 continue;
695 };
696 let surface = surface_of(source.brep, source.brep.topology().faces()[face].surface)?;
697 for other in &matched[face] {
698 let theirs = match &other.shifted {
699 Some(shifted) => &shifted.surface,
700 None => surface_of(
701 target.brep,
702 target.brep.topology().faces()[other.face].surface,
703 )?,
704 };
705 let moves: Vec<Vec3> = [(lo + hi) * 0.5, lo, hi]
708 .iter()
709 .filter_map(|q| {
710 let here = evaluate(surface, q.x, q.y).ok()?;
711 let there = evaluate(theirs, q.x, q.y).ok()?;
712 Some(there - here)
713 })
714 .collect();
715 let [t, a, b] = moves[..] else {
716 continue;
717 };
718 let rigid =
719 (a - t).length().max((b - t).length()) <= 1e-9 * (1.0 + t.length()) + rounding;
720 let fresh = rigid
721 && t.is_finite()
722 && t.length() > 0.0
723 && found
724 .iter()
725 .all(|seen| (*seen - t).length() > 1e-9 * (1.0 + t.length()) + rounding);
726 if fresh && found.len() < MAX_DISPLACEMENTS {
727 found.push(t);
728 }
729 }
730 }
731 Ok(found)
732}
733
734fn witness_offset(element: &Element, w: Point3) -> Scalar {
740 (w - element.centre).length() * (1.0 + RATIO_EPSILONS * Scalar::EPSILON)
741}
742
743fn witnessed_lower(found: &PointDistance, error: Scalar) -> Scalar {
746 (found.lower - error).max(0.0)
747}
748
749struct State {
751 lower: Scalar,
752 witness: Option<(Point3, Point3, Shape, Shape)>,
754}
755
756fn search(
757 source: &Side<'_>,
758 target: &mut Nearest<'_>,
759 accuracy: Scalar,
760 max_splits: usize,
761 items: &[core::ops::Range<usize>],
762 mut cuts: Option<&mut CutBounds<'_>>,
763) -> Result<Witnessed, ExactMeasureError> {
764 let matched = matches(source.brep, target.side.brep);
765 let point_accuracy = 0.25 * accuracy;
766 let outer_scale = 2.0 * (source.scale + target.side.scale);
769 let outer_excess = 2.0 * (source.witness_excess() + target.side.witness_excess());
770 let source_error = source.witness_error();
771 let mut state = State {
772 lower: 0.0,
773 witness: None,
774 };
775 let mut faces: BinaryHeap<(Key, usize)> = BinaryHeap::new();
776 let mut edges: BinaryHeap<(Key, usize)> = BinaryHeap::new();
777 let mut pieces: Vec<Element> = Vec::new();
779
780 let seeds = displacements(source, &target.side, &matched)?
783 .into_iter()
784 .flat_map(|t| items.iter().map(move |edges| (t, edges.clone())));
785 for (t, edges) in seeds {
786 let Some((p, edge)) = support_point(source.brep, -t, edges) else {
787 continue;
788 };
789 if let Some(found) = target.query(p, point_accuracy)? {
790 let lower = witnessed_lower(&found, source_error);
791 if lower > state.lower || state.witness.is_none() {
792 state.lower = state.lower.max(lower);
793 let on = Shape::Edge {
794 edge,
795 t0: 0.0,
796 t1: 0.0,
797 };
798 state.witness = Some((p, found.nearest, on, found.on));
799 }
800 }
801 }
802
803 let mut admit = |element: Element,
806 state: &mut State,
807 faces: &mut BinaryHeap<(Key, usize)>,
808 edges: &mut BinaryHeap<(Key, usize)>,
809 pieces: &mut Vec<Element>|
810 -> Result<(), ExactMeasureError> {
811 let mut upper = Scalar::INFINITY;
812 if let Shape::Face { face, lo, hi, .. } = element.shape {
813 let surface = surface_of(source.brep, source.brep.topology().faces()[face].surface)?;
814 for other in &matched[face] {
815 let bound = match &other.shifted {
816 None => {
817 let target_surface = surface_of(
818 target.side.brep,
819 target.side.brep.topology().faces()[other.face].surface,
820 )?;
821 matched_bound(surface, target_surface, lo, hi)
822 }
823 Some(shifted) => matched_bound(surface, &shifted.surface, lo, hi)
824 .and_then(|bound| shifted.bound(bound, lo, hi)),
825 };
826 if let Some(bound) = bound {
827 upper = upper.min(bound);
828 }
829 }
830 if let Some(cuts) = cuts.as_deref_mut() {
833 if let Some(bound) = cuts.bound(source.brep, face, lo, hi)? {
834 upper = upper.min(bound);
835 }
836 }
837 }
838 if let Some(w) = element.witness {
840 if upper > state.lower {
841 if let Some(found) = target.query(w, point_accuracy)? {
842 let lower = witnessed_lower(&found, source_error);
843 if lower > state.lower || state.witness.is_none() {
844 state.lower = state.lower.max(lower);
845 state.witness = Some((w, found.nearest, element.shape, found.on));
846 }
847 upper = upper.min(found.upper + element.radius + witness_offset(&element, w));
850 }
851 }
852 }
853 if element.witness.is_none() && upper > state.lower {
856 if let Some(found) = target.query(element.centre, point_accuracy)? {
857 upper = upper.min(found.upper + element.radius);
858 }
859 }
860 if !upper.is_finite() {
861 return Err(crate::exact::NOT_CONVERGED);
862 }
863 pieces.push(element);
864 let key = Key(upper, Reverse(pieces.len()));
865 match element.shape {
866 Shape::Face { .. } => faces.push((key, pieces.len() - 1)),
867 Shape::Edge { .. } => edges.push((key, pieces.len() - 1)),
868 }
869 Ok(())
870 };
871
872 for element in source.elements.clone() {
873 admit(element, &mut state, &mut faces, &mut edges, &mut pieces)?;
874 }
875
876 let mut splits = 0;
877 loop {
878 let face_top = faces.peek().map_or(Scalar::NEG_INFINITY, |(key, _)| key.0);
881 let upper = face_top.max(state.lower);
882 let settled = floor(outer_scale, upper) + outer_excess;
883 if upper - state.lower <= accuracy.max(settled) || splits >= max_splits {
884 break;
885 }
886 splits += 1;
887 let edge_worth = edges
890 .peek()
891 .is_some_and(|(key, _)| key.0 > state.lower + accuracy);
892 let take_edge = edge_worth && (splits % 2 == 0 || faces.is_empty());
893 let popped = if take_edge { edges.pop() } else { faces.pop() };
894 let Some((key, index)) = popped else {
895 break;
896 };
897 let element = pieces[index];
898 let children = source.split(&element)?;
899 let parent = source.size(&element);
900 if children.iter().any(|child| source.size(child) >= parent) {
901 if !take_edge {
904 faces.push((key, index));
905 break;
906 }
907 continue;
908 }
909 for child in children {
910 admit(child, &mut state, &mut faces, &mut edges, &mut pieces)?;
911 }
912 }
913
914 let face_top = faces.peek().map_or(Scalar::NEG_INFINITY, |(key, _)| key.0);
915 let (point_from, point_to, from, to) = state.witness.ok_or(crate::exact::NOT_CONVERGED)?;
916 Ok(Witnessed {
917 bounds: HausdorffBounds {
918 lower: state.lower,
919 upper: face_top.max(state.lower),
920 point_from,
921 point_to,
922 },
923 from,
924 to,
925 })
926}
927
928#[cfg(test)]
929mod tests {
930 use super::{matched_bound, same_shape};
934 use axiolid_core::{Frame3, Point2, Point3, Vec3};
935 use axiolid_curve::{BSplineSurface, KnotSpec};
936 use axiolid_evaluate::surface::evaluate;
937 use axiolid_surface::{Cone, Cylinder, EllipticalCylinder, Plane, Sphere, Surface, Torus};
938
939 fn frame(turn: f64, shift: Vec3) -> Frame3 {
940 let z = Vec3::new(0.0, -0.6, 0.8);
942 let x0 = Vec3::new(0.6, 0.64, 0.48);
943 let y0 = z.cross(x0);
944 let (s, c) = turn.sin_cos();
945 Frame3 {
946 origin: Point3::new(1.5, -2.0, 0.75) + shift,
947 x: x0 * c + y0 * s,
948 y: y0 * c - x0 * s,
949 z,
950 }
951 }
952
953 fn families(f: Frame3) -> Vec<(Surface, Point2, Point2)> {
954 vec![
955 (
956 Surface::Plane(Plane { frame: f }),
957 Point2::new(-1.0, 0.5),
958 Point2::new(2.0, 1.25),
959 ),
960 (
961 Surface::Cylinder(Cylinder {
962 frame: f,
963 radius: 2.5,
964 }),
965 Point2::new(0.3, -1.0),
966 Point2::new(1.9, 2.0),
967 ),
968 (
969 Surface::EllipticalCylinder(EllipticalCylinder {
970 frame: f,
971 semi_axis_x: 3.0,
972 semi_axis_y: 1.0,
973 }),
974 Point2::new(0.2, 0.0),
975 Point2::new(1.4, 1.0),
976 ),
977 (
978 Surface::Cone(Cone {
979 frame: f,
980 radius: 1.5,
981 semi_angle: 0.4,
982 }),
983 Point2::new(0.5, 0.2),
984 Point2::new(2.5, 1.5),
985 ),
986 (
987 Surface::Sphere(Sphere {
988 frame: f,
989 radius: 2.0,
990 }),
991 Point2::new(0.4, -0.3),
992 Point2::new(2.0, 1.1),
993 ),
994 (
995 Surface::Torus(Torus {
996 frame: f,
997 major_radius: 3.0,
998 minor_radius: 1.0,
999 }),
1000 Point2::new(0.1, 0.5),
1001 Point2::new(1.7, 2.9),
1002 ),
1003 ]
1004 }
1005
1006 fn samples(lo: Point2, hi: Point2) -> impl Iterator<Item = Point2> {
1007 const N: usize = 24;
1008 (0..=N).flat_map(move |i| {
1009 (0..=N).map(move |j| {
1010 Point2::new(
1011 lo.x + (hi.x - lo.x) * i as f64 / N as f64,
1012 lo.y + (hi.y - lo.y) * j as f64 / N as f64,
1013 )
1014 })
1015 })
1016 }
1017
1018 fn reach_and_bound(a: &Surface, b: &Surface, lo: Point2, hi: Point2) -> (f64, f64) {
1020 let bound = matched_bound(a, b, lo, hi).expect("a matched pair");
1021 let mut reach: f64 = 0.0;
1022 for p in samples(lo, hi) {
1023 let pa = evaluate(a, p.x, p.y).expect("point");
1024 let pb = evaluate(b, p.x, p.y).expect("point");
1025 reach = reach.max((pa - pb).length());
1026 }
1027 (reach, bound)
1028 }
1029
1030 #[test]
1031 fn a_translate_is_bounded_by_its_translation_exactly() {
1032 let shift = Vec3::new(0.3, -0.4, 1.2);
1033 let a = families(frame(0.0, Vec3::ZERO));
1034 let b = families(frame(0.0, shift));
1035 for ((sa, lo, hi), (sb, _, _)) in a.iter().zip(&b) {
1036 assert!(same_shape(sa, sb));
1037 let (reach, bound) = reach_and_bound(sa, sb, *lo, *hi);
1038 assert!(reach <= bound, "{sa:?}: {reach} > {bound}");
1039 assert!(
1040 (bound - shift.length()).abs() < 1e-9,
1041 "{sa:?}: {bound} is not the translation"
1042 );
1043 }
1044 }
1045
1046 #[test]
1050 fn each_axis_moved_alone_is_held_over_tall_and_wide_patches() {
1051 let base = Frame3 {
1052 origin: Point3::ZERO,
1053 x: Vec3::X,
1054 y: Vec3::Y,
1055 z: Vec3::Z,
1056 };
1057 let tall = (Point2::new(0.3, 0.2), Point2::new(0.35, 1.4));
1058 let cases = |f: Frame3| {
1059 vec![
1060 (
1061 Surface::Sphere(Sphere {
1062 frame: f,
1063 radius: 2.0,
1064 }),
1065 tall,
1066 ),
1067 (
1068 Surface::Torus(Torus {
1069 frame: f,
1070 major_radius: 3.0,
1071 minor_radius: 1.0,
1072 }),
1073 tall,
1074 ),
1075 (
1076 Surface::Cone(Cone {
1077 frame: f,
1078 radius: 0.1,
1079 semi_angle: 0.4,
1080 }),
1081 (Point2::new(0.0, 5.0), Point2::new(1.6, 5.2)),
1082 ),
1083 ]
1084 };
1085 for axis in 0..3 {
1086 let mut moved = base;
1087 let nudge = Vec3::new(0.3, 0.0, 0.0);
1088 match axis {
1089 0 => moved.x += nudge,
1090 1 => moved.y += nudge,
1091 _ => moved.z += nudge,
1092 }
1093 for ((sa, (lo, hi)), (sb, _)) in cases(base).iter().zip(cases(moved)) {
1094 let (reach, bound) = reach_and_bound(sa, &sb, *lo, *hi);
1095 assert!(reach <= bound, "{sa:?}, axis {axis}: {reach} > {bound}");
1096 }
1097 }
1098 }
1099
1100 #[test]
1101 fn a_turned_and_moved_copy_is_held_and_not_vacuously() {
1102 let a = families(frame(0.0, Vec3::ZERO));
1103 let b = families(frame(0.05, Vec3::new(0.01, 0.0, -0.02)));
1104 for ((sa, lo, hi), (sb, _, _)) in a.iter().zip(&b) {
1105 let (reach, bound) = reach_and_bound(sa, sb, *lo, *hi);
1106 assert!(reach <= bound, "{sa:?}: {reach} > {bound}");
1107 assert!(bound <= 4.0 * reach + 1e-9, "{sa:?}: {bound} vs {reach}");
1108 let mid = (*lo + *hi) * 0.5;
1110 let (small_reach, small) = reach_and_bound(sa, sb, *lo, mid);
1111 assert!(small_reach <= small, "{sa:?}: {small_reach} > {small}");
1112 }
1113 }
1114
1115 #[test]
1116 fn a_different_family_cone_or_net_is_not_matched() {
1117 let f = frame(0.0, Vec3::ZERO);
1118 let thin = Surface::Cylinder(Cylinder {
1119 frame: f,
1120 radius: 1.0,
1121 });
1122 let thick = Surface::Cylinder(Cylinder {
1123 frame: f,
1124 radius: 1.5,
1125 });
1126 assert!(same_shape(&thin, &thick));
1128 let (lo, hi) = (Point2::new(0.2, -0.5), Point2::new(1.1, 0.5));
1129 let (reach, bound) = reach_and_bound(&thin, &thick, lo, hi);
1130 assert!(reach <= bound && bound <= 4.0 * reach, "{reach} vs {bound}");
1131 assert!(!same_shape(&thin, &Surface::Plane(Plane { frame: f })));
1132 let cone = |semi_angle| {
1133 Surface::Cone(Cone {
1134 frame: f,
1135 radius: 1.0,
1136 semi_angle,
1137 })
1138 };
1139 assert!(!same_shape(&cone(0.3), &cone(0.4)));
1140 assert!(!same_shape(
1142 &cone(0.3),
1143 &Surface::Cone(Cone {
1144 frame: f,
1145 radius: 1.2,
1146 semi_angle: 0.3,
1147 })
1148 ));
1149 }
1150
1151 #[test]
1152 fn a_spline_net_moved_point_by_point_is_held_by_its_largest_move() {
1153 let net = |lift: f64| BSplineSurface {
1154 u_degree: 1,
1155 v_degree: 1,
1156 control_points: vec![
1157 vec![Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 1.0, lift)],
1158 vec![
1159 Point3::new(1.0, 0.0, 0.0),
1160 Point3::new(1.0, 1.0, 0.5 * lift),
1161 ],
1162 ],
1163 u_knots: vec![0.0, 1.0],
1164 u_multiplicities: vec![2, 2],
1165 v_knots: vec![0.0, 1.0],
1166 v_multiplicities: vec![2, 2],
1167 weights: None,
1168 u_closed: false,
1169 v_closed: false,
1170 knot_spec: KnotSpec::Unspecified,
1171 self_intersect: None,
1172 };
1173 let (a, b) = (Surface::BSpline(net(0.0)), Surface::BSpline(net(0.2)));
1174 assert!(same_shape(&a, &b));
1175 let (lo, hi) = (Point2::new(0.0, 0.0), Point2::new(1.0, 1.0));
1176 let (reach, bound) = reach_and_bound(&a, &b, lo, hi);
1177 assert!(reach <= bound && bound <= 0.2 + 1e-9, "{reach} vs {bound}");
1178 let mut other = net(0.0);
1180 other.u_knots = vec![0.0, 2.0];
1181 assert!(!same_shape(&a, &Surface::BSpline(other)));
1182 }
1183
1184 fn sheet(
1187 origin: Point3,
1188 pcurve: axiolid_curve::Curve2,
1189 interval: axiolid_core::Interval,
1190 ) -> axiolid_brep::ExactBRep {
1191 use axiolid_topology::{Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Vertex};
1192 let mut builder = axiolid_brep::ExactBRepBuilder::default();
1193 let curve3 = builder.add_curve3(axiolid_curve::Curve3::Line(axiolid_curve::Line3 {
1194 origin,
1195 direction: Vec3::X,
1196 }));
1197 let curve2 = builder.add_curve2(pcurve);
1198 let surface = builder.add_surface(Surface::Plane(Plane {
1199 frame: Frame3 {
1200 origin,
1201 x: Vec3::X,
1202 y: Vec3::Y,
1203 z: Vec3::Z,
1204 },
1205 }));
1206 let topology = builder.topology_mut();
1207 let vertices: Vec<_> = [Point3::X, Point3::Y, Point3::Z]
1208 .into_iter()
1209 .map(|position| topology.add_vertex(Vertex { position }))
1210 .collect();
1211 let edges: Vec<_> = (0..3)
1212 .map(|i| {
1213 topology.add_edge(Edge {
1214 start: vertices[i],
1215 end: vertices[(i + 1) % 3],
1216 curve: Some(curve3),
1217 })
1218 })
1219 .collect();
1220 let loop_id = topology.add_loop(Loop {
1221 edges: edges
1222 .iter()
1223 .map(|&edge| EdgeUse {
1224 edge,
1225 orientation: Orientation::Forward,
1226 pcurve: Some(curve2),
1227 })
1228 .collect(),
1229 });
1230 topology.add_face(Face {
1231 surface: Some(surface),
1232 bounds: vec![FaceBound {
1233 loop_id,
1234 orientation: Orientation::Forward,
1235 outer: true,
1236 }],
1237 orientation: Orientation::Forward,
1238 });
1239 for edge in edges {
1240 builder.set_edge_interval(edge, axiolid_core::Interval::UNIT);
1241 }
1242 for use_index in 0..3 {
1243 builder.set_pcurve_interval(loop_id, use_index, interval);
1244 }
1245 builder.finish().expect("a valid sheet")
1246 }
1247
1248 #[test]
1251 fn faces_match_only_on_the_same_trim() {
1252 use axiolid_core::{Interval, Vec2};
1253 use axiolid_curve::{Curve2, Line2};
1254 let line = |dx: f64| {
1255 Curve2::Line(Line2 {
1256 origin: Point2::new(0.5, 0.0),
1257 direction: Vec2::new(dx, 1.0),
1258 })
1259 };
1260 let unit = Interval::UNIT;
1261 let a = sheet(Point3::ZERO, line(1.0), unit);
1262 let matched = |b: &axiolid_brep::ExactBRep| {
1263 super::same_face(&a, &a.topology().faces()[0], b, &b.topology().faces()[0])
1264 };
1265 assert!(matched(&sheet(
1266 Point3::new(3.0, 1.0, -2.0),
1267 line(1.0),
1268 unit
1269 )));
1270 assert!(!matched(&sheet(Point3::ZERO, line(2.0), unit)));
1271 assert!(!matched(&sheet(
1272 Point3::ZERO,
1273 line(1.0),
1274 Interval::new(0.0, 0.5)
1275 )));
1276 let found = super::matches(&a, &a);
1277 assert_eq!(found.len(), 1);
1278 assert_eq!(found[0].len(), 1);
1279 assert_eq!(found[0][0].face, 0);
1280 assert!(found[0][0].shifted.is_none());
1281 }
1282
1283 #[test]
1288 fn a_point_under_an_edge_finds_the_edge_witness() {
1289 use super::Nearest;
1290 use crate::exact_distance::fixtures::unit_box;
1291 use crate::exact_distance::{floor, Metric, Side};
1292 for shift in [Vec3::ZERO, Vec3::new(450_000.0, 5_600_000.0, 0.0)] {
1293 let b = unit_box(Point3::ZERO + shift + Vec3::new(0.0, 0.0, 0.02));
1294 let mut nearest = Nearest::new(Side::new(&b, 1e-12, Metric::Space).expect("a side"));
1295 for k in 1..64 {
1296 let t = f64::from(k) / 64.0;
1297 for q in [Point3::new(t, 0.0, 0.0), Point3::new(t, t, 0.0)] {
1298 let p = q + shift;
1299 let found = nearest
1300 .query(p, 1e-10)
1301 .expect("evaluates")
1302 .expect("a boundary point");
1303 let settled = floor(p.length() + nearest.side.scale, found.upper);
1304 assert!(
1305 found.lower <= 0.02 && 0.02 <= found.upper,
1306 "{p:?}: [{}, {}]",
1307 found.lower,
1308 found.upper
1309 );
1310 assert!(
1311 found.upper - found.lower <= 1e-10_f64.max(settled),
1312 "{p:?}: [{}, {}]",
1313 found.lower,
1314 found.upper
1315 );
1316 }
1317 }
1318 }
1319 }
1320
1321 #[test]
1324 fn a_point_over_a_face_closes_on_its_foot() {
1325 use super::Nearest;
1326 use crate::exact_distance::fixtures::unit_box;
1327 use crate::exact_distance::{Metric, Side};
1328 let b = unit_box(Point3::ZERO);
1329 let mut nearest = Nearest::new(Side::new(&b, 1e-12, Metric::Space).expect("a side"));
1330 let found = nearest
1331 .query(Point3::new(0.37, 0.61, 1.5), 1e-12)
1332 .expect("evaluates")
1333 .expect("a boundary point");
1334 assert!(found.lower <= 0.5 && 0.5 <= found.upper, "{found:?}");
1335 assert!(found.upper - found.lower <= 1e-12, "{found:?}");
1336 }
1337
1338 fn charted_sheet(frame: Frame3, corners: &[Point2]) -> axiolid_brep::ExactBRep {
1342 use axiolid_core::{Interval, Vec2};
1343 use axiolid_curve::{Curve2, Curve3, Line2, Line3};
1344 use axiolid_topology::{Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Vertex};
1345 let mut builder = axiolid_brep::ExactBRepBuilder::default();
1346 let surface = builder.add_surface(Surface::Plane(Plane { frame }));
1347 let at = |p: Point2| frame.origin + frame.x * p.x + frame.y * p.y;
1348 let vertices: Vec<_> = corners
1349 .iter()
1350 .map(|p| {
1351 builder
1352 .topology_mut()
1353 .add_vertex(Vertex { position: at(*p) })
1354 })
1355 .collect();
1356 let n = corners.len();
1357 let mut uses = Vec::new();
1358 let mut lengths = Vec::new();
1359 for i in 0..n {
1360 let (p, q) = (corners[i], corners[(i + 1) % n]);
1361 let length = (q - p).length();
1362 let d = Vec2::new((q.x - p.x) / length, (q.y - p.y) / length);
1363 let curve = builder.add_curve3(Curve3::Line(Line3 {
1364 origin: at(p),
1365 direction: frame.x * d.x + frame.y * d.y,
1366 }));
1367 let pcurve = builder.add_curve2(Curve2::Line(Line2 {
1368 origin: p,
1369 direction: d,
1370 }));
1371 let edge = builder.topology_mut().add_edge(Edge {
1372 start: vertices[i],
1373 end: vertices[(i + 1) % n],
1374 curve: Some(curve),
1375 });
1376 builder.set_edge_interval(edge, Interval::new(0.0, length));
1377 uses.push(EdgeUse {
1378 edge,
1379 orientation: Orientation::Forward,
1380 pcurve: Some(pcurve),
1381 });
1382 lengths.push(length);
1383 }
1384 let loop_id = builder.topology_mut().add_loop(Loop { edges: uses });
1385 for (index, length) in lengths.into_iter().enumerate() {
1386 builder.set_pcurve_interval(loop_id, index, Interval::new(0.0, length));
1387 }
1388 builder.topology_mut().add_face(Face {
1389 surface: Some(surface),
1390 bounds: vec![FaceBound {
1391 loop_id,
1392 orientation: Orientation::Forward,
1393 outer: true,
1394 }],
1395 orientation: Orientation::Forward,
1396 });
1397 builder.finish().expect("a valid sheet")
1398 }
1399
1400 #[test]
1407 fn a_far_charted_face_never_lifts_the_lower_bound_past_the_distance() {
1408 let x = Vec3::new(0.375, 0.5, 0.0);
1409 let frame = |origin: Point3| Frame3 {
1410 origin,
1411 x,
1412 y: Vec3::Z,
1413 z: x.cross(Vec3::Z),
1414 };
1415 let b = charted_sheet(
1416 frame(Point3::new(0.25, -0.1875, 0.0)),
1417 &[
1418 Point2::new(-1.0, -1.0),
1419 Point2::new(4.0, -1.0),
1420 Point2::new(4.0, 2.0),
1421 Point2::new(-1.0, 2.0),
1422 ],
1423 );
1424 for k in 1..=16_i32 {
1425 let along = 1.6e6 * f64::from(k);
1426 let a = charted_sheet(
1427 frame(Point3::ZERO + x * along),
1428 &[
1429 Point2::new(-along, 0.0),
1430 Point2::new(-along + 1.6, 0.0),
1431 Point2::new(-along + 1.6, 1.0),
1432 Point2::new(-along, 1.0),
1433 ],
1434 );
1435 let found = super::one_sided_boundary_hausdorff_with_budget(
1436 &a,
1437 &b,
1438 1e-9,
1439 axiolid_core::Tolerance::METRE,
1440 16,
1441 )
1442 .expect("bounded");
1443 assert!(
1444 found.lower <= 0.3125 && 0.3125 <= found.upper,
1445 "{k}: [{}, {}]",
1446 found.lower,
1447 found.upper
1448 );
1449 assert!(found.lower >= 0.3125 - 2e-7, "{k}: {}", found.lower);
1450 }
1451 }
1452
1453 fn spline_sheet(interior: [[f64; 2]; 2], weight: f64) -> axiolid_brep::ExactBRep {
1458 use axiolid_core::{Interval, Vec2};
1459 use axiolid_curve::{Curve2, Curve3, Line2, Line3};
1460 use axiolid_topology::{Edge, EdgeUse, Face, FaceBound, Loop, Orientation, Vertex};
1461 let mut points = vec![vec![Point3::ZERO; 4]; 4];
1462 let mut weights = vec![vec![1.0; 4]; 4];
1463 for (i, column) in points.iter_mut().enumerate() {
1464 for (j, point) in column.iter_mut().enumerate() {
1465 let height = if (1..3).contains(&i) && (1..3).contains(&j) {
1466 interior[i - 1][j - 1]
1467 } else {
1468 0.0
1469 };
1470 *point = Point3::new(i as f64 / 3.0, j as f64 / 3.0, height);
1471 }
1472 }
1473 weights[1][1] = weight;
1474 let net = BSplineSurface {
1475 u_degree: 3,
1476 v_degree: 3,
1477 control_points: points,
1478 u_knots: vec![0.0, 1.0],
1479 u_multiplicities: vec![4, 4],
1480 v_knots: vec![0.0, 1.0],
1481 v_multiplicities: vec![4, 4],
1482 weights: Some(weights),
1483 u_closed: false,
1484 v_closed: false,
1485 knot_spec: KnotSpec::Unspecified,
1486 self_intersect: None,
1487 };
1488 let mut builder = axiolid_brep::ExactBRepBuilder::default();
1489 let surface = builder.add_surface(Surface::BSpline(net));
1490 let corners = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
1491 let directions = [(1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0)];
1492 let vertices: Vec<_> = corners
1493 .iter()
1494 .map(|c| {
1495 builder.topology_mut().add_vertex(Vertex {
1496 position: Point3::new(c.0, c.1, 0.0),
1497 })
1498 })
1499 .collect();
1500 let mut uses = Vec::new();
1501 for i in 0..4 {
1502 let (p, d) = (corners[i], directions[i]);
1503 let curve = builder.add_curve3(Curve3::Line(Line3 {
1504 origin: Point3::new(p.0, p.1, 0.0),
1505 direction: Vec3::new(d.0, d.1, 0.0),
1506 }));
1507 let pcurve = builder.add_curve2(Curve2::Line(Line2 {
1508 origin: Point2::new(p.0, p.1),
1509 direction: Vec2::new(d.0, d.1),
1510 }));
1511 let edge = builder.topology_mut().add_edge(Edge {
1512 start: vertices[i],
1513 end: vertices[(i + 1) % 4],
1514 curve: Some(curve),
1515 });
1516 builder.set_edge_interval(edge, Interval::new(0.0, 1.0));
1517 uses.push(EdgeUse {
1518 edge,
1519 orientation: Orientation::Forward,
1520 pcurve: Some(pcurve),
1521 });
1522 }
1523 let loop_id = builder.topology_mut().add_loop(Loop { edges: uses });
1524 for i in 0..4 {
1525 builder.set_pcurve_interval(loop_id, i, Interval::new(0.0, 1.0));
1526 }
1527 builder.topology_mut().add_face(Face {
1528 surface: Some(surface),
1529 bounds: vec![FaceBound {
1530 loop_id,
1531 orientation: Orientation::Forward,
1532 outer: true,
1533 }],
1534 orientation: Orientation::Forward,
1535 });
1536 builder.finish().expect("a valid sheet")
1537 }
1538
1539 fn small_sheet() -> axiolid_brep::ExactBRep {
1541 charted_sheet(
1542 Frame3 {
1543 origin: Point3::ZERO,
1544 x: Vec3::X,
1545 y: Vec3::Y,
1546 z: Vec3::Z,
1547 },
1548 &[
1549 Point2::new(0.25, 0.25),
1550 Point2::new(0.4, 0.25),
1551 Point2::new(0.4, 0.4),
1552 Point2::new(0.25, 0.4),
1553 ],
1554 )
1555 }
1556
1557 #[test]
1563 fn a_spline_patch_is_bounded_about_its_witness_not_its_net_centre() {
1564 let b = small_sheet();
1565 let a = spline_sheet([[0.05, 0.1], [-0.05, 0.3]], 1000.0);
1566 let h = 0.6 * 2.0_f64.sqrt();
1567 for budget in 0..4 {
1568 let found = super::one_sided_boundary_hausdorff_with_budget(
1569 &a,
1570 &b,
1571 1e-9,
1572 axiolid_core::Tolerance::METRE,
1573 budget,
1574 )
1575 .expect("bounded");
1576 assert!(
1577 found.lower <= h && h <= found.upper,
1578 "{budget}: [{}, {}] must contain {h}",
1579 found.lower,
1580 found.upper
1581 );
1582 }
1583 }
1584
1585 #[test]
1586 fn a_witness_is_off_its_patch_centre_only_on_a_spline() {
1587 use super::{witness_offset, Metric, Shape, Side};
1588 let tolerance = axiolid_core::Tolerance::METRE.linear();
1589 let offsets = |brep: &axiolid_brep::ExactBRep| -> Vec<f64> {
1590 let side = Side::new(brep, tolerance, Metric::Space).expect("a side");
1591 side.elements
1592 .iter()
1593 .filter(|e| matches!(e.shape, Shape::Face { .. }))
1594 .map(|e| witness_offset(e, e.witness.expect("a face witness")))
1595 .collect()
1596 };
1597 assert_eq!(offsets(&small_sheet()), [0.0]);
1598 let spline = offsets(&spline_sheet([[0.05, 0.1], [-0.05, 0.3]], 1000.0));
1599 assert_eq!(spline.len(), 1);
1600 assert!(spline[0] > 0.2, "{spline:?}");
1601 }
1602}