1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
23use ogeom_math::{Aabb, Direction, Point, Point2, Vector};
24use ogeom_mesh::{
25 Deflection, face_boundary_in_chart, inside_boundary, open_chart_ring, triangulate,
26};
27use ogeom_topo::{Model, NodeData, Shape, ShapeType};
28
29use crate::measure::project_on_surface;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Containment {
34 In,
36 On,
38 Out,
40}
41
42impl Containment {
43 #[must_use]
45 pub const fn is_inside_or_on(self) -> bool {
46 matches!(self, Self::In | Self::On)
47 }
48
49 #[must_use]
54 pub const fn inverted(self) -> Self {
55 match self {
56 Self::In => Self::Out,
57 Self::On => Self::On,
58 Self::Out => Self::In,
59 }
60 }
61}
62
63pub fn classify_on_face(
76 model: &Model,
77 face: &Shape,
78 point: Point,
79 deflection: Deflection,
80 tol: Tolerances,
81) -> OgeomResult<Containment> {
82 deflection.validate()?;
83 if model.kind_of(face)? != ShapeType::Face {
84 ogeom_bail!(Construction, "expected a face");
85 }
86 let Some(node) = model.node(face) else {
87 ogeom_bail!(Dangling, "face is not in this model");
88 };
89 let NodeData::Face(data) = node.data() else {
90 ogeom_bail!(Construction, "face node holds no face data");
91 };
92 let Some(surface) = model.geometry().surface(data.surface) else {
93 ogeom_bail!(Dangling, "face refers to a surface not in this model");
94 };
95
96 let placement = face.transform(model.datums())?;
99 let local = placement.inverse()?.apply(point);
100
101 let projection = project_on_surface(surface, local, 32, tol)?;
105 let reach = tol.confusion().max(data.tolerance.get());
106 if projection.distance > reach {
107 return Ok(Containment::Out);
108 }
109
110 let rings = Rings::of(model, face, surface, deflection, tol)?;
113 let (u, v) = projection.parameters;
114 let at = rings.place(surface, Point2::new(u, v), tol);
115 let band = parametric_band(surface, (u, v), reach + deflection.chord, tol);
123 if rings.within(model, face, deflection, at, band, tol)? {
124 return Ok(Containment::On);
125 }
126 Ok(if rings.inside(model, face, deflection, at, tol)? {
127 Containment::In
128 } else {
129 Containment::Out
130 })
131}
132
133pub struct FaceTrim<'m> {
136 face: &'m Shape,
137 surface: &'m ogeom_geom::SurfaceGeometry,
138 rings: Rings,
139 deflection: Deflection,
140}
141
142impl core::fmt::Debug for FaceTrim<'_> {
143 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
144 f.debug_struct("FaceTrim")
145 .field("face", self.face)
146 .field("deflection", &self.deflection)
147 .finish_non_exhaustive()
148 }
149}
150
151impl<'m> FaceTrim<'m> {
152 pub fn of(
158 model: &'m Model,
159 face: &'m Shape,
160 deflection: Deflection,
161 tol: Tolerances,
162 ) -> OgeomResult<Self> {
163 deflection.validate()?;
164 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
165 ogeom_bail!(Construction, "expected a face");
166 };
167 let Some(surface) = model.geometry().surface(data.surface) else {
168 ogeom_bail!(Dangling, "face refers to a surface not in this model");
169 };
170 let rings = Rings::of(model, face, surface, deflection, tol)?;
171 Ok(Self {
172 face,
173 surface,
174 rings,
175 deflection,
176 })
177 }
178
179 pub fn holds(
186 &self,
187 model: &Model,
188 at: (f64, f64),
189 reach: f64,
190 tol: Tolerances,
191 ) -> OgeomResult<Containment> {
192 let placed = self.rings.place(self.surface, Point2::new(at.0, at.1), tol);
193 let band = parametric_band(self.surface, at, reach + self.deflection.chord, tol);
194 if self
195 .rings
196 .within(model, self.face, self.deflection, placed, band, tol)?
197 {
198 return Ok(Containment::On);
199 }
200 Ok(
201 if self
202 .rings
203 .inside(model, self.face, self.deflection, placed, tol)?
204 {
205 Containment::In
206 } else {
207 Containment::Out
208 },
209 )
210 }
211}
212
213pub fn classify_in_solid(
230 model: &Model,
231 solid: &Shape,
232 point: Point,
233 deflection: Deflection,
234 tol: Tolerances,
235) -> OgeomResult<Containment> {
236 SolidMesh::of(model, solid, deflection, tol)?.holds(point, tol)
237}
238
239#[derive(Debug, Clone)]
246pub struct SolidMesh {
247 triangles: Vec<[Point; 3]>,
248 bound: ogeom_math::Aabb,
249 reach: f64,
250}
251
252impl SolidMesh {
253 pub fn of(
259 model: &Model,
260 solid: &Shape,
261 deflection: Deflection,
262 tol: Tolerances,
263 ) -> OgeomResult<Self> {
264 deflection.validate()?;
265 let mesh = triangulate(model, solid, deflection, tol)?;
266 if mesh.is_empty() || !mesh.is_closed() {
267 ogeom_bail!(
268 Construction,
269 "the boundary is not closed, so there is no inside to be in"
270 );
271 }
272 let triangles: Vec<[Point; 3]> = mesh
273 .triangles
274 .iter()
275 .map(|t| t.map(|i| mesh.positions[i as usize]))
276 .collect();
277 let bound = ogeom_math::Aabb::of_points(&mesh.positions);
278 Ok(Self {
279 triangles,
280 bound,
281 reach: tol.confusion() + deflection.chord,
282 })
283 }
284
285 pub fn holds(&self, point: Point, tol: Tolerances) -> OgeomResult<Containment> {
293 if !self.bound.expanded(self.reach).contains(point) {
296 return Ok(Containment::Out);
297 }
298 for t in &self.triangles {
302 if distance_to_triangle(point, *t) <= self.reach {
303 return Ok(Containment::On);
304 }
305 }
306 for direction in RAY_DIRECTIONS {
311 let ray = Direction::new(Vector::new(direction[0], direction[1], direction[2]), tol)?;
312 if let Some(crossings) = count_crossings(&self.triangles, point, ray, tol) {
313 return Ok(if crossings % 2 == 1 {
314 Containment::In
315 } else {
316 Containment::Out
317 });
318 }
319 }
320 if let Some(read) =
324 crate::winding::reading(crate::winding::winding_number(point, self.triangles.iter()))
325 {
326 return Ok(read);
327 }
328 ogeom_bail!(
329 NotDone,
330 "every ray tried met an edge or a vertex, where the crossing count is \
331 ambiguous, and the winding number is not near zero or one"
332 )
333 }
334}
335
336pub fn classify_in_solid_exact(
365 model: &Model,
366 solid: &Shape,
367 point: Point,
368 tol: Tolerances,
369) -> OgeomResult<Containment> {
370 classify_in_solid_exact_banded(model, solid, point, exact_ring_chord(tol), tol)
371}
372
373pub(crate) fn exact_ring_chord(tol: Tolerances) -> f64 {
375 tol.confusion() * 1e4
376}
377
378pub fn classify_in_solid_exact_banded(
391 model: &Model,
392 solid: &Shape,
393 point: Point,
394 ring_chord: f64,
395 tol: Tolerances,
396) -> OgeomResult<Containment> {
397 SolidBoundary::of(model, solid, ring_chord, tol)?.holds(model, point, tol)
398}
399
400#[derive(Debug)]
403struct PreparedFace {
404 placed: std::sync::OnceLock<OgeomResult<(ogeom_geom::SurfaceGeometry, ogeom_math::Transform)>>,
408 face: Shape,
410 rings: std::sync::OnceLock<OgeomResult<Rings>>,
415 fine_rings: std::sync::OnceLock<OgeomResult<Rings>>,
418 surface: ogeom_topo::SurfaceId,
421 bound: Aabb,
424 own: Aabb,
426 reach: f64,
429 rim: std::sync::OnceLock<OgeomResult<f64>>,
432}
433
434const NEAR_TRIM: f64 = 50.0;
437
438#[derive(Debug)]
440enum Rings {
441 All(Vec<Vec<Point2>>),
443 Open {
447 outer: Vec<Point2>,
448 holes: Vec<Hole>,
449 },
450}
451
452#[derive(Debug)]
454struct Hole {
455 wire: Shape,
456 low: Point2,
458 high: Point2,
459 ring: std::sync::OnceLock<OgeomResult<Option<Vec<Point2>>>>,
460}
461
462impl Hole {
463 fn ring(
464 &self,
465 model: &Model,
466 face: &Shape,
467 deflection: Deflection,
468 tol: Tolerances,
469 ) -> OgeomResult<Option<&[Point2]>> {
470 match self
471 .ring
472 .get_or_init(|| open_chart_ring(model, face, &self.wire, false, deflection, tol))
473 {
474 Ok(ring) => Ok(ring.as_deref()),
475 Err(e) => Err(e.clone()),
476 }
477 }
478
479 fn box_distance(&self, p: Point2) -> f64 {
481 let dx = (self.low.x - p.x).max(p.x - self.high.x).max(0.0);
482 let dy = (self.low.y - p.y).max(p.y - self.high.y).max(0.0);
483 dx.hypot(dy)
484 }
485}
486
487impl Rings {
488 fn of(
493 model: &Model,
494 face: &Shape,
495 surface: &ogeom_geom::SurfaceGeometry,
496 deflection: Deflection,
497 tol: Tolerances,
498 ) -> OgeomResult<Self> {
499 let all = || face_boundary_in_chart(model, face, deflection, tol).map(Rings::All);
500 let Some(NodeData::Face(data)) = model.node(face).map(|n| n.data()) else {
501 return all();
502 };
503 let wires = model.children_of(face)?;
506 if !matches!(surface, ogeom_geom::SurfaceGeometry::Plane(_)) || wires.len() < 2 {
507 return all();
508 }
509 let Some(outer) = open_chart_ring(model, face, &wires[0], true, deflection, tol)? else {
510 return all();
511 };
512 let mut area = 0.0;
515 if let Some(&anchor) = outer.first() {
516 for i in 0..outer.len() {
517 area += (outer[i] - anchor).cross(outer[(i + 1) % outer.len()] - anchor);
518 }
519 }
520 let area = area.abs() * 0.5;
521 let mut holes = Vec::with_capacity(wires.len() - 1);
522 for wire in &wires[1..] {
523 let Some((low, high)) = chart_box(model, data.surface, wire, tol) else {
524 return all();
525 };
526 if (high.x - low.x) * (high.y - low.y) >= area {
527 return all();
528 }
529 holes.push(Hole {
530 wire: wire.clone(),
531 low,
532 high,
533 ring: std::sync::OnceLock::new(),
534 });
535 }
536 Ok(Rings::Open { outer, holes })
537 }
538
539 fn place(&self, surface: &ogeom_geom::SurfaceGeometry, at: Point2, tol: Tolerances) -> Point2 {
541 match self {
542 Rings::All(rings) => place_on_rings(surface, rings, at, tol),
543 Rings::Open { .. } => at,
544 }
545 }
546
547 fn within(
549 &self,
550 model: &Model,
551 face: &Shape,
552 deflection: Deflection,
553 at: Point2,
554 band: ChartBand,
555 tol: Tolerances,
556 ) -> OgeomResult<bool> {
557 match self {
558 Rings::All(rings) => Ok(band.meets_rings(rings, at)),
559 Rings::Open { outer, holes } => {
560 if band.meets_ring(outer, at) {
561 return Ok(true);
562 }
563 for hole in holes {
564 if hole.box_distance(at) <= band.radius()
565 && let Some(ring) = hole.ring(model, face, deflection, tol)?
566 && band.meets_ring(ring, at)
567 {
568 return Ok(true);
569 }
570 }
571 Ok(false)
572 }
573 }
574 }
575
576 fn inside(
578 &self,
579 model: &Model,
580 face: &Shape,
581 deflection: Deflection,
582 at: Point2,
583 tol: Tolerances,
584 ) -> OgeomResult<bool> {
585 match self {
586 Rings::All(rings) => Ok(inside_boundary(rings, at)),
587 Rings::Open { outer, holes } => {
588 let mut inside = inside_boundary(core::slice::from_ref(outer), at);
589 for hole in holes {
590 if hole.box_distance(at) <= 0.0
591 && let Some(ring) = hole.ring(model, face, deflection, tol)?
592 && inside_boundary(&[ring.to_vec()], at)
593 {
594 inside = !inside;
595 }
596 }
597 Ok(inside)
598 }
599 }
600 }
601}
602
603fn chart_box(
606 model: &Model,
607 surface: ogeom_topo::SurfaceId,
608 wire: &Shape,
609 tol: Tolerances,
610) -> Option<(Point2, Point2)> {
611 use ogeom_geom::Curve2d as _;
612 use ogeom_geom::PlanarCurve;
613 let mut low = Point2::new(f64::INFINITY, f64::INFINITY);
614 let mut high = Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY);
615 let mut take = |p: Point2, rx: f64, ry: f64| {
616 low = Point2::new(low.x.min(p.x - rx), low.y.min(p.y - ry));
617 high = Point2::new(high.x.max(p.x + rx), high.y.max(p.y + ry));
618 };
619 let mut loose = tol.confusion();
623 for edge in model.ordered_children_of(wire).ok()? {
624 let data = model.node(&edge)?.data().as_edge()?;
625 loose = loose.max(data.tolerance.get());
626 for vertex in model.children_of(&edge).ok()? {
627 if let Some(v) = model.node(&vertex).and_then(|n| n.data().as_vertex()) {
628 loose = loose.max(v.tolerance.get());
629 }
630 }
631 let ogeom_topo::EdgeRepr::PCurve { curve, range, .. } =
632 data.pcurve_for(surface, edge.location())?
633 else {
634 return None;
635 };
636 match model.geometry().pcurve(*curve)? {
637 line @ PlanarCurve::Line(_) => {
638 take(line.point_at(range.0, tol).ok()?, 0.0, 0.0);
639 take(line.point_at(range.1, tol).ok()?, 0.0, 0.0);
640 }
641 PlanarCurve::Circle(arc) => {
642 let circle = arc.circle();
643 let (x, y) = (circle.frame().x().vector(), circle.frame().y().vector());
644 let r = circle.radius();
645 take(
646 circle.centre(),
647 r * (x.x.abs() + y.x.abs()),
648 r * (x.y.abs() + y.y.abs()),
649 );
650 }
651 _ => return None,
652 }
653 }
654 if !(low.x.is_finite() && low.y.is_finite() && high.x.is_finite() && high.y.is_finite()) {
655 return None;
656 }
657 let slack = loose.mul_add(
660 4.0,
661 1e-9 * (1.0
662 + low
663 .x
664 .abs()
665 .max(low.y.abs())
666 .max(high.x.abs())
667 .max(high.y.abs())),
668 );
669 Some((
670 Point2::new(low.x - slack, low.y - slack),
671 Point2::new(high.x + slack, high.y + slack),
672 ))
673}
674
675impl PreparedFace {
676 fn placed(
678 &self,
679 model: &Model,
680 ) -> OgeomResult<&(ogeom_geom::SurfaceGeometry, ogeom_math::Transform)> {
681 let read = || -> OgeomResult<(ogeom_geom::SurfaceGeometry, ogeom_math::Transform)> {
682 let Some(NodeData::Face(data)) = model.node(&self.face).map(|n| n.data()) else {
683 ogeom_bail!(Dangling, "face is not in this model");
684 };
685 let Some(surface) = model.geometry().surface(data.surface) else {
686 ogeom_bail!(Dangling, "face refers to a surface not in this model");
687 };
688 let inverse = self.face.transform(model.datums())?.inverse()?;
689 Ok((surface.clone(), inverse))
690 };
691 match self.placed.get_or_init(read) {
692 Ok(placed) => Ok(placed),
693 Err(e) => Err(e.clone()),
694 }
695 }
696
697 fn rings(&self, model: &Model, deflection: Deflection, tol: Tolerances) -> OgeomResult<&Rings> {
699 let (surface, _) = self.placed(model)?;
700 match self
701 .rings
702 .get_or_init(|| Rings::of(model, &self.face, surface, deflection, tol))
703 {
704 Ok(rings) => Ok(rings),
705 Err(e) => Err(e.clone()),
706 }
707 }
708
709 fn fine_rings(
719 &self,
720 model: &Model,
721 deflection: Deflection,
722 tol: Tolerances,
723 ) -> OgeomResult<&Rings> {
724 let (surface, _) = self.placed(model)?;
725 let fine = Deflection {
726 chord: deflection.chord * 0.1,
727 ..deflection
728 };
729 match self
730 .fine_rings
731 .get_or_init(|| Rings::of(model, &self.face, surface, fine, tol))
732 {
733 Ok(rings) => Ok(rings),
734 Err(e) => Err(e.clone()),
735 }
736 }
737
738 fn rim(&self, model: &Model) -> OgeomResult<f64> {
745 let read = || -> OgeomResult<f64> {
746 let mut widest = 0.0_f64;
747 for kind in [ShapeType::Edge, ShapeType::Vertex] {
748 for shape in ogeom_topo::explore_unique(model, &self.face, kind)? {
749 widest = widest.max(match model.node(&shape).map(|n| n.data()) {
750 Some(NodeData::Edge(data)) => data.tolerance.get(),
751 Some(NodeData::Vertex(data)) => data.tolerance.get(),
752 _ => 0.0,
753 });
754 }
755 }
756 Ok(widest)
757 };
758 match self.rim.get_or_init(read) {
759 Ok(rim) => Ok(*rim),
760 Err(e) => Err(e.clone()),
761 }
762 }
763
764 fn holds(
768 &self,
769 model: &Model,
770 point: Point,
771 deflection: Deflection,
772 tol: Tolerances,
773 ) -> OgeomResult<Containment> {
774 let (surface, inverse) = self.placed(model)?;
775 let local = inverse.apply(point);
776 let projection = project_on_surface(surface, local, 32, tol)?;
777 if projection.distance > self.reach {
778 return Ok(Containment::Out);
779 }
780 let rings = self.rings(model, deflection, tol)?;
782 let (u, v) = projection.parameters;
783 let at = rings.place(surface, Point2::new(u, v), tol);
784 let band = parametric_band(surface, (u, v), self.reach + deflection.chord, tol);
785 if rings.within(model, &self.face, deflection, at, band, tol)? {
786 return Ok(Containment::On);
787 }
788 Ok(if rings.inside(model, &self.face, deflection, at, tol)? {
789 Containment::In
790 } else {
791 Containment::Out
792 })
793 }
794}
795
796#[derive(Debug)]
805pub struct SolidBoundary {
806 prepared: std::sync::Arc<Prepared>,
809 rays: Vec<std::sync::OnceLock<Option<std::sync::Arc<KeptSurface>>>>,
813 kept: ProbeCache,
816}
817
818#[derive(Debug)]
825struct Prepared {
826 faces: Vec<std::sync::Arc<PreparedFace>>,
827 tol: Tolerances,
830 bound: ogeom_math::Aabb,
831 centre: Point,
832 diagonal: f64,
833 ring_chord: f64,
834 checked: bool,
836 solid: Shape,
839 winding: std::sync::OnceLock<Option<std::sync::Arc<crate::winding::WindingMesh>>>,
842 places: std::sync::OnceLock<ogeom_core::FastMap<ogeom_topo::SameKey, u32>>,
844 boxes: std::sync::OnceLock<FaceBoxes>,
846}
847
848impl Prepared {
849 fn boxes(&self) -> &FaceBoxes {
851 self.boxes.get_or_init(|| {
852 let bounds: Vec<Aabb> = self.faces.iter().map(|f| f.bound).collect();
853 FaceBoxes::of(&bounds)
854 })
855 }
856
857 fn face_of(&self, face: &Shape) -> Option<&std::sync::Arc<PreparedFace>> {
859 let places = self.places.get_or_init(|| {
860 self.faces
861 .iter()
862 .enumerate()
863 .map(|(i, f)| {
864 let at = u32::try_from(i).unwrap_or(u32::MAX);
865 (ogeom_topo::SameKey(f.face.clone()), at)
866 })
867 .collect()
868 });
869 let at = *places.get(&ogeom_topo::SameKey(face.clone()))?;
870 let prepared = self.faces.get(at as usize)?;
871 (prepared.face == *face).then_some(prepared)
872 }
873
874 fn same(&self, other: &Self) -> bool {
876 self.faces.len() == other.faces.len()
877 && self.faces.iter().zip(&other.faces).all(|(a, b)| {
878 a.face == b.face
879 && a.surface == b.surface
880 && a.bound == b.bound
881 && a.own == b.own
882 && a.reach.to_bits() == b.reach.to_bits()
883 })
884 && self.tol == other.tol
885 && self.bound == other.bound
886 && self.centre == other.centre
887 && self.diagonal.to_bits() == other.diagonal.to_bits()
888 && self.ring_chord.to_bits() == other.ring_chord.to_bits()
889 && self.checked == other.checked
890 && self.solid == other.solid
891 }
892}
893
894const KEPT_FACES: usize = 64;
898
899impl SolidBoundary {
900 pub fn of(model: &Model, solid: &Shape, ring_chord: f64, tol: Tolerances) -> OgeomResult<Self> {
906 Self::prepare(model, solid, ring_chord, true, tol)
907 }
908
909 pub fn of_closed(
917 model: &Model,
918 solid: &Shape,
919 ring_chord: f64,
920 tol: Tolerances,
921 ) -> OgeomResult<Self> {
922 Self::prepare(model, solid, ring_chord, false, tol)
923 }
924
925 pub fn of_kept(
942 model: &Model,
943 solid: &Shape,
944 ring_chord: f64,
945 closed: bool,
946 tol: Tolerances,
947 ) -> OgeomResult<Self> {
948 let check_closed = !closed;
949 let prepared = model.kept_read(
950 solid,
951 |p: &Prepared| {
952 p.ring_chord.to_bits() == ring_chord.to_bits()
953 && p.tol == tol
954 && (p.checked || !check_closed)
955 },
956 || {
957 let kept = model.kept_reads(|p: &Prepared| {
960 p.ring_chord.to_bits() == ring_chord.to_bits() && p.tol == tol
961 });
962 let (prepared, boxed, taken) =
963 Self::prepared(model, solid, ring_chord, check_closed, tol, &kept)?;
964 if taken > 0 {
965 crate::derived::note_derived(taken);
966 if crate::derived::checks_derived_reads() {
967 let (fresh, fresh_boxed, _) =
968 Self::prepared(model, solid, ring_chord, check_closed, tol, &[])?;
969 assert!(
970 boxed == fresh_boxed && prepared.same(&fresh),
971 "a boundary prepared from kept faces differs from its fresh one"
972 );
973 }
974 }
975 let keep = boxed && prepared.faces.len() >= KEPT_FACES;
976 Ok((prepared, keep))
977 },
978 )?;
979 Ok(Self::sharing(prepared))
980 }
981
982 fn sharing(prepared: std::sync::Arc<Prepared>) -> Self {
984 let rays = (0..prepared.faces.len())
985 .map(|_| std::sync::OnceLock::new())
986 .collect();
987 Self {
988 prepared,
989 rays,
990 kept: ProbeCache::default(),
991 }
992 }
993
994 fn prepare(
995 model: &Model,
996 solid: &Shape,
997 ring_chord: f64,
998 check_closed: bool,
999 tol: Tolerances,
1000 ) -> OgeomResult<Self> {
1001 let (prepared, _, _) = Self::prepared(model, solid, ring_chord, check_closed, tol, &[])?;
1002 Ok(Self::sharing(std::sync::Arc::new(prepared)))
1003 }
1004
1005 fn prepared(
1013 model: &Model,
1014 solid: &Shape,
1015 ring_chord: f64,
1016 check_closed: bool,
1017 tol: Tolerances,
1018 kept: &[std::sync::Arc<Prepared>],
1019 ) -> OgeomResult<(Prepared, bool, usize)> {
1020 let kind = model.kind_of(solid)?;
1021 let lumps = kind == ShapeType::Compound
1024 && model
1025 .children_of(solid)?
1026 .iter()
1027 .all(|part| model.kind_of(part).is_ok_and(|k| k == ShapeType::Solid));
1028 if !matches!(kind, ShapeType::Solid | ShapeType::Shell) && !lumps {
1029 ogeom_bail!(Construction, "expected a solid or a shell, got {kind:?}");
1030 }
1031 let shells = if kind == ShapeType::Shell {
1032 vec![solid.clone()]
1033 } else {
1034 ogeom_topo::explore_unique(model, solid, ShapeType::Shell)?
1035 };
1036 if shells.is_empty() {
1037 ogeom_bail!(Construction, "the shape has no shell, so no boundary");
1038 }
1039 for shell in shells.iter().filter(|_| check_closed) {
1040 if !crate::build::is_shell_closed(model, shell)? {
1041 ogeom_bail!(
1042 Construction,
1043 "the boundary is not closed, so there is no inside to be in"
1044 );
1045 }
1046 }
1047
1048 let faces = ogeom_topo::explore_unique(model, solid, ShapeType::Face)?;
1049 let prepare = |face: &Shape| -> OgeomResult<std::sync::Arc<PreparedFace>> {
1052 ogeom_core::progress::checkpoint()?;
1053 let Some(node) = model.node(face) else {
1054 ogeom_bail!(Dangling, "face is not in this model");
1055 };
1056 let NodeData::Face(data) = node.data() else {
1057 ogeom_bail!(Construction, "face node holds no face data");
1058 };
1059 let Some(surface) = model.geometry().surface(data.surface) else {
1060 ogeom_bail!(Dangling, "face refers to a surface not in this model");
1061 };
1062 let own = match model.face_bounds(face) {
1066 Some(kept) if closed_form(surface) => kept,
1067 Some(kept) => kept.expanded(Deflection::default().chord),
1068 None => crate::measure::shape_bounds(model, face, tol)?,
1069 };
1070 let bound = own.expanded(
1071 ring_chord + data.tolerance.get() + tol.confusion() * 1e2 + own.diagonal() * 0.02,
1072 );
1073 Ok(std::sync::Arc::new(PreparedFace {
1074 placed: std::sync::OnceLock::new(),
1075 face: face.clone(),
1076 rings: std::sync::OnceLock::new(),
1077 fine_rings: std::sync::OnceLock::new(),
1078 surface: data.surface,
1079 bound,
1080 own,
1081 reach: tol.confusion().max(data.tolerance.get()),
1082 rim: std::sync::OnceLock::new(),
1083 }))
1084 };
1085 let boxed = faces.iter().all(|face| model.face_bounds(face).is_some());
1089 let shared: Vec<Option<std::sync::Arc<PreparedFace>>> = faces
1090 .iter()
1091 .map(|face| kept.iter().find_map(|k| k.face_of(face)).cloned())
1092 .collect();
1093 let taken = shared.iter().flatten().count();
1094 let each = |face: &Shape, shared: &Option<std::sync::Arc<PreparedFace>>| match shared {
1095 Some(shared) => Ok(std::sync::Arc::clone(shared)),
1096 None => prepare(face),
1097 };
1098 let prepared = if boxed {
1099 faces
1100 .iter()
1101 .zip(&shared)
1102 .map(|(face, shared)| each(face, shared))
1103 .collect::<OgeomResult<Vec<_>>>()?
1104 } else {
1105 ogeom_core::parallel::map_ordered(&faces, |i, face| each(face, &shared[i]))
1106 .into_iter()
1107 .collect::<OgeomResult<Vec<_>>>()?
1108 };
1109 let bound = prepared
1114 .iter()
1115 .fold(Aabb::EMPTY, |bound, face| bound.union(&face.own));
1116 let (Some(centre), diagonal) = (bound.centre(), bound.diagonal()) else {
1117 ogeom_bail!(Construction, "the boundary bounds nothing");
1118 };
1119 Ok((
1120 Prepared {
1121 faces: prepared,
1122 tol,
1123 bound,
1124 centre,
1125 diagonal,
1126 ring_chord,
1127 checked: check_closed,
1128 solid: solid.clone(),
1129 winding: std::sync::OnceLock::new(),
1130 places: std::sync::OnceLock::new(),
1131 boxes: std::sync::OnceLock::new(),
1132 },
1133 boxed,
1134 taken,
1135 ))
1136 }
1137
1138 pub(crate) const fn kept(&self) -> &ProbeCache {
1140 &self.kept
1141 }
1142
1143 pub(crate) fn keeping(mut self, kept: &ProbeCache) -> Self {
1145 self.kept = kept.clone();
1146 self
1147 }
1148
1149 pub fn holds(&self, model: &Model, point: Point, tol: Tolerances) -> OgeomResult<Containment> {
1155 let Prepared {
1156 faces,
1157 bound,
1158 ring_chord,
1159 ..
1160 } = &*self.prepared;
1161 let ring_deflection = Deflection {
1162 chord: *ring_chord,
1163 angular: 0.05,
1164 ..Deflection::default()
1165 };
1166 if !bound.expanded(tol.confusion()).contains(point) {
1167 return Ok(Containment::Out);
1168 }
1169
1170 for k in self.prepared.boxes().holding(point) {
1174 let prepared = &faces[k];
1175 if !prepared.bound.contains(point) {
1176 continue;
1177 }
1178 if prepared.holds(model, point, ring_deflection, tol)? != Containment::Out {
1179 return Ok(Containment::On);
1180 }
1181 }
1182 if let Some(read) = self.cast(model, point, tol)? {
1183 return Ok(read);
1184 }
1185 if let Some(read) = self.reached(model, point, tol)? {
1186 return Ok(read);
1187 }
1188 if let Some(read) = self.wound(model, point, tol) {
1189 return Ok(read);
1190 }
1191 ogeom_bail!(
1192 NotDone,
1193 "every ray tried met a tangency, a boundary, or a degenerate point, \
1194 where the crossing count is ambiguous, and the winding number is \
1195 not near zero or one"
1196 )
1197 }
1198
1199 pub(crate) fn clear_between(
1211 &self,
1212 model: &Model,
1213 from: Point,
1214 to: Point,
1215 except: &Shape,
1216 tol: Tolerances,
1217 ) -> OgeomResult<bool> {
1218 self.clear_of(model, (from, to), std::slice::from_ref(except), false, tol)
1219 }
1220
1221 pub fn clear_from(
1234 &self,
1235 model: &Model,
1236 from: Point,
1237 to: Point,
1238 on: &[Shape],
1239 tol: Tolerances,
1240 ) -> OgeomResult<bool> {
1241 self.clear_of(model, (from, to), on, true, tol)
1242 }
1243
1244 fn clear_of(
1247 &self,
1248 model: &Model,
1249 (from, to): (Point, Point),
1250 except: &[Shape],
1251 at_start: bool,
1252 tol: Tolerances,
1253 ) -> OgeomResult<bool> {
1254 let Prepared {
1255 faces, ring_chord, ..
1256 } = &*self.prepared;
1257 let ring_chord = *ring_chord;
1258 let ring_deflection = Deflection {
1259 chord: ring_chord,
1260 angular: 0.05,
1261 ..Deflection::default()
1262 };
1263 let reach = tol.confusion();
1264 for k in self.prepared.boxes().meeting(from, to) {
1265 let (prepared, rays) = (&faces[k], &self.rays[k]);
1266 let excepted = except.iter().any(|face| prepared.face.is_same(face));
1267 if (excepted && !at_start) || !segment_meets(&prepared.bound, from, to) {
1268 continue;
1269 }
1270 let (surface, inverse) = prepared.placed(model)?;
1271 let (start, end) = (inverse.apply(from), inverse.apply(to));
1272 let scale = (end - start).magnitude() / (to - from).magnitude();
1275 let found = match rays.get_or_init(|| {
1276 self.kept
1277 .surface(prepared.surface, surface, self.prepared.tol)
1278 }) {
1279 Some(kept) if kept.tol == tol => kept.cross((start, end))?,
1280 _ => {
1281 let ray: ogeom_geom::Curve =
1282 ogeom_geom::LineCurve::segment(start, end, tol)?.into();
1283 ogeom_intersect::intersect_curve_surface(
1284 &ray,
1285 surface,
1286 ogeom_intersect::CurveSurfaceOptions::default(),
1287 tol,
1288 )?
1289 }
1290 };
1291 if !found.lying.is_empty() {
1292 return Ok(false);
1293 }
1294 for hit in &found.crossings {
1295 if excepted && hit.on_curve <= (reach + ring_chord) * scale {
1296 continue;
1297 }
1298 let (u, v) = hit.on_surface;
1299 let rings = prepared.rings(model, ring_deflection, tol)?;
1300 let at = rings.place(surface, Point2::new(u, v), tol);
1301 let band = parametric_band(surface, (u, v), reach + ring_chord, tol);
1302 let face = &prepared.face;
1303 if rings.within(model, face, ring_deflection, at, band, tol)?
1304 || rings.inside(model, face, ring_deflection, at, tol)?
1305 {
1306 return Ok(false);
1307 }
1308 }
1309 }
1310 Ok(true)
1311 }
1312
1313 pub(crate) fn first_crossing(
1327 &self,
1328 model: &Model,
1329 (from, along): (Point, Vector),
1330 max: f64,
1331 on: &[Shape],
1332 sin: f64,
1333 tol: Tolerances,
1334 ) -> OgeomResult<Option<f64>> {
1335 let Prepared {
1336 faces,
1337 centre,
1338 diagonal,
1339 ring_chord,
1340 ..
1341 } = &*self.prepared;
1342 let ring_chord = *ring_chord;
1343 let ring_deflection = Deflection {
1344 chord: ring_chord,
1345 angular: 0.05,
1346 ..Deflection::default()
1347 };
1348 let reach = tol.confusion();
1349 let length = max.min(from.distance(*centre) + diagonal + 1.0);
1351 if length.is_nan() || length <= 0.0 {
1352 return Ok(None);
1353 }
1354 let to = from + along * length;
1355 let mut first: Option<f64> = None;
1356 for k in self.prepared.boxes().meeting(from, to) {
1357 let (prepared, rays) = (&faces[k], &self.rays[k]);
1358 if !segment_meets(&prepared.bound, from, to) {
1359 continue;
1360 }
1361 let at_start = on.iter().any(|face| prepared.face.is_same(face));
1362 let (surface, inverse) = prepared.placed(model)?;
1363 let (start, end) = (inverse.apply(from), inverse.apply(to));
1364 let scale = (end - start).magnitude() / length;
1367 let direction = (end - start) / (end - start).magnitude();
1368 let found = match rays.get_or_init(|| {
1369 self.kept
1370 .surface(prepared.surface, surface, self.prepared.tol)
1371 }) {
1372 Some(kept) if kept.tol == tol => kept.cross((start, end))?,
1373 _ => {
1374 let ray: ogeom_geom::Curve =
1375 ogeom_geom::LineCurve::segment(start, end, tol)?.into();
1376 ogeom_intersect::intersect_curve_surface(
1377 &ray,
1378 surface,
1379 ogeom_intersect::CurveSurfaceOptions::default(),
1380 tol,
1381 )?
1382 }
1383 };
1384 for hit in &found.crossings {
1385 let distance = hit.on_curve / scale;
1386 if (at_start && hit.on_curve <= (reach + ring_chord) * scale)
1387 || first.is_some_and(|held| distance >= held)
1388 {
1389 continue;
1390 }
1391 let (u, v) = hit.on_surface;
1392 use ogeom_geom::Surface as _;
1393 if let Ok((du, dv)) = surface.d1_at(u, v, tol) {
1394 let normal = du.cross(dv);
1395 let size = normal.magnitude();
1396 if size > tol.confusion() && normal.dot(direction).abs() <= sin * size {
1398 continue;
1399 }
1400 }
1401 let rings = prepared.rings(model, ring_deflection, tol)?;
1402 let at = rings.place(surface, Point2::new(u, v), tol);
1403 let band = parametric_band(surface, (u, v), reach + ring_chord, tol);
1404 let face = &prepared.face;
1405 if rings.within(model, face, ring_deflection, at, band, tol)?
1406 || rings.inside(model, face, ring_deflection, at, tol)?
1407 {
1408 first = Some(distance);
1409 }
1410 }
1411 }
1412 Ok(first)
1413 }
1414
1415 fn cast(
1419 &self,
1420 model: &Model,
1421 point: Point,
1422 tol: Tolerances,
1423 ) -> OgeomResult<Option<Containment>> {
1424 Ok(self.crossed(model, point, tol)?.map(|(crossings, _)| {
1425 if crossings % 2 == 1 {
1426 Containment::In
1427 } else {
1428 Containment::Out
1429 }
1430 }))
1431 }
1432
1433 pub fn winding(
1444 &self,
1445 model: &Model,
1446 point: Point,
1447 tol: Tolerances,
1448 ) -> OgeomResult<Option<i64>> {
1449 if !self.faces_at(model, point, tol)?.is_empty() {
1450 return Ok(None);
1451 }
1452 Ok(self.crossed(model, point, tol)?.map(|(_, winding)| winding))
1453 }
1454
1455 pub fn faces_at(
1461 &self,
1462 model: &Model,
1463 point: Point,
1464 tol: Tolerances,
1465 ) -> OgeomResult<Vec<Shape>> {
1466 Ok(self
1467 .faces_holding(model, point, tol)?
1468 .into_iter()
1469 .map(|(face, _)| face)
1470 .collect())
1471 }
1472
1473 pub(crate) fn faces_holding(
1477 &self,
1478 model: &Model,
1479 point: Point,
1480 tol: Tolerances,
1481 ) -> OgeomResult<Vec<(Shape, Containment)>> {
1482 let Prepared {
1483 faces, ring_chord, ..
1484 } = &*self.prepared;
1485 let ring_deflection = Deflection {
1486 chord: *ring_chord,
1487 angular: 0.05,
1488 ..Deflection::default()
1489 };
1490 let mut on = Vec::new();
1491 for k in self.prepared.boxes().holding(point) {
1492 let prepared = &faces[k];
1493 if !prepared.bound.contains(point) {
1494 continue;
1495 }
1496 let held = prepared.holds(model, point, ring_deflection, tol)?;
1497 if held != Containment::Out {
1498 on.push((prepared.face.clone(), held));
1499 }
1500 }
1501 Ok(on)
1502 }
1503
1504 fn crossed(
1510 &self,
1511 model: &Model,
1512 point: Point,
1513 tol: Tolerances,
1514 ) -> OgeomResult<Option<(usize, i64)>> {
1515 let Prepared {
1516 faces,
1517 centre,
1518 diagonal,
1519 ring_chord,
1520 ..
1521 } = &*self.prepared;
1522 let ring_chord = *ring_chord;
1523 let ring_deflection = Deflection {
1524 chord: ring_chord,
1525 angular: 0.05,
1526 ..Deflection::default()
1527 };
1528 let reach = tol.confusion();
1529 let length = point.distance(*centre) + diagonal + 1.0;
1530 'directions: for direction in RAY_DIRECTIONS {
1531 let along = Vector::new(direction[0], direction[1], direction[2]);
1532 let far = point + along * length;
1533 let mut crossings = 0_usize;
1534 let mut winding = 0_i64;
1535
1536 for k in self.prepared.boxes().meeting(point, far) {
1537 let (prepared, rays) = (&faces[k], &self.rays[k]);
1538 if !segment_meets(&prepared.bound, point, far) {
1539 continue;
1540 }
1541 let (surface, inverse) = prepared.placed(model)?;
1542 let from = inverse.apply(point);
1545 let to = inverse.apply(far);
1546 let found = match rays.get_or_init(|| {
1547 self.kept
1548 .surface(prepared.surface, surface, self.prepared.tol)
1549 }) {
1550 Some(kept) if kept.tol == tol => kept.cross((from, to))?,
1551 _ => {
1552 let ray: ogeom_geom::Curve =
1553 ogeom_geom::LineCurve::segment(from, to, tol)?.into();
1554 ogeom_intersect::intersect_curve_surface(
1555 &ray,
1556 surface,
1557 ogeom_intersect::CurveSurfaceOptions::default(),
1558 tol,
1559 )?
1560 }
1561 };
1562 if !found.lying.is_empty() {
1563 continue 'directions;
1566 }
1567 for hit in &found.crossings {
1568 if hit.on_curve <= tol.confusion() {
1569 let (u, v) = hit.on_surface;
1576 let rings = prepared.rings(model, ring_deflection, tol)?;
1577 let at = rings.place(surface, Point2::new(u, v), tol);
1578 let band = parametric_band(surface, (u, v), reach + ring_chord, tol);
1579 let face = &prepared.face;
1580 if rings.within(model, face, ring_deflection, at, band, tol)?
1581 || rings.inside(model, face, ring_deflection, at, tol)?
1582 {
1583 continue 'directions;
1584 }
1585 continue;
1586 }
1587 let (u, v) = hit.on_surface;
1588 use ogeom_geom::Surface as _;
1589 let Ok((du, dv)) = surface.d1_at(u, v, tol) else {
1590 continue 'directions;
1591 };
1592 let normal = du.cross(dv);
1593 if normal.magnitude() <= tol.confusion() {
1594 continue 'directions;
1596 }
1597 let ray_direction = (to - from) / (to - from).magnitude();
1598 if normal.dot(ray_direction).abs() <= GRAZING * normal.magnitude() {
1599 continue 'directions;
1602 }
1603 let mut rings = prepared.rings(model, ring_deflection, tol)?;
1604 let mut at = rings.place(surface, Point2::new(u, v), tol);
1605 let band = parametric_band(
1606 surface,
1607 (u, v),
1608 reach.max(prepared.rim(model)?) + ring_chord,
1609 tol,
1610 );
1611 let face = &prepared.face;
1612 if !matches!(surface, ogeom_geom::SurfaceGeometry::Plane(_))
1616 && rings.within(
1617 model,
1618 face,
1619 ring_deflection,
1620 at,
1621 parametric_band(surface, (u, v), (reach + ring_chord) * NEAR_TRIM, tol),
1622 tol,
1623 )?
1624 {
1625 rings = prepared.fine_rings(model, ring_deflection, tol)?;
1626 at = rings.place(surface, Point2::new(u, v), tol);
1627 }
1628 if rings.within(model, face, ring_deflection, at, band, tol)? {
1629 continue 'directions;
1635 }
1636 if rings.inside(model, face, ring_deflection, at, tol)? {
1637 crossings += 1;
1638 let leaves = (normal.dot(ray_direction) > 0.0)
1642 != (face.orientation() == ogeom_topo::Orientation::Reversed);
1643 winding += if leaves { 1 } else { -1 };
1644 }
1645 }
1646 }
1647 return Ok(Some((crossings, winding)));
1648 }
1649 Ok(None)
1650 }
1651
1652 fn reached(
1662 &self,
1663 model: &Model,
1664 point: Point,
1665 tol: Tolerances,
1666 ) -> OgeomResult<Option<Containment>> {
1667 use ogeom_geom::Surface as _;
1668 let Prepared {
1669 faces, ring_chord, ..
1670 } = &*self.prepared;
1671 let ring_chord = *ring_chord;
1672 let ring_deflection = Deflection {
1673 chord: ring_chord,
1674 angular: 0.05,
1675 ..Deflection::default()
1676 };
1677 let reach = tol.confusion();
1678 let mut feet = Vec::new();
1682 for (k, prepared) in faces.iter().enumerate() {
1683 let (surface, inverse) = prepared.placed(model)?;
1684 let local = inverse.apply(point);
1685 let forward = inverse.inverse()?;
1686 let mut aims: Vec<(f64, f64)> = Vec::new();
1690 if let Ok(found) = project_on_surface(surface, local, 16, tol) {
1691 aims.push(found.parameters);
1692 }
1693 if let Ok(mesh) =
1694 ogeom_mesh::triangulate_face(model, &prepared.face, ring_deflection, tol)
1695 && mesh.parameters.len() == mesh.positions.len()
1696 {
1697 let mut middles: Vec<(f64, (f64, f64))> = mesh
1698 .triangles
1699 .iter()
1700 .map(|t| {
1701 let [a, b, c] = t.map(|i| i as usize);
1702 let (pa, pb, pc) =
1703 (mesh.parameters[a], mesh.parameters[b], mesh.parameters[c]);
1704 let uv = ((pa.0 + pb.0 + pc.0) / 3.0, (pa.1 + pb.1 + pc.1) / 3.0);
1705 let at = Point::new(
1706 (mesh.positions[a].x + mesh.positions[b].x + mesh.positions[c].x) / 3.0,
1707 (mesh.positions[a].y + mesh.positions[b].y + mesh.positions[c].y) / 3.0,
1708 (mesh.positions[a].z + mesh.positions[b].z + mesh.positions[c].z) / 3.0,
1709 );
1710 (at.distance(point), uv)
1711 })
1712 .collect();
1713 middles.sort_by(|a, b| a.0.total_cmp(&b.0));
1714 aims.extend(middles.into_iter().take(AIMS_PER_FACE).map(|(_, uv)| uv));
1715 }
1716 let rings = prepared.rings(model, ring_deflection, tol)?;
1717 for (u, v) in aims {
1718 let Ok((du, dv)) = surface.d1_at(u, v, tol) else {
1719 continue;
1720 };
1721 let normal = du.cross(dv);
1722 if normal.magnitude() <= tol.confusion() {
1723 continue;
1724 }
1725 let at = rings.place(surface, Point2::new(u, v), tol);
1726 let band = parametric_band(surface, (u, v), reach + ring_chord, tol);
1727 let face = &prepared.face;
1728 if rings.within(model, face, ring_deflection, at, band, tol)?
1729 || !rings.inside(model, face, ring_deflection, at, tol)?
1730 {
1731 continue;
1732 }
1733 let foot = forward.apply(surface.point_at(u, v, tol)?);
1734 feet.push((point.distance(foot), k, foot, (u, v), normal));
1735 }
1736 }
1737 feet.sort_by(|a, b| a.0.total_cmp(&b.0));
1738 'feet: for (length, k, foot, _, normal) in feet {
1739 if length <= reach {
1740 continue;
1741 }
1742 let along = (foot - point) / length;
1745 let far = point + along * (length * 1.5);
1746 let margin = reach + ring_chord;
1747 for j in self.prepared.boxes().meeting(point, far) {
1748 let prepared = &faces[j];
1749 if !segment_meets(&prepared.bound, point, far) {
1750 continue;
1751 }
1752 let (surface, inverse) = prepared.placed(model)?;
1753 let (from, to) = (inverse.apply(point), inverse.apply(far));
1754 let ray: ogeom_geom::Curve = ogeom_geom::LineCurve::segment(from, to, tol)?.into();
1755 let found = ogeom_intersect::intersect_curve_surface(
1756 &ray,
1757 surface,
1758 ogeom_intersect::CurveSurfaceOptions::default(),
1759 tol,
1760 )?;
1761 if !found.lying.is_empty() {
1762 continue 'feet;
1763 }
1764 let scale = (to - from).magnitude() / (length * 1.5);
1768 let at_foot = length * scale;
1769 for hit in &found.crossings {
1770 let (hu, hv) = hit.on_surface;
1771 if j == k && (hit.on_curve - at_foot).abs() <= margin * scale {
1772 let Ok((du, dv)) = surface.d1_at(hu, hv, tol) else {
1775 continue 'feet;
1776 };
1777 let n = du.cross(dv);
1778 let d = (to - from) / (to - from).magnitude();
1779 if n.dot(d).abs() <= GRAZING * n.magnitude() {
1780 continue 'feet;
1781 }
1782 continue;
1783 }
1784 if hit.on_curve > at_foot + margin * scale {
1785 continue;
1786 }
1787 let rings = prepared.rings(model, ring_deflection, tol)?;
1790 let at = rings.place(surface, Point2::new(hu, hv), tol);
1791 let band = parametric_band(surface, (hu, hv), margin, tol);
1792 let face = &prepared.face;
1793 if rings.within(model, face, ring_deflection, at, band, tol)?
1794 || rings.inside(model, face, ring_deflection, at, tol)?
1795 {
1796 continue 'feet;
1797 }
1798 }
1799 }
1800 let prepared = &faces[k];
1804 let (_, inverse) = prepared.placed(model)?;
1805 let side = normal.dot(inverse.apply(foot) - inverse.apply(point));
1806 let outward = if prepared.face.orientation() == ogeom_topo::Orientation::Reversed {
1807 -side
1808 } else {
1809 side
1810 };
1811 if outward.abs() <= GRAZING * normal.magnitude() * length {
1812 continue;
1813 }
1814 return Ok(Some(if outward > 0.0 {
1815 Containment::In
1816 } else {
1817 Containment::Out
1818 }));
1819 }
1820 Ok(None)
1821 }
1822
1823 fn wound(&self, model: &Model, point: Point, tol: Tolerances) -> Option<Containment> {
1832 self.wound_finer(model, point, 3, tol)
1833 }
1834
1835 pub(crate) fn wound_coarsely(
1839 &self,
1840 model: &Model,
1841 point: Point,
1842 tol: Tolerances,
1843 ) -> Option<Containment> {
1844 self.wound_finer(model, point, 1, tol)
1845 }
1846
1847 fn wound_finer(
1849 &self,
1850 model: &Model,
1851 point: Point,
1852 meshes: usize,
1853 tol: Tolerances,
1854 ) -> Option<Containment> {
1855 use crate::winding::{WindingMesh, Wound};
1856 let prepared = &*self.prepared;
1857 let finest = (prepared.diagonal * WINDING_FINEST).max(tol.confusion() * 1e3);
1858 let mesh = |chord: f64| {
1859 WindingMesh::of(model, &prepared.solid, chord, Some(self.kept.meshes()), tol)
1860 .ok()
1861 .map(std::sync::Arc::new)
1862 };
1863 let mut meshed = prepared
1864 .winding
1865 .get_or_init(|| mesh(prepared.winding_chord(tol)))
1866 .clone()?;
1867 for asked in 1..=meshes {
1868 match meshed.wind(point) {
1869 Wound::Reads(read) => return Some(read),
1870 Wound::Unread => return None,
1871 Wound::Near(distance) => {
1872 let chord = distance * 0.25;
1873 if asked == meshes || chord < finest || chord >= meshed.chord() {
1874 return None;
1875 }
1876 meshed = mesh(chord)?;
1877 }
1878 }
1879 }
1880 None
1881 }
1882}
1883
1884const WINDING_FINEST: f64 = 1e-5;
1887
1888impl Prepared {
1889 fn winding_chord(&self, tol: Tolerances) -> f64 {
1892 (self.diagonal * 1e-3).max(tol.confusion() * 1e3)
1893 }
1894}
1895
1896#[derive(Debug, Clone, Default)]
1907pub struct ProbeCache(std::sync::Arc<Kept>);
1908
1909#[derive(Debug, Default)]
1910struct Kept {
1911 meshes: ogeom_mesh::FaceMeshCache,
1912 rays: std::sync::Mutex<ogeom_core::FastMap<ogeom_topo::SurfaceId, std::sync::Arc<KeptSurface>>>,
1913}
1914
1915#[derive(Debug)]
1917struct KeptSurface {
1918 readied: ogeom_intersect::PreparedSurface,
1919 tol: Tolerances,
1920 answers:
1921 std::sync::Mutex<ogeom_core::FastMap<[u64; 6], ogeom_intersect::CurveSurfaceIntersection>>,
1922}
1923
1924impl ProbeCache {
1925 #[must_use]
1927 pub fn new() -> Self {
1928 Self::default()
1929 }
1930
1931 pub(crate) fn meshes(&self) -> &ogeom_mesh::FaceMeshCache {
1933 &self.0.meshes
1934 }
1935
1936 fn surface(
1941 &self,
1942 id: ogeom_topo::SurfaceId,
1943 surface: &ogeom_geom::SurfaceGeometry,
1944 tol: Tolerances,
1945 ) -> Option<std::sync::Arc<KeptSurface>> {
1946 use ogeom_geom::SurfaceGeometry as S;
1947 if matches!(
1948 surface,
1949 S::Plane(_) | S::Sphere(_) | S::Cylinder(_) | S::Cone(_) | S::Torus(_)
1950 ) {
1951 return None;
1952 }
1953 let mut rays = self
1954 .0
1955 .rays
1956 .lock()
1957 .unwrap_or_else(std::sync::PoisonError::into_inner);
1958 if let Some(kept) = rays.get(&id)
1959 && kept.tol == tol
1960 && kept.readied.surface() == surface
1961 {
1962 return Some(kept.clone());
1963 }
1964 let options = ogeom_intersect::CurveSurfaceOptions::default();
1965 let kept = std::sync::Arc::new(KeptSurface {
1966 readied: ogeom_intersect::PreparedSurface::new(surface.clone(), options, tol),
1967 tol,
1968 answers: std::sync::Mutex::default(),
1969 });
1970 rays.insert(id, kept.clone());
1971 Some(kept)
1972 }
1973}
1974
1975impl KeptSurface {
1976 fn cross(
1980 &self,
1981 (from, to): (Point, Point),
1982 ) -> OgeomResult<ogeom_intersect::CurveSurfaceIntersection> {
1983 let key = [from.x, from.y, from.z, to.x, to.y, to.z].map(f64::to_bits);
1984 let answers = || {
1985 self.answers
1986 .lock()
1987 .unwrap_or_else(std::sync::PoisonError::into_inner)
1988 };
1989 if let Some(found) = answers().get(&key) {
1990 return Ok(found.clone());
1991 }
1992 let ray: ogeom_geom::Curve = ogeom_geom::LineCurve::segment(from, to, self.tol)?.into();
1993 let found = self.readied.intersect(&ray)?;
1994 answers().insert(key, found.clone());
1995 Ok(found)
1996 }
1997}
1998
1999const GRAZING: f64 = 1e-6;
2008
2009const AIMS_PER_FACE: usize = 3;
2013
2014const RAY_DIRECTIONS: [[f64; 3]; 6] = [
2023 [0.577_35, 0.577_35, 0.577_35],
2024 [-0.301_5, 0.904_5, 0.301_5],
2025 [0.727_6, -0.485_1, 0.485_1],
2026 [0.259_5, 0.259_5, -0.930_0],
2027 [-0.816_5, -0.408_2, 0.408_2],
2028 [0.132_5, -0.662_3, -0.737_5],
2029];
2030
2031fn count_crossings(
2034 triangles: &[[Point; 3]],
2035 from: Point,
2036 along: Direction,
2037 tol: Tolerances,
2038) -> Option<usize> {
2039 let mut crossings = 0;
2040 for t in triangles {
2041 match ray_hits_triangle(from, along, *t, tol) {
2042 Hit::Crosses => crossings += 1,
2043 Hit::Misses => {}
2044 Hit::Ambiguous => return None,
2045 }
2046 }
2047 Some(crossings)
2048}
2049
2050enum Hit {
2052 Crosses,
2054 Misses,
2056 Ambiguous,
2059}
2060
2061fn ray_hits_triangle(from: Point, along: Direction, t: [Point; 3], tol: Tolerances) -> Hit {
2064 let direction = along.vector();
2065 let (e1, e2) = (t[1] - t[0], t[2] - t[0]);
2066 let h = direction.cross(e2);
2067 let determinant = e1.dot(h);
2068
2069 let scale = e1.magnitude() * e2.magnitude();
2073 let flat = tol.confusion() * scale;
2074 if determinant.abs() <= flat {
2075 let normal = e1.cross(e2);
2078 let reach = tol.confusion() * scale;
2079 return if normal.dot(from - t[0]).abs() <= reach {
2080 Hit::Ambiguous
2081 } else {
2082 Hit::Misses
2083 };
2084 }
2085
2086 let inverse = 1.0 / determinant;
2087 let s = from - t[0];
2088 let u = inverse * s.dot(h);
2089 let q = s.cross(e1);
2090 let v = inverse * direction.dot(q);
2091 let w = 1.0 - u - v;
2092
2093 let edge = tol.confusion();
2096 if [u, v, w].iter().any(|c| c.abs() <= edge) {
2097 return if u >= -edge && v >= -edge && w >= -edge {
2100 Hit::Ambiguous
2101 } else {
2102 Hit::Misses
2103 };
2104 }
2105 if u < 0.0 || v < 0.0 || w < 0.0 {
2106 return Hit::Misses;
2107 }
2108
2109 let distance = inverse * e2.dot(q);
2110 if distance <= tol.confusion() {
2111 return Hit::Misses;
2114 }
2115 Hit::Crosses
2116}
2117
2118pub(crate) fn distance_to_triangle(p: Point, t: [Point; 3]) -> f64 {
2120 let (e1, e2) = (t[1] - t[0], t[2] - t[0]);
2125 let d = t[0] - p;
2126 let (a, b, c) = (e1.dot(e1), e1.dot(e2), e2.dot(e2));
2127 let (dd, e) = (e1.dot(d), e2.dot(d));
2128 let determinant = b.mul_add(-b, a * c);
2129
2130 if determinant.abs() <= f64::MIN_POSITIVE {
2131 return edge_distance(p, t);
2133 }
2134 let mut s = b.mul_add(e, -(c * dd)) / determinant;
2135 let mut u = b.mul_add(dd, -(a * e)) / determinant;
2136
2137 if s >= 0.0 && u >= 0.0 && s + u <= 1.0 {
2138 let closest = t[0] + e1 * s + e2 * u;
2139 return p.distance(closest);
2140 }
2141 s = s.clamp(0.0, 1.0);
2143 u = u.clamp(0.0, 1.0);
2144 let _ = (s, u);
2145 edge_distance(p, t)
2146}
2147
2148fn edge_distance(p: Point, t: [Point; 3]) -> f64 {
2150 let mut best = f64::INFINITY;
2151 for i in 0..3 {
2152 best = best.min(segment_distance(p, t[i], t[(i + 1) % 3]));
2153 }
2154 best
2155}
2156
2157pub(crate) fn segment_distance(p: Point, a: Point, b: Point) -> f64 {
2159 let d = b - a;
2160 let squared = d.dot(d);
2161 if squared <= f64::MIN_POSITIVE {
2162 return p.distance(a);
2163 }
2164 let t = ((p - a).dot(d) / squared).clamp(0.0, 1.0);
2165 p.distance(a + d * t)
2166}
2167
2168fn segment_distance_2d(p: Point2, a: Point2, b: Point2) -> f64 {
2170 let d = b - a;
2171 let squared = d.dot(d);
2172 if squared <= f64::MIN_POSITIVE {
2173 return p.distance(a);
2174 }
2175 let t = ((p - a).dot(d) / squared).clamp(0.0, 1.0);
2176 p.distance(a + d * t)
2177}
2178
2179pub(crate) fn fold_toward_rings(
2186 surface: &ogeom_geom::SurfaceGeometry,
2187 rings: &[Vec<Point2>],
2188 mut at: Point2,
2189) -> Point2 {
2190 use ogeom_geom::Surface as _;
2191 let ((ua, ub), (va, vb)) = surface.domain();
2194 let u_period = (surface.is_periodic_u() && ub > ua).then_some(ub - ua);
2195 let v_period = (surface.is_periodic_v() && vb > va).then_some(vb - va);
2196 let fold = |x: f64, lo: f64, hi: f64, period: f64| -> f64 {
2197 let mut x = x;
2198 while x < lo && x + period <= hi + period {
2199 x += period;
2200 if x >= lo {
2201 break;
2202 }
2203 }
2204 while x > hi && x - period >= lo - period {
2205 x -= period;
2206 if x <= hi {
2207 break;
2208 }
2209 }
2210 x
2211 };
2212 if let Some(period) = u_period {
2213 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
2214 for ring in rings {
2215 for q in ring {
2216 lo = lo.min(q.x);
2217 hi = hi.max(q.x);
2218 }
2219 }
2220 if lo.is_finite() {
2221 at.x = fold(at.x, lo, hi, period);
2222 }
2223 }
2224 if let Some(period) = v_period {
2225 let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
2226 for ring in rings {
2227 for q in ring {
2228 lo = lo.min(q.y);
2229 hi = hi.max(q.y);
2230 }
2231 }
2232 if lo.is_finite() {
2233 at.y = fold(at.y, lo, hi, period);
2234 }
2235 }
2236 if !inside_boundary(rings, at) {
2245 let (low, high) = rings.iter().flatten().fold(
2246 (
2247 Point2::new(f64::INFINITY, f64::INFINITY),
2248 Point2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
2249 ),
2250 |(lo, hi), q| {
2251 (
2252 Point2::new(lo.x.min(q.x), lo.y.min(q.y)),
2253 Point2::new(hi.x.max(q.x), hi.y.max(q.y)),
2254 )
2255 },
2256 );
2257 let turns = |x: f64, lo: f64, hi: f64, period: f64| -> Vec<f64> {
2260 if !(lo.is_finite() && hi.is_finite()) {
2261 return Vec::new();
2262 }
2263 let first = ((lo - x) / period).ceil() - 1.0;
2264 let last = ((hi - x) / period).floor() + 1.0;
2265 if !(first.is_finite() && last.is_finite()) || last - first > 4096.0 {
2266 return vec![-1.0, 1.0];
2267 }
2268 #[allow(clippy::cast_possible_truncation, reason = "at most 4096 turns")]
2269 let (first, last) = (first as i64, last as i64);
2270 #[allow(clippy::cast_precision_loss, reason = "at most 4096 turns")]
2271 let mut all: Vec<f64> = (first..=last)
2272 .filter(|k| *k != 0)
2273 .map(|k| k as f64)
2274 .collect();
2275 all.sort_by(|a, b| a.abs().total_cmp(&b.abs()));
2276 all
2277 };
2278 let mut shifts: Vec<(f64, f64)> = Vec::new();
2279 if let Some(p) = u_period {
2280 shifts.extend(
2281 turns(at.x, low.x, high.x, p)
2282 .into_iter()
2283 .map(|k| (k * p, 0.0)),
2284 );
2285 }
2286 if let Some(p) = v_period {
2287 shifts.extend(
2288 turns(at.y, low.y, high.y, p)
2289 .into_iter()
2290 .map(|k| (0.0, k * p)),
2291 );
2292 }
2293 if let Some(inside) = shifts
2294 .into_iter()
2295 .map(|(du, dv)| Point2::new(at.x + du, at.y + dv))
2296 .find(|q| inside_boundary(rings, *q))
2297 {
2298 return inside;
2299 }
2300 }
2301 at
2302}
2303
2304fn place_on_rings(
2312 surface: &ogeom_geom::SurfaceGeometry,
2313 rings: &[Vec<Point2>],
2314 at: Point2,
2315 tol: Tolerances,
2316) -> Point2 {
2317 let folded = fold_toward_rings(surface, rings, at);
2318 if inside_boundary(rings, folded) {
2319 return folded;
2320 }
2321 let ogeom_geom::SurfaceGeometry::Revolution(revolution) = surface else {
2322 return folded;
2323 };
2324 let other = (|| -> Option<Point2> {
2325 use ogeom_geom::Surface as _;
2326 let point = surface.point_at(at.x, at.y, tol).ok()?;
2327 let ((u0, u1), _) = surface.domain();
2328 let half = core::f64::consts::PI;
2329 let u = [at.x + half, at.x - half]
2330 .into_iter()
2331 .find(|u| *u >= u0 - tol.angular() && *u <= u1 + tol.angular())?;
2332 let back = ogeom_math::Transform::rotation(revolution.axis(), -u).apply(point);
2333 let found = crate::measure::project_on_curve(revolution.curve(), back, 64, tol).ok()?;
2334 (found.distance <= tol.confusion()).then(|| Point2::new(u, found.parameter))
2335 })();
2336 other
2337 .map(|q| fold_toward_rings(surface, rings, q))
2338 .filter(|q| inside_boundary(rings, *q))
2339 .unwrap_or(folded)
2340}
2341
2342#[derive(Debug, Clone, Copy)]
2353pub(crate) struct ChartBand {
2354 reach: f64,
2355 su: f64,
2356 sv: f64,
2357}
2358
2359impl ChartBand {
2360 pub(crate) fn radius(self) -> f64 {
2363 let scale = self.su.min(self.sv);
2364 if scale <= 0.0 {
2365 f64::INFINITY
2366 } else {
2367 self.reach / scale
2368 }
2369 }
2370
2371 pub(crate) fn meets_ring(self, ring: &[Point2], p: Point2) -> bool {
2373 if self.su <= 0.0 && self.sv <= 0.0 {
2374 return true;
2375 }
2376 let scaled = |q: Point2| Point2::new((q.x - p.x) * self.su, (q.y - p.y) * self.sv);
2377 let origin = Point2::new(0.0, 0.0);
2378 (0..ring.len()).any(|i| {
2379 let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
2380 segment_distance_2d(origin, scaled(a), scaled(b)) <= self.reach
2381 })
2382 }
2383
2384 pub(crate) fn meets_rings(self, rings: &[Vec<Point2>], p: Point2) -> bool {
2386 rings.iter().any(|ring| self.meets_ring(ring, p))
2387 }
2388}
2389
2390pub(crate) fn parametric_band(
2393 surface: &ogeom_geom::SurfaceGeometry,
2394 at: (f64, f64),
2395 reach: f64,
2396 tol: Tolerances,
2397) -> ChartBand {
2398 use ogeom_geom::Surface;
2399 let Ok((du, dv)) = surface.d1_at(at.0, at.1, tol) else {
2400 return ChartBand {
2401 reach,
2402 su: 1.0,
2403 sv: 1.0,
2404 };
2405 };
2406 let scale = |t: ogeom_math::Vector| {
2408 let m = t.magnitude();
2409 if m <= tol.confusion() { 0.0 } else { m }
2410 };
2411 ChartBand {
2412 reach,
2413 su: scale(du),
2414 sv: scale(dv),
2415 }
2416}
2417
2418fn closed_form(surface: &ogeom_geom::SurfaceGeometry) -> bool {
2421 use ogeom_geom::SurfaceGeometry as S;
2422 matches!(
2423 surface,
2424 S::Plane(_) | S::Cylinder(_) | S::Cone(_) | S::Sphere(_) | S::Torus(_)
2425 )
2426}
2427
2428#[derive(Debug)]
2443struct FaceBoxes {
2444 nodes: Vec<BoxNode>,
2445 order: Vec<u32>,
2447 always: Vec<u32>,
2449}
2450
2451#[derive(Debug, Clone, Copy)]
2454struct BoxNode {
2455 bound: Aabb,
2456 children: Option<(u32, u32)>,
2457 run: (u32, u32),
2458}
2459
2460const LEAF_FACES: usize = 8;
2462
2463impl FaceBoxes {
2464 fn of(bounds: &[Aabb]) -> Self {
2465 let finite = |b: &Aabb| {
2466 b.low()
2467 .zip(b.high())
2468 .is_some_and(|(l, h)| [l.x, l.y, l.z, h.x, h.y, h.z].iter().all(|c| c.is_finite()))
2469 };
2470 let mut order = Vec::new();
2471 let mut always = Vec::new();
2472 for (k, b) in bounds.iter().enumerate() {
2473 let k = u32::try_from(k).unwrap_or(u32::MAX);
2474 if finite(b) {
2475 order.push(k);
2476 } else if !b.is_empty() {
2477 always.push(k);
2478 }
2479 }
2480 let mut tree = Self {
2481 nodes: Vec::new(),
2482 order,
2483 always,
2484 };
2485 if !tree.order.is_empty() {
2486 tree.split(bounds, 0, tree.order.len());
2487 }
2488 tree
2489 }
2490
2491 fn split(&mut self, bounds: &[Aabb], from: usize, to: usize) -> u32 {
2494 let middle = |k: u32| bounds[k as usize].centre().unwrap_or(Point::ORIGIN);
2495 let bound = self.order[from..to]
2496 .iter()
2497 .fold(Aabb::EMPTY, |b, &k| b.union(&bounds[k as usize]));
2498 let at = u32::try_from(self.nodes.len()).unwrap_or(u32::MAX);
2499 let run = (
2500 u32::try_from(from).unwrap_or(u32::MAX),
2501 u32::try_from(to).unwrap_or(u32::MAX),
2502 );
2503 self.nodes.push(BoxNode {
2504 bound,
2505 children: None,
2506 run,
2507 });
2508 if to - from <= LEAF_FACES {
2509 return at;
2510 }
2511 let spread = self.order[from..to]
2512 .iter()
2513 .fold(Aabb::EMPTY, |b, &k| b.with_point(middle(k)))
2514 .size();
2515 let axis = if spread.x >= spread.y && spread.x >= spread.z {
2516 0
2517 } else if spread.y >= spread.z {
2518 1
2519 } else {
2520 2
2521 };
2522 let key = |k: u32| {
2523 let m = middle(k);
2524 [m.x, m.y, m.z][axis]
2525 };
2526 let half = from + (to - from) / 2;
2527 self.order[from..to].select_nth_unstable_by(half - from, |&a, &b| {
2528 key(a).total_cmp(&key(b)).then(a.cmp(&b))
2529 });
2530 let left = self.split(bounds, from, half);
2531 let right = self.split(bounds, half, to);
2532 self.nodes[at as usize].children = Some((left, right));
2533 at
2534 }
2535
2536 fn candidates(&self, meets: impl Fn(&Aabb) -> bool) -> Vec<usize> {
2538 let mut out: Vec<usize> = self.always.iter().map(|&k| k as usize).collect();
2539 if !self.nodes.is_empty() {
2540 let mut stack = vec![0_u32];
2541 while let Some(n) = stack.pop() {
2542 let node = &self.nodes[n as usize];
2543 if !meets(&node.bound) {
2544 continue;
2545 }
2546 match node.children {
2547 Some((left, right)) => {
2548 stack.push(right);
2549 stack.push(left);
2550 }
2551 None => out.extend(
2552 self.order[node.run.0 as usize..node.run.1 as usize]
2553 .iter()
2554 .map(|&k| k as usize),
2555 ),
2556 }
2557 }
2558 }
2559 out.sort_unstable();
2560 out
2561 }
2562
2563 fn meeting(&self, a: Point, b: Point) -> Vec<usize> {
2565 self.candidates(|bound| segment_meets(bound, a, b))
2566 }
2567
2568 fn holding(&self, point: Point) -> Vec<usize> {
2570 self.candidates(|bound| bound.contains(point))
2571 }
2572}
2573
2574fn segment_meets(bound: &Aabb, a: Point, b: Point) -> bool {
2575 let (Some(low), Some(high)) = (bound.low(), bound.high()) else {
2576 return false;
2577 };
2578 let (mut enter, mut leave) = (0.0_f64, 1.0_f64);
2579 for (from, to, lo, hi) in [
2580 (a.x, b.x, low.x, high.x),
2581 (a.y, b.y, low.y, high.y),
2582 (a.z, b.z, low.z, high.z),
2583 ] {
2584 let d = to - from;
2585 if d.abs() <= f64::EPSILON * (from.abs() + to.abs() + 1.0) {
2586 if from < lo || from > hi {
2587 return false;
2588 }
2589 continue;
2590 }
2591 let (t0, t1) = ((lo - from) / d, (hi - from) / d);
2592 let (t0, t1) = if t0 <= t1 { (t0, t1) } else { (t1, t0) };
2593 enter = enter.max(t0);
2594 leave = leave.min(t1);
2595 if enter > leave {
2596 return false;
2597 }
2598 }
2599 true
2600}
2601
2602#[cfg(test)]
2603#[allow(clippy::unwrap_used, clippy::expect_used)]
2604mod tests {
2605 use super::*;
2606
2607 #[test]
2613 fn face_boxes_hold_every_bound_a_probe_meets() {
2614 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
2615 let mut step = || {
2616 state ^= state << 13;
2617 state ^= state >> 7;
2618 state ^= state << 17;
2619 u32::try_from(state >> 40).unwrap()
2620 };
2621 let unit = |n: u32| f64::from(n) / f64::from(1_u32 << 24);
2622 let mut bounds = Vec::new();
2623 for k in 0..600 {
2624 let low = Point::new(
2625 unit(step()) * 100.0,
2626 unit(step()) * 100.0,
2627 unit(step()) * 100.0,
2628 );
2629 let size = if k % 50 == 0 {
2630 0.0
2631 } else {
2632 unit(step()) * 20.0
2633 };
2634 let high = Point::new(low.x + size, low.y + unit(step()) * 20.0, low.z);
2635 bounds.push(if k % 97 == 0 {
2636 Aabb::EMPTY
2637 } else {
2638 Aabb::of_corners(low, high)
2639 });
2640 }
2641 bounds.push(Aabb::of_corners(
2642 Point::new(f64::NAN, 0.0, 0.0),
2643 Point::new(1.0, 1.0, 1.0),
2644 ));
2645 let tree = FaceBoxes::of(&bounds);
2646 for _ in 0..400 {
2647 let a = Point::new(
2648 unit(step()) * 120.0 - 10.0,
2649 unit(step()) * 120.0 - 10.0,
2650 unit(step()) * 120.0 - 10.0,
2651 );
2652 let b = if unit(step()) < 0.3 {
2653 Point::new(a.x, a.y, unit(step()) * 120.0 - 10.0)
2654 } else {
2655 Point::new(
2656 unit(step()) * 120.0 - 10.0,
2657 unit(step()) * 120.0 - 10.0,
2658 unit(step()) * 120.0 - 10.0,
2659 )
2660 };
2661 let along = tree.meeting(a, b);
2662 assert!(along.windows(2).all(|w| w[0] < w[1]));
2663 for (k, bound) in bounds.iter().enumerate() {
2664 if segment_meets(bound, a, b) {
2665 assert!(
2666 along.binary_search(&k).is_ok(),
2667 "face {k} missed by {a:?}-{b:?}"
2668 );
2669 }
2670 }
2671 let pick = usize::try_from(step() % 600).unwrap();
2672 let at = bounds[pick].high().unwrap_or(a);
2673 let held = tree.holding(at);
2674 assert!(held.windows(2).all(|w| w[0] < w[1]));
2675 for (k, bound) in bounds.iter().enumerate() {
2676 if bound.contains(at) {
2677 assert!(held.binary_search(&k).is_ok(), "face {k} missed at {at:?}");
2678 }
2679 }
2680 }
2681 }
2682
2683 #[test]
2689 fn a_point_on_a_strip_winding_many_turns_folds_onto_its_own_turn() {
2690 use core::f64::consts::TAU;
2691 use ogeom_geom::{CylinderSurface, SurfaceGeometry};
2692 use ogeom_math::{Cylinder, Frame};
2693 let t = Tolerances::millimetres();
2694 let drum: SurfaceGeometry =
2695 CylinderSurface::new(Cylinder::new(Frame::WORLD, 1.6, t).unwrap(), (-10.0, 10.0))
2696 .unwrap()
2697 .into();
2698 let (turns, rise, height) = (14.0, 0.7, 0.2);
2699 let (u0, u1) = (0.3, 0.3 + turns * TAU);
2700 let v = |u: f64| -5.0 + (u - u0) / TAU * rise;
2701 let ring = vec![
2702 Point2::new(u0, v(u0)),
2703 Point2::new(u1, v(u1)),
2704 Point2::new(u1, v(u1) + height),
2705 Point2::new(u0, v(u0) + height),
2706 ];
2707 let rings = vec![ring];
2708 let on = 0.3 + 13.5 * TAU;
2710 let here = Point2::new(on, v(on) + height * 0.1);
2711 assert!(inside_boundary(&rings, here));
2712 let named = Point2::new(here.x - 13.0 * TAU, here.y);
2713 let folded = fold_toward_rings(&drum, &rings, named);
2714 assert!(inside_boundary(&rings, folded), "{folded:?}");
2715 assert!(
2716 (folded.x - here.x).abs() < 1e-9,
2717 "{folded:?} against {here:?}"
2718 );
2719 let off = Point2::new(named.x + 0.5 * TAU, named.y);
2722 let kept = fold_toward_rings(&drum, &rings, off);
2723 assert!(!inside_boundary(&rings, kept));
2724 }
2725 use crate::make_box;
2726 use ogeom_math::Frame;
2727 use ogeom_topo::{ShapeType, explore_unique};
2728
2729 const T: Tolerances = Tolerances::millimetres();
2730
2731 fn fine() -> Deflection {
2732 Deflection {
2733 chord: 1e-3,
2734 angular: 0.05,
2735 ..Deflection::default()
2736 }
2737 }
2738
2739 #[test]
2740 fn a_point_inside_a_box_is_inside_it() {
2741 let mut model = Model::new();
2742 let built = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
2743
2744 for p in [
2745 Point::new(1.0, 1.0, 1.0),
2746 Point::new(0.1, 0.1, 0.1),
2747 Point::new(1.9, 1.9, 1.9),
2748 ] {
2749 assert_eq!(
2750 classify_in_solid(&model, &built.shape, p, fine(), T).unwrap(),
2751 Containment::In,
2752 "{p:?} should be inside"
2753 );
2754 }
2755 }
2756
2757 #[test]
2760 fn a_point_in_a_spherical_void_is_outside_the_material() {
2761 let mut model = Model::new();
2762 let corner = Frame::new(
2763 Point::new(-20.0, -20.0, -20.0),
2764 ogeom_math::Direction::Z,
2765 ogeom_math::Direction::X,
2766 T,
2767 )
2768 .unwrap();
2769 let block = make_box(&mut model, corner, (40.0, 40.0, 40.0), T).unwrap();
2770 let ball = crate::make_sphere(&mut model, Frame::WORLD, 10.0, T).unwrap();
2771 let outer = explore_unique(&model, &block.shape, ShapeType::Shell).unwrap()[0].clone();
2772 let skin = explore_unique(&model, &ball.shape, ShapeType::Face).unwrap()[0].clone();
2773 let void = model.add_shell(&[skin.reversed()]).unwrap();
2774 let holed = crate::make_solid(&mut model, &[outer, void]).unwrap().shape;
2775 for (p, want) in [
2776 (Point::new(0.0, 0.0, 0.0), Containment::Out),
2777 (Point::new(3.0, 3.0, 3.0), Containment::Out),
2778 (Point::new(-3.0, -3.0, -3.0), Containment::Out),
2779 (Point::new(15.0, 0.0, 0.0), Containment::In),
2780 ] {
2781 assert_eq!(
2782 classify_in_solid_exact(&model, &holed, p, T).unwrap(),
2783 want,
2784 "{p:?}"
2785 );
2786 assert_eq!(
2787 classify_in_solid(&model, &holed, p, fine(), T).unwrap(),
2788 want,
2789 "{p:?}"
2790 );
2791 }
2792 }
2793
2794 #[test]
2800 fn points_beside_a_rim_near_a_sphere_s_pole_are_in_or_out() {
2801 use core::f64::consts::TAU;
2802 let mut model = Model::new();
2803 let radius = 10.0;
2804 let sphere = ogeom_math::Sphere::new(Frame::WORLD, radius, T).unwrap();
2805 let surface: ogeom_geom::SurfaceGeometry = ogeom_geom::SphereSurface::new(sphere).into();
2806 let height = |off: f64| (radius * radius - off * off).sqrt();
2808 let rim_frame = Frame::new(
2809 Point::new(0.0, 0.0, height(0.5)),
2810 ogeom_math::Direction::Z,
2811 ogeom_math::Direction::X,
2812 T,
2813 )
2814 .unwrap();
2815 let circle = ogeom_math::Circle::new(rim_frame, 0.5, T).unwrap();
2816 let start = crate::make_vertex(&mut model, Point::new(0.5, 0.0, height(0.5))).shape;
2817 let rim = crate::make_edge_between(
2818 &mut model,
2819 ogeom_geom::CircleCurve::new(circle).into(),
2820 (0.0, TAU),
2821 &start,
2822 &start,
2823 T,
2824 )
2825 .unwrap()
2826 .shape;
2827 let top = crate::make_vertex(&mut model, Point::new(0.0, 0.0, radius)).shape;
2828 let mut data = ogeom_topo::EdgeData::new();
2829 data.degenerate = true;
2830 let pole = model.add_edge(data, &[top.clone(), top]).unwrap();
2831 let cap = crate::make_revolution_band(&mut model, &surface, &rim, &pole, T).unwrap();
2832 let deflection = Deflection {
2834 chord: 1e-2,
2835 ..Deflection::default()
2836 };
2837 let at = |off: f64| Point::new(-off, 0.0, height(off));
2839 for (off, want) in [
2840 (0.4, Containment::In),
2841 (0.45, Containment::In),
2842 (0.5, Containment::On),
2843 (0.505, Containment::On),
2844 (0.55, Containment::Out),
2845 (0.6, Containment::Out),
2846 ] {
2847 assert_eq!(
2848 classify_on_face(&model, &cap, at(off), deflection, T).unwrap(),
2849 want,
2850 "{off} from the pole"
2851 );
2852 }
2853 }
2854
2855 #[test]
2856 fn a_point_outside_a_box_is_outside_it() {
2857 let mut model = Model::new();
2858 let built = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
2859
2860 for p in [
2861 Point::new(3.0, 1.0, 1.0),
2862 Point::new(-1.0, 1.0, 1.0),
2863 Point::new(1.0, 1.0, -0.5),
2864 Point::new(-5.0, -5.0, -5.0),
2865 ] {
2866 assert_eq!(
2867 classify_in_solid(&model, &built.shape, p, fine(), T).unwrap(),
2868 Containment::Out,
2869 "{p:?} should be outside"
2870 );
2871 }
2872 }
2873
2874 #[test]
2875 fn a_point_on_a_boxs_face_is_on_it_rather_than_forced_to_a_side() {
2876 let mut model = Model::new();
2879 let built = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
2880
2881 for p in [
2882 Point::new(1.0, 1.0, 0.0), Point::new(0.0, 1.0, 1.0), Point::new(2.0, 2.0, 1.0), Point::ORIGIN, Point::new(2.0, 2.0, 2.0), ] {
2888 assert_eq!(
2889 classify_in_solid(&model, &built.shape, p, fine(), T).unwrap(),
2890 Containment::On,
2891 "{p:?} should be on the boundary"
2892 );
2893 }
2894 }
2895
2896 #[test]
2897 fn a_ray_along_a_grid_of_edges_does_not_defeat_the_classifier() {
2898 let mut model = Model::new();
2903 let built = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
2904
2905 assert_eq!(
2908 classify_in_solid(&model, &built.shape, Point::new(1.0, 1.0, 1.0), fine(), T).unwrap(),
2909 Containment::In
2910 );
2911 }
2912
2913 #[test]
2914 fn an_open_shell_has_no_inside() {
2915 let mut model = Model::new();
2916 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
2917 let face = explore_unique(&model, &built.shape, ShapeType::Face).unwrap()[0].clone();
2918 assert!(classify_in_solid(&model, &face, Point::ORIGIN, fine(), T).is_err());
2919 }
2920
2921 #[test]
2922 fn a_point_on_a_face_is_inside_its_trimming_or_not() {
2923 let mut model = Model::new();
2924 let built = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
2925 let bottom = explore_unique(&model, &built.shape, ShapeType::Face)
2928 .unwrap()
2929 .into_iter()
2930 .find(|f| {
2931 model
2932 .provenance_of(f)
2933 .and_then(ogeom_core::Provenance::role)
2934 == Some(crate::primitive::roles::FACE_MIN_Z)
2935 })
2936 .expect("the box has a face at z = 0");
2937
2938 assert_eq!(
2939 classify_on_face(&model, &bottom, Point::new(1.0, 1.5, 0.0), fine(), T).unwrap(),
2940 Containment::In
2941 );
2942 assert_eq!(
2943 classify_on_face(&model, &bottom, Point::new(5.0, 1.5, 0.0), fine(), T).unwrap(),
2944 Containment::Out,
2945 "on the surface's plane but outside the trimming"
2946 );
2947 assert_eq!(
2948 classify_on_face(&model, &bottom, Point::new(1.0, 1.5, 1.0), fine(), T).unwrap(),
2949 Containment::Out,
2950 "off the surface entirely"
2951 );
2952 assert_eq!(
2953 classify_on_face(&model, &bottom, Point::new(0.0, 1.5, 0.0), fine(), T).unwrap(),
2954 Containment::On,
2955 "on the trimming boundary"
2956 );
2957 }
2958
2959 #[test]
2960 fn the_answers_invert_the_way_a_complement_does() {
2961 assert_eq!(Containment::In.inverted(), Containment::Out);
2962 assert_eq!(Containment::Out.inverted(), Containment::In);
2963 assert_eq!(Containment::On.inverted(), Containment::On);
2965
2966 assert!(Containment::In.is_inside_or_on());
2967 assert!(Containment::On.is_inside_or_on());
2968 assert!(!Containment::Out.is_inside_or_on());
2969 }
2970
2971 #[test]
2972 fn a_translated_box_classifies_the_same_way_translated_points() {
2973 let offset = Vector::new(10.0, -20.0, 30.0);
2974 let mut model = Model::new();
2975 let frame = Frame::new(Point::ORIGIN + offset, Direction::Z, Direction::X, T).unwrap();
2976 let built = make_box(&mut model, frame, (2.0, 2.0, 2.0), T).unwrap();
2977
2978 assert_eq!(
2979 classify_in_solid(
2980 &model,
2981 &built.shape,
2982 Point::new(1.0, 1.0, 1.0) + offset,
2983 fine(),
2984 T
2985 )
2986 .unwrap(),
2987 Containment::In
2988 );
2989 assert_eq!(
2990 classify_in_solid(&model, &built.shape, Point::new(1.0, 1.0, 1.0), fine(), T).unwrap(),
2991 Containment::Out,
2992 "the untranslated point is nowhere near the translated box"
2993 );
2994 }
2995
2996 #[test]
2997 fn the_exact_classifier_agrees_with_the_tessellated_one_on_a_box() {
2998 let mut model = Model::new();
2999 let built = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
3000
3001 for (p, want) in [
3002 (Point::new(1.0, 1.0, 1.0), Containment::In),
3003 (Point::new(0.1, 0.1, 0.1), Containment::In),
3004 (Point::new(3.0, 1.0, 1.0), Containment::Out),
3005 (Point::new(1.0, 1.0, -0.5), Containment::Out),
3006 (Point::new(-50.0, -50.0, -50.0), Containment::Out),
3007 (Point::new(1.0, 1.0, 0.0), Containment::On),
3008 (Point::new(2.0, 2.0, 1.0), Containment::On),
3009 (Point::ORIGIN, Containment::On),
3010 ] {
3011 assert_eq!(
3012 classify_in_solid_exact(&model, &built.shape, p, T).unwrap(),
3013 want,
3014 "{p:?}"
3015 );
3016 }
3017 }
3018
3019 #[test]
3020 fn the_exact_classifier_resolves_what_the_deflection_band_cannot() {
3021 let mut model = Model::new();
3026 let built = crate::make_sphere(&mut model, Frame::WORLD, 2.0, T).unwrap();
3027
3028 let barely_in = Point::new(0.0, 0.0, 2.0 - 1e-5);
3029 let barely_out = Point::new(0.0, 0.0, 2.0 + 1e-5);
3030
3031 assert_eq!(
3032 classify_in_solid(&model, &built.shape, barely_in, fine(), T).unwrap(),
3033 Containment::On,
3034 "the mesh cannot tell a micron from the wall"
3035 );
3036 assert_eq!(
3037 classify_in_solid_exact(&model, &built.shape, barely_in, T).unwrap(),
3038 Containment::In
3039 );
3040 assert_eq!(
3041 classify_in_solid_exact(&model, &built.shape, barely_out, T).unwrap(),
3042 Containment::Out
3043 );
3044 assert_eq!(
3046 classify_in_solid_exact(&model, &built.shape, Point::new(0.0, 0.0, 2.0), T).unwrap(),
3047 Containment::On
3048 );
3049 }
3050
3051 #[test]
3052 fn the_exact_classifier_handles_a_cylinder_wall_and_caps() {
3053 let mut model = Model::new();
3054 let built = crate::make_cylinder(&mut model, Frame::WORLD, 1.5, 4.0, T).unwrap();
3055
3056 for (p, want) in [
3057 (Point::new(0.0, 0.0, 2.0), Containment::In),
3058 (Point::new(1.5 - 1e-5, 0.0, 2.0), Containment::In),
3059 (Point::new(1.5 + 1e-5, 0.0, 2.0), Containment::Out),
3060 (Point::new(0.3, 0.4, 4.0 - 1e-5), Containment::In),
3061 (Point::new(0.3, 0.4, 4.0 + 1e-5), Containment::Out),
3062 (Point::new(1.5, 0.0, 2.0), Containment::On),
3063 (Point::new(0.3, 0.4, 0.0), Containment::On),
3064 ] {
3065 assert_eq!(
3066 classify_in_solid_exact(&model, &built.shape, p, T).unwrap(),
3067 want,
3068 "{p:?}"
3069 );
3070 }
3071 }
3072
3073 #[test]
3074 fn the_exact_classifier_walks_the_general_path_through_a_torus() {
3075 let mut model = Model::new();
3079 let built = crate::make_torus(&mut model, Frame::WORLD, 3.0, 1.0, T).unwrap();
3080
3081 for (p, want) in [
3082 (Point::new(3.0, 0.0, 0.0), Containment::In),
3083 (Point::new(3.0, 0.0, 0.9), Containment::In),
3084 (Point::ORIGIN, Containment::Out),
3085 (Point::new(3.0, 0.0, 1.5), Containment::Out),
3086 (Point::new(5.0, 5.0, 0.0), Containment::Out),
3087 (Point::new(3.0, 0.0, 1.0), Containment::On),
3088 ] {
3089 assert_eq!(
3090 classify_in_solid_exact(&model, &built.shape, p, T).unwrap(),
3091 want,
3092 "{p:?}"
3093 );
3094 }
3095 }
3096
3097 #[test]
3098 fn the_exact_classifier_respects_a_placed_solid() {
3099 let offset = Vector::new(10.0, -20.0, 30.0);
3100 let mut model = Model::new();
3101 let frame = Frame::new(Point::ORIGIN + offset, Direction::Z, Direction::X, T).unwrap();
3102 let built = make_box(&mut model, frame, (2.0, 2.0, 2.0), T).unwrap();
3103
3104 assert_eq!(
3105 classify_in_solid_exact(&model, &built.shape, Point::new(1.0, 1.0, 1.0) + offset, T)
3106 .unwrap(),
3107 Containment::In
3108 );
3109 assert_eq!(
3110 classify_in_solid_exact(&model, &built.shape, Point::new(1.0, 1.0, 1.0), T).unwrap(),
3111 Containment::Out
3112 );
3113 }
3114
3115 #[test]
3116 fn the_exact_classifier_refuses_an_open_boundary() {
3117 let mut model = Model::new();
3118 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3119 let face = explore_unique(&model, &built.shape, ShapeType::Face).unwrap()[0].clone();
3120 assert!(classify_in_solid_exact(&model, &face, Point::ORIGIN, T).is_err());
3121 }
3122
3123 #[test]
3124 fn distance_to_a_triangle_is_measured_from_the_nearest_part_of_it() {
3125 let t = [
3126 Point::ORIGIN,
3127 Point::new(1.0, 0.0, 0.0),
3128 Point::new(0.0, 1.0, 0.0),
3129 ];
3130 assert!((distance_to_triangle(Point::new(0.25, 0.25, 2.0), t) - 2.0).abs() < 1e-12);
3132 assert!((distance_to_triangle(Point::new(-3.0, 0.0, 0.0), t) - 3.0).abs() < 1e-12);
3134 assert!(distance_to_triangle(Point::new(0.25, 0.25, 0.0), t) < 1e-12);
3136 }
3137
3138 fn every_ray_through_a_vertex(model: &mut Model, apex: Point) -> Shape {
3142 let positions: Vec<Point> = RAY_DIRECTIONS
3143 .iter()
3144 .map(|d| apex + Vector::new(d[0], d[1], d[2]))
3145 .collect();
3146 let hull: [[u32; 3]; 8] = [
3149 [3, 1, 4],
3150 [5, 2, 4],
3151 [5, 3, 4],
3152 [5, 3, 2],
3153 [0, 2, 4],
3154 [0, 1, 4],
3155 [0, 3, 2],
3156 [0, 3, 1],
3157 ];
3158 let triangles = hull
3159 .iter()
3160 .map(|&[a, b, c]| {
3161 let [pa, pb, pc] = [a, b, c].map(|i| positions[i as usize]);
3162 if (pb - pa).cross(pc - pa).dot(pa - apex) > 0.0 {
3163 [a, b, c]
3164 } else {
3165 [a, c, b]
3166 }
3167 })
3168 .collect();
3169 let mesh = ogeom_topo::Triangulation {
3170 positions,
3171 triangles,
3172 ..ogeom_topo::Triangulation::default()
3173 };
3174 let options = crate::mesh_solid::MeshSolidOptions {
3175 recognize: false,
3176 ..crate::mesh_solid::MeshSolidOptions::default()
3177 };
3178 crate::mesh_solid::solid_from_mesh(model, &mesh, &options, T)
3179 .unwrap()
3180 .shape
3181 }
3182
3183 #[test]
3188 fn a_point_every_ray_from_which_meets_a_vertex_is_read_by_its_winding_number() {
3189 let mut model = Model::new();
3190 let apex = Point::new(0.3, 0.2, 0.1);
3191 let solid = every_ray_through_a_vertex(&mut model, apex);
3192
3193 let boundary = SolidBoundary::of(&model, &solid, T.confusion() * 1e4, T).unwrap();
3194 assert_eq!(boundary.cast(&model, apex, T).unwrap(), None);
3195 assert_eq!(boundary.holds(&model, apex, T).unwrap(), Containment::In);
3196
3197 let meshed = SolidMesh::of(&model, &solid, fine(), T).unwrap();
3198 for d in RAY_DIRECTIONS {
3199 let ray = Direction::new(Vector::new(d[0], d[1], d[2]), T).unwrap();
3200 assert_eq!(count_crossings(&meshed.triangles, apex, ray, T), None);
3201 }
3202 assert_eq!(meshed.holds(apex, T).unwrap(), Containment::In);
3203
3204 let [a, b, c] = [0, 1, 3].map(|i| {
3206 let d = RAY_DIRECTIONS[i];
3207 apex + Vector::new(d[0], d[1], d[2])
3208 });
3209 let on = Point::new(
3210 (a.x + b.x + c.x) / 3.0,
3211 (a.y + b.y + c.y) / 3.0,
3212 (a.z + b.z + c.z) / 3.0,
3213 );
3214 assert_eq!(boundary.holds(&model, on, T).unwrap(), Containment::On);
3215 assert_eq!(meshed.holds(on, T).unwrap(), Containment::On);
3216 }
3217
3218 #[test]
3222 fn an_open_or_doubled_boundary_is_not_read_by_its_winding_number() {
3223 let mut model = Model::new();
3224 let built = make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
3225 let faces = explore_unique(&model, &built.shape, ShapeType::Face).unwrap();
3226 let open = crate::make_shell(&mut model, &faces[1..]).unwrap().shape;
3227 let boundary = SolidBoundary::of_closed(&model, &open, T.confusion() * 1e4, T).unwrap();
3228 assert_eq!(boundary.wound(&model, Point::new(1.0, 1.0, 1.0), T), None);
3229
3230 let moved = Frame::WORLD.with_origin(Point::new(1.0, 1.0, 1.0));
3231 let other = make_box(&mut model, moved, (2.0, 2.0, 2.0), T).unwrap();
3232 let both = crate::make_compound(&mut model, &[built.shape.clone(), other.shape])
3233 .unwrap()
3234 .shape;
3235 let boundary = SolidBoundary::of(&model, &both, T.confusion() * 1e4, T).unwrap();
3236 assert_eq!(boundary.wound(&model, Point::new(1.5, 1.5, 1.5), T), None);
3237 assert_eq!(
3239 boundary.wound(&model, Point::new(0.5, 0.5, 0.5), T),
3240 Some(Containment::In)
3241 );
3242 assert_eq!(
3243 boundary.wound(&model, Point::new(3.5, 0.5, 0.5), T),
3244 Some(Containment::Out)
3245 );
3246 }
3247
3248 #[test]
3249 fn an_unusable_deflection_is_refused() {
3250 let mut model = Model::new();
3251 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3252 let bad = Deflection {
3253 chord: f64::NAN,
3254 ..Deflection::default()
3255 };
3256 assert!(classify_in_solid(&model, &built.shape, Point::ORIGIN, bad, T).is_err());
3257 }
3258}