1use ogeom_core::{Arena, Key, OgeomResult, Tolerance, Tolerances, ogeom_bail};
26use ogeom_geom::{Curve, PlanarCurve, SurfaceGeometry};
27use ogeom_math::Point;
28use smallvec::SmallVec;
29
30use crate::location::Location;
31use crate::tessellation::Triangulation;
32
33pub type CurveId = Key<Curve>;
35pub type PCurveId = Key<PlanarCurve>;
37pub type SurfaceId = Key<SurfaceGeometry>;
39
40pub type TriangulationId = Key<Triangulation>;
42
43#[derive(Debug, Clone, Default)]
45pub struct GeometryStore {
46 curves: Arena<Curve>,
47 pcurves: Arena<PlanarCurve>,
48 surfaces: Arena<SurfaceGeometry>,
49 triangulations: Arena<Triangulation>,
50 revision: u64,
53 edits: u64,
56}
57
58impl GeometryStore {
59 #[must_use]
61 pub const fn new() -> Self {
62 Self {
63 curves: Arena::new(),
64 pcurves: Arena::new(),
65 surfaces: Arena::new(),
66 triangulations: Arena::new(),
67 revision: 0,
68 edits: 0,
69 }
70 }
71
72 pub(crate) const fn revision(&self) -> u64 {
74 self.revision
75 }
76
77 pub(crate) const fn edits(&self) -> u64 {
80 self.edits
81 }
82
83 pub fn add_curve(&mut self, curve: Curve) -> CurveId {
85 self.curves.insert(curve)
86 }
87
88 pub fn add_pcurve(&mut self, curve: PlanarCurve) -> PCurveId {
90 self.pcurves.insert(curve)
91 }
92
93 pub fn add_surface(&mut self, surface: SurfaceGeometry) -> SurfaceId {
95 self.surfaces.insert(surface)
96 }
97
98 pub fn add_triangulation(&mut self, mesh: Triangulation) -> TriangulationId {
100 self.triangulations.insert(mesh)
101 }
102
103 #[must_use]
105 pub fn curve(&self, id: CurveId) -> Option<&Curve> {
106 self.curves.get(id)
107 }
108
109 #[must_use]
111 pub fn pcurve(&self, id: PCurveId) -> Option<&PlanarCurve> {
112 self.pcurves.get(id)
113 }
114
115 pub fn pcurve_mut(&mut self, id: PCurveId) -> Option<&mut PlanarCurve> {
122 self.edits += 1;
123 self.pcurves.get_mut(id)
124 }
125
126 #[must_use]
128 pub fn surface(&self, id: SurfaceId) -> Option<&SurfaceGeometry> {
129 self.surfaces.get(id)
130 }
131
132 pub fn surface_mut(&mut self, id: SurfaceId) -> Option<&mut SurfaceGeometry> {
139 self.revision += 1;
140 self.edits += 1;
141 self.surfaces.get_mut(id)
142 }
143
144 #[must_use]
146 pub fn triangulation(&self, id: TriangulationId) -> Option<&Triangulation> {
147 self.triangulations.get(id)
148 }
149
150 #[must_use]
152 pub fn counts(&self) -> (usize, usize, usize) {
153 (self.curves.len(), self.pcurves.len(), self.surfaces.len())
154 }
155
156 pub(crate) const fn scopes(&self) -> GeometryScopes {
161 GeometryScopes {
162 curves: self.curves.scope(),
163 pcurves: self.pcurves.scope(),
164 surfaces: self.surfaces.scope(),
165 triangulations: self.triangulations.scope(),
166 }
167 }
168
169 pub(crate) fn appends(&self) -> bool {
174 self.curves.next_index().is_some()
175 && self.pcurves.next_index().is_some()
176 && self.surfaces.next_index().is_some()
177 && self.triangulations.next_index().is_some()
178 }
179
180 pub(crate) fn retain(
183 &mut self,
184 curves: impl Fn(CurveId) -> bool,
185 pcurves: impl Fn(PCurveId) -> bool,
186 surfaces: impl Fn(SurfaceId) -> bool,
187 triangulations: impl Fn(TriangulationId) -> bool,
188 ) {
189 self.curves.retain(|id, _| curves(id));
190 self.pcurves.retain(|id, _| pcurves(id));
191 self.surfaces.retain(|id, _| surfaces(id));
192 self.triangulations.retain(|id, _| triangulations(id));
193 }
194
195 pub(crate) fn append(&mut self, other: Self) -> GeometryOffsets {
201 let offsets = GeometryOffsets {
202 curves: arena_len(&self.curves),
203 pcurves: arena_len(&self.pcurves),
204 surfaces: arena_len(&self.surfaces),
205 triangulations: arena_len(&self.triangulations),
206 };
207 for curve in other.curves.into_values() {
208 self.curves.insert(curve);
209 }
210 for pcurve in other.pcurves.into_values() {
211 self.pcurves.insert(pcurve);
212 }
213 for surface in other.surfaces.into_values() {
214 self.surfaces.insert(surface);
215 }
216 for mesh in other.triangulations.into_values() {
217 self.triangulations.insert(mesh);
218 }
219 offsets
220 }
221
222 #[must_use]
227 pub fn holds(&self, repr: &EdgeRepr) -> bool {
228 match repr {
229 EdgeRepr::Curve3d { curve, .. } => self.curve(*curve).is_some(),
230 EdgeRepr::PCurve { curve, surface, .. } => {
231 self.pcurve(*curve).is_some() && self.surface(*surface).is_some()
232 }
233 EdgeRepr::Seam {
234 forward,
235 reversed,
236 surface,
237 ..
238 } => {
239 self.pcurve(*forward).is_some()
240 && self.pcurve(*reversed).is_some()
241 && self.surface(*surface).is_some()
242 }
243 EdgeRepr::Polyline { .. } => true,
245 EdgeRepr::PolygonOnTriangulation { triangulation, .. } => {
246 self.triangulation(*triangulation).is_some()
247 }
248 }
249 }
250
251 pub fn curves(&self) -> impl Iterator<Item = (CurveId, &Curve)> {
253 self.curves.iter()
254 }
255
256 pub fn pcurves(&self) -> impl Iterator<Item = (PCurveId, &PlanarCurve)> {
258 self.pcurves.iter()
259 }
260
261 pub fn surfaces(&self) -> impl Iterator<Item = (SurfaceId, &SurfaceGeometry)> {
263 self.surfaces.iter()
264 }
265
266 pub fn triangulations(&self) -> impl Iterator<Item = (TriangulationId, &Triangulation)> {
268 self.triangulations.iter()
269 }
270
271 #[must_use]
273 pub fn triangulation_count(&self) -> usize {
274 self.triangulations.len()
275 }
276}
277
278#[derive(Debug, Clone, Copy)]
280pub(crate) struct GeometryScopes {
281 pub curves: u32,
282 pub pcurves: u32,
283 pub surfaces: u32,
284 pub triangulations: u32,
285}
286
287#[derive(Debug, Clone, Copy)]
290pub(crate) struct GeometryOffsets {
291 pub curves: u32,
292 pub pcurves: u32,
293 pub surfaces: u32,
294 pub triangulations: u32,
295}
296
297#[allow(clippy::expect_used, reason = "documented panic; see # Panics")]
304pub(crate) fn arena_len<T>(arena: &ogeom_core::Arena<T>) -> u32 {
305 arena
306 .next_index()
307 .expect("an append onto an arena that refills freed slots")
308}
309
310pub(crate) fn key_is_unbound<T>(key: ogeom_core::Key<T>) -> bool {
313 key.scope() == ogeom_core::UNSCOPED && key.generation() == 0
314}
315
316#[allow(clippy::expect_used, reason = "documented panic; see # Panics")]
326pub(crate) fn shifted_key<T>(key: ogeom_core::Key<T>, offset: u32) -> ogeom_core::Key<T> {
327 ogeom_core::Key::from_parts(
328 key.index()
329 .checked_add(offset)
330 .expect("arena exceeded u32::MAX slots"),
331 key.generation(),
332 )
333}
334
335#[derive(Debug, Clone, PartialEq)]
340#[non_exhaustive]
341pub enum EdgeRepr {
342 Curve3d {
344 curve: CurveId,
346 location: Location,
348 range: (f64, f64),
350 },
351 PCurve {
353 curve: PCurveId,
355 surface: SurfaceId,
357 location: Location,
359 range: (f64, f64),
361 },
362 Seam {
369 forward: PCurveId,
371 reversed: PCurveId,
373 surface: SurfaceId,
375 location: Location,
377 range: (f64, f64),
379 },
380 PolygonOnTriangulation {
388 triangulation: TriangulationId,
390 indices: Vec<u32>,
392 location: Location,
394 },
395 Polyline {
401 points: Vec<Point>,
403 parameters: Vec<f64>,
411 location: Location,
413 deflection: f64,
415 },
416}
417
418impl EdgeRepr {
419 pub(crate) fn rebind(&mut self, geometry: &GeometryScopes, datums: u32) {
424 match self {
425 Self::Curve3d {
426 curve, location, ..
427 } => {
428 *curve = curve.with_scope(geometry.curves);
429 *location = location.with_datum_scope(datums);
430 }
431 Self::PCurve {
432 curve,
433 surface,
434 location,
435 ..
436 } => {
437 *curve = curve.with_scope(geometry.pcurves);
438 *surface = surface.with_scope(geometry.surfaces);
439 *location = location.with_datum_scope(datums);
440 }
441 Self::Seam {
442 forward,
443 reversed,
444 surface,
445 location,
446 ..
447 } => {
448 *forward = forward.with_scope(geometry.pcurves);
449 *reversed = reversed.with_scope(geometry.pcurves);
450 *surface = surface.with_scope(geometry.surfaces);
451 *location = location.with_datum_scope(datums);
452 }
453 Self::Polyline { location, .. } => {
454 *location = location.with_datum_scope(datums);
455 }
456 Self::PolygonOnTriangulation {
457 triangulation,
458 location,
459 ..
460 } => {
461 *triangulation = triangulation.with_scope(geometry.triangulations);
462 *location = location.with_datum_scope(datums);
463 }
464 }
465 }
466
467 pub(crate) fn shift(&mut self, geometry: &GeometryOffsets, datums: u32) {
473 match self {
474 Self::Curve3d {
475 curve, location, ..
476 } => {
477 *curve = shifted_key(*curve, geometry.curves);
478 *location = location.with_datum_offset(datums);
479 }
480 Self::PCurve {
481 curve,
482 surface,
483 location,
484 ..
485 } => {
486 *curve = shifted_key(*curve, geometry.pcurves);
487 *surface = shifted_key(*surface, geometry.surfaces);
488 *location = location.with_datum_offset(datums);
489 }
490 Self::Seam {
491 forward,
492 reversed,
493 surface,
494 location,
495 ..
496 } => {
497 *forward = shifted_key(*forward, geometry.pcurves);
498 *reversed = shifted_key(*reversed, geometry.pcurves);
499 *surface = shifted_key(*surface, geometry.surfaces);
500 *location = location.with_datum_offset(datums);
501 }
502 Self::Polyline { location, .. } => {
503 *location = location.with_datum_offset(datums);
504 }
505 Self::PolygonOnTriangulation {
506 triangulation,
507 location,
508 ..
509 } => {
510 *triangulation = shifted_key(*triangulation, geometry.triangulations);
511 *location = location.with_datum_offset(datums);
512 }
513 }
514 }
515
516 pub(crate) fn is_unbound(&self) -> bool {
519 let local_location = |location: &Location| {
520 location
521 .chain()
522 .iter()
523 .all(|&(datum, _)| key_is_unbound(datum))
524 };
525 match self {
526 Self::Curve3d {
527 curve, location, ..
528 } => key_is_unbound(*curve) && local_location(location),
529 Self::PCurve {
530 curve,
531 surface,
532 location,
533 ..
534 } => key_is_unbound(*curve) && key_is_unbound(*surface) && local_location(location),
535 Self::Seam {
536 forward,
537 reversed,
538 surface,
539 location,
540 ..
541 } => {
542 key_is_unbound(*forward)
543 && key_is_unbound(*reversed)
544 && key_is_unbound(*surface)
545 && local_location(location)
546 }
547 Self::Polyline { location, .. } => local_location(location),
548 Self::PolygonOnTriangulation {
549 triangulation,
550 location,
551 ..
552 } => key_is_unbound(*triangulation) && local_location(location),
553 }
554 }
555
556 #[must_use]
558 pub const fn surface(&self) -> Option<SurfaceId> {
559 match self {
560 Self::PCurve { surface, .. } | Self::Seam { surface, .. } => Some(*surface),
561 Self::Curve3d { .. } | Self::Polyline { .. } | Self::PolygonOnTriangulation { .. } => {
562 None
563 }
564 }
565 }
566
567 #[must_use]
569 pub const fn location(&self) -> Option<&Location> {
570 match self {
571 Self::Curve3d { location, .. }
572 | Self::PCurve { location, .. }
573 | Self::Seam { location, .. }
574 | Self::Polyline { location, .. }
575 | Self::PolygonOnTriangulation { location, .. } => Some(location),
576 }
577 }
578
579 #[must_use]
581 pub const fn range(&self) -> Option<(f64, f64)> {
582 match self {
583 Self::Curve3d { range, .. } | Self::PCurve { range, .. } | Self::Seam { range, .. } => {
584 Some(*range)
585 }
586 Self::Polyline { .. } | Self::PolygonOnTriangulation { .. } => None,
587 }
588 }
589
590 #[must_use]
592 pub const fn is_curve3d(&self) -> bool {
593 matches!(self, Self::Curve3d { .. })
594 }
595
596 #[must_use]
598 pub const fn is_parametric(&self) -> bool {
599 matches!(self, Self::PCurve { .. } | Self::Seam { .. })
600 }
601}
602
603#[derive(Debug, Clone, PartialEq)]
605pub struct VertexData {
606 pub point: Point,
608 pub tolerance: Tolerance,
610}
611
612impl VertexData {
613 #[must_use]
615 pub fn new(point: Point) -> Self {
616 Self {
617 point,
618 tolerance: Tolerance::MIN,
619 }
620 }
621
622 pub fn with_tolerance(point: Point, tolerance: f64) -> OgeomResult<Self> {
629 if !point.is_finite() {
630 ogeom_bail!(Construction, "vertex position is not finite");
631 }
632 Ok(Self {
633 point,
634 tolerance: Tolerance::new(tolerance)?,
635 })
636 }
637
638 pub fn widen(&mut self, to: Tolerance) {
640 self.tolerance = self.tolerance.widen(to);
641 }
642}
643
644#[derive(Debug, Clone, PartialEq)]
647pub struct EdgeData {
648 pub tolerance: Tolerance,
650 pub representations: SmallVec<[EdgeRepr; 3]>,
652 same_parameter: bool,
656 pub degenerate: bool,
662}
663
664impl EdgeData {
665 #[must_use]
667 pub fn new() -> Self {
668 Self {
669 tolerance: Tolerance::MIN,
670 representations: SmallVec::new(),
671 same_parameter: true,
672 degenerate: false,
673 }
674 }
675
676 #[must_use]
678 pub fn on_curve(curve: CurveId, location: Location, range: (f64, f64)) -> Self {
679 let mut edge = Self::new();
680 edge.representations.push(EdgeRepr::Curve3d {
681 curve,
682 location,
683 range,
684 });
685 edge
686 }
687
688 pub fn add(&mut self, repr: EdgeRepr) {
694 self.representations.push(repr);
695 self.same_parameter = false;
696 }
697
698 #[must_use]
709 pub const fn same_parameter(&self) -> bool {
710 self.same_parameter
711 }
712
713 pub const fn assert_same_parameter(&mut self, agrees: bool) {
721 self.same_parameter = agrees;
722 }
723
724 #[must_use]
726 pub fn curve3d(&self) -> Option<&EdgeRepr> {
727 self.representations.iter().find(|r| r.is_curve3d())
728 }
729
730 #[must_use]
732 pub fn pcurve_on(&self, surface: SurfaceId) -> Option<&EdgeRepr> {
733 self.representations
734 .iter()
735 .find(|r| r.surface() == Some(surface))
736 }
737
738 #[must_use]
750 pub fn pcurve_for(&self, surface: SurfaceId, location: &Location) -> Option<&EdgeRepr> {
751 self.representations
752 .iter()
753 .find(|r| r.surface() == Some(surface) && r.location() == Some(location))
754 .or_else(|| {
755 self.representations
759 .iter()
760 .filter(|r| {
761 r.surface() == Some(surface)
762 && r.location()
763 .is_some_and(|l| !l.is_identity() && location.ends_with(l))
764 })
765 .max_by_key(|r| r.location().map_or(0, Location::depth))
766 })
767 .or_else(|| {
768 self.representations.iter().find(|r| {
769 r.surface() == Some(surface) && r.location().is_some_and(Location::is_identity)
770 })
771 })
772 }
773
774 #[must_use]
776 pub fn parametric_surfaces(&self) -> Vec<SurfaceId> {
777 self.representations
778 .iter()
779 .filter_map(EdgeRepr::surface)
780 .collect()
781 }
782
783 pub fn widen(&mut self, to: Tolerance) {
785 self.tolerance = self.tolerance.widen(to);
786 }
787}
788
789impl Default for EdgeData {
790 fn default() -> Self {
791 Self::new()
792 }
793}
794
795#[derive(Debug, Clone, PartialEq)]
802pub struct FaceData {
803 pub surface: SurfaceId,
805 pub location: Location,
807 pub tolerance: Tolerance,
809 pub natural_restriction: bool,
816 pub triangulation: Option<TriangulationId>,
822}
823
824impl FaceData {
825 #[must_use]
827 pub fn new(surface: SurfaceId, location: Location) -> Self {
828 Self {
829 surface,
830 location,
831 tolerance: Tolerance::MIN,
832 natural_restriction: false,
833 triangulation: None,
834 }
835 }
836
837 #[must_use]
839 pub fn natural(surface: SurfaceId, location: Location) -> Self {
840 Self {
841 natural_restriction: true,
842 ..Self::new(surface, location)
843 }
844 }
845
846 pub fn widen(&mut self, to: Tolerance) {
848 self.tolerance = self.tolerance.widen(to);
849 }
850}
851
852#[derive(Debug, Clone, PartialEq)]
858pub enum NodeData {
859 Vertex(VertexData),
861 Edge(Box<EdgeData>),
863 Face(Box<FaceData>),
865 Container,
868}
869
870impl NodeData {
871 #[must_use]
876 pub fn tolerance(&self) -> Option<Tolerance> {
877 match self {
878 Self::Vertex(v) => Some(v.tolerance),
879 Self::Edge(e) => Some(e.tolerance),
880 Self::Face(f) => Some(f.tolerance),
881 Self::Container => None,
882 }
883 }
884
885 pub fn widen(&mut self, to: Tolerance) {
887 match self {
888 Self::Vertex(v) => v.widen(to),
889 Self::Edge(e) => e.widen(to),
890 Self::Face(f) => f.widen(to),
891 Self::Container => {}
892 }
893 }
894
895 pub(crate) fn set_tolerance(&mut self, to: Tolerance) {
897 match self {
898 Self::Vertex(v) => v.tolerance = to,
899 Self::Edge(e) => e.tolerance = to,
900 Self::Face(f) => f.tolerance = to,
901 Self::Container => {}
902 }
903 }
904
905 #[must_use]
907 pub const fn as_vertex(&self) -> Option<&VertexData> {
908 match self {
909 Self::Vertex(v) => Some(v),
910 _ => None,
911 }
912 }
913
914 #[must_use]
916 pub const fn as_edge(&self) -> Option<&EdgeData> {
917 match self {
918 Self::Edge(e) => Some(e),
919 _ => None,
920 }
921 }
922
923 #[must_use]
925 pub const fn as_face(&self) -> Option<&FaceData> {
926 match self {
927 Self::Face(f) => Some(f),
928 _ => None,
929 }
930 }
931}
932
933pub fn check_containment(bounding: Tolerance, bounded: Tolerance) -> OgeomResult<()> {
946 ogeom_core::check_containment(bounding, bounded)
947}
948
949#[must_use]
955pub fn enforce_containment(bounding: Tolerance, bounded: Tolerance) -> Tolerance {
956 bounding.widen(bounded)
957}
958
959pub fn check_range(range: (f64, f64), tol: Tolerances) -> OgeomResult<()> {
966 let (a, b) = range;
967 if !a.is_finite() || !b.is_finite() || b <= a + tol.parametric() {
968 ogeom_bail!(
969 Construction,
970 "parameter range [{a}, {b}] is empty or non-finite"
971 );
972 }
973 Ok(())
974}
975
976#[cfg(test)]
977#[allow(clippy::unwrap_used)]
978mod tests {
979 use super::*;
980 use ogeom_geom::{CircleCurve, LineCurve, PlaneSurface};
981 use ogeom_math::{Circle, Direction, Frame, Plane};
982
983 const T: Tolerances = Tolerances::millimetres();
984
985 fn store() -> (GeometryStore, CurveId, SurfaceId, PCurveId) {
986 let mut s = GeometryStore::new();
987 let curve = s.add_curve(
988 LineCurve::segment(Point::ORIGIN, Point::new(10.0, 0.0, 0.0), T)
989 .unwrap()
990 .into(),
991 );
992 let surface = s.add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
993 let pcurve = s.add_pcurve(
994 ogeom_geom::Line2d::segment(
995 ogeom_math::Point2::ORIGIN,
996 ogeom_math::Point2::new(10.0, 0.0),
997 T,
998 )
999 .unwrap()
1000 .into(),
1001 );
1002 (s, curve, surface, pcurve)
1003 }
1004
1005 #[test]
1006 fn the_geometry_store_hands_back_what_it_was_given() {
1007 let (s, curve, surface, pcurve) = store();
1008 assert!(s.curve(curve).is_some());
1009 assert!(s.surface(surface).is_some());
1010 assert!(s.pcurve(pcurve).is_some());
1011 assert_eq!(s.counts(), (1, 1, 1));
1012
1013 let mut other = GeometryStore::new();
1019 let mut beyond = curve;
1020 for _ in 0..5 {
1021 beyond = other.add_curve(
1022 LineCurve::segment(Point::ORIGIN, Point::new(1.0, 0.0, 0.0), T)
1023 .unwrap()
1024 .into(),
1025 );
1026 }
1027 assert!(
1028 s.curve(beyond).is_none(),
1029 "a handle past the end does not resolve"
1030 );
1031 }
1032
1033 #[test]
1034 fn tolerances_start_at_the_minimum_and_only_widen() {
1035 let mut v = VertexData::new(Point::ORIGIN);
1036 assert_eq!(v.tolerance, Tolerance::MIN);
1037
1038 let wide = Tolerance::new(1e-3).unwrap();
1039 v.widen(wide);
1040 assert_eq!(v.tolerance, wide);
1041
1042 v.widen(Tolerance::new(1e-9).unwrap());
1046 assert_eq!(v.tolerance, wide);
1047 }
1048
1049 #[test]
1050 fn degenerate_vertex_data_is_refused() {
1051 assert!(VertexData::with_tolerance(Point::ORIGIN, -1.0).is_err());
1052 assert!(VertexData::with_tolerance(Point::ORIGIN, f64::NAN).is_err());
1053 assert!(VertexData::with_tolerance(Point::new(f64::INFINITY, 0.0, 0.0), 1e-6).is_err());
1054 assert!(VertexData::with_tolerance(Point::ORIGIN, 1e-3).is_ok());
1055 }
1056
1057 #[test]
1058 fn the_containment_rule_holds_downward_and_is_repaired_upward() {
1059 let fine = Tolerance::new(1e-6).unwrap();
1060 let coarse = Tolerance::new(1e-3).unwrap();
1061
1062 assert!(
1063 check_containment(coarse, fine).is_ok(),
1064 "vertex coarser than edge"
1065 );
1066 assert!(
1067 check_containment(fine, coarse).is_err(),
1068 "and not the other way"
1069 );
1070
1071 let repaired = enforce_containment(fine, coarse);
1073 assert_eq!(repaired, coarse);
1074 assert!(check_containment(repaired, coarse).is_ok());
1075 }
1076
1077 #[test]
1078 fn an_edge_holds_several_representations_at_once() {
1079 let (mut s, curve, surface, pcurve) = store();
1083 let other_surface =
1084 s.add_surface(PlaneSurface::new(Plane::through(Point::ORIGIN, Direction::Y)).into());
1085 let other_pcurve = s.add_pcurve(
1086 ogeom_geom::Line2d::segment(
1087 ogeom_math::Point2::ORIGIN,
1088 ogeom_math::Point2::new(0.0, 10.0),
1089 T,
1090 )
1091 .unwrap()
1092 .into(),
1093 );
1094
1095 let mut edge = EdgeData::on_curve(curve, Location::identity(), (0.0, 10.0));
1096 edge.add(EdgeRepr::PCurve {
1097 curve: pcurve,
1098 surface,
1099 location: Location::identity(),
1100 range: (0.0, 10.0),
1101 });
1102 edge.add(EdgeRepr::PCurve {
1103 curve: other_pcurve,
1104 surface: other_surface,
1105 location: Location::identity(),
1106 range: (0.0, 10.0),
1107 });
1108
1109 assert_eq!(edge.representations.len(), 3);
1110 assert!(edge.curve3d().is_some());
1111 assert!(edge.pcurve_on(surface).is_some());
1112 assert!(edge.pcurve_on(other_surface).is_some());
1113 assert_eq!(edge.parametric_surfaces().len(), 2);
1114 }
1115
1116 #[test]
1117 fn adding_a_representation_withdraws_the_same_parameter_claim() {
1118 let (_, curve, surface, pcurve) = store();
1123 let mut edge = EdgeData::on_curve(curve, Location::identity(), (0.0, 10.0));
1124 assert!(
1125 edge.same_parameter(),
1126 "a lone curve trivially agrees with itself"
1127 );
1128
1129 edge.add(EdgeRepr::PCurve {
1130 curve: pcurve,
1131 surface,
1132 location: Location::identity(),
1133 range: (0.0, 10.0),
1134 });
1135 assert!(
1136 !edge.same_parameter(),
1137 "the new representation is unverified"
1138 );
1139
1140 edge.assert_same_parameter(true);
1141 assert!(edge.same_parameter());
1142 }
1143
1144 #[test]
1145 fn a_seam_carries_two_pcurves_because_one_cannot_express_both_sides() {
1146 let mut s = GeometryStore::new();
1150 let cylinder = s.add_surface(
1151 ogeom_geom::CylinderSurface::new(
1152 ogeom_math::Cylinder::new(Frame::WORLD, 2.0, T).unwrap(),
1153 (0.0, 5.0),
1154 )
1155 .unwrap()
1156 .into(),
1157 );
1158 let at_zero = s.add_pcurve(
1159 ogeom_geom::Line2d::segment(
1160 ogeom_math::Point2::ORIGIN,
1161 ogeom_math::Point2::new(0.0, 5.0),
1162 T,
1163 )
1164 .unwrap()
1165 .into(),
1166 );
1167 let at_tau = s.add_pcurve(
1168 ogeom_geom::Line2d::segment(
1169 ogeom_math::Point2::new(core::f64::consts::TAU, 0.0),
1170 ogeom_math::Point2::new(core::f64::consts::TAU, 5.0),
1171 T,
1172 )
1173 .unwrap()
1174 .into(),
1175 );
1176
1177 let mut edge = EdgeData::new();
1178 edge.add(EdgeRepr::Seam {
1179 forward: at_zero,
1180 reversed: at_tau,
1181 surface: cylinder,
1182 location: Location::identity(),
1183 range: (0.0, 5.0),
1184 });
1185
1186 let seam = &edge.representations[0];
1187 assert!(seam.is_parametric());
1188 assert_eq!(seam.surface(), Some(cylinder));
1189 assert_eq!(seam.range(), Some((0.0, 5.0)));
1190 assert!(matches!(seam, EdgeRepr::Seam { .. }));
1191 }
1192
1193 #[test]
1194 fn a_polyline_representation_records_what_it_was_built_to() {
1195 let repr = EdgeRepr::Polyline {
1199 points: vec![Point::ORIGIN, Point::new(1.0, 0.0, 0.0)],
1200 parameters: vec![0.0, 1.0],
1201 location: Location::identity(),
1202 deflection: 1e-3,
1203 };
1204 assert!(!repr.is_curve3d() && !repr.is_parametric());
1205 assert_eq!(repr.surface(), None);
1206 assert_eq!(repr.range(), None, "a polyline has no parameter range");
1207 }
1208
1209 #[test]
1210 fn a_degenerate_edge_is_marked_rather_than_dropped() {
1211 let mut edge = EdgeData::new();
1214 edge.degenerate = true;
1215 assert!(edge.degenerate);
1216 assert!(edge.representations.is_empty());
1217 }
1218
1219 #[test]
1220 fn a_natural_face_covers_its_whole_surface() {
1221 let (_, _, surface, _) = store();
1222 let trimmed = FaceData::new(surface, Location::identity());
1223 let whole = FaceData::natural(surface, Location::identity());
1224 assert!(!trimmed.natural_restriction);
1225 assert!(whole.natural_restriction);
1226 assert_eq!(whole.tolerance, Tolerance::MIN);
1227 }
1228
1229 #[test]
1230 fn node_data_exposes_only_what_it_holds() {
1231 let vertex = NodeData::Vertex(VertexData::new(Point::ORIGIN));
1232 let edge = NodeData::Edge(Box::default());
1233 let container = NodeData::Container;
1234
1235 assert!(vertex.as_vertex().is_some());
1236 assert!(vertex.as_edge().is_none());
1237 assert!(edge.as_edge().is_some());
1238 assert!(edge.as_face().is_none());
1239
1240 assert_eq!(container.tolerance(), None);
1244 assert_eq!(vertex.tolerance(), Some(Tolerance::MIN));
1245 }
1246
1247 #[test]
1248 fn widening_a_container_is_a_no_op_rather_than_an_error() {
1249 let mut container = NodeData::Container;
1253 container.widen(Tolerance::new(1e-3).unwrap());
1254 assert_eq!(container.tolerance(), None);
1255 }
1256
1257 #[test]
1258 fn empty_parameter_ranges_are_refused() {
1259 assert!(check_range((0.0, 1.0), T).is_ok());
1260 assert!(check_range((1.0, 0.0), T).is_err());
1261 assert!(check_range((1.0, 1.0), T).is_err());
1262 assert!(check_range((0.0, f64::NAN), T).is_err());
1263 }
1264
1265 #[test]
1266 fn a_circular_edge_can_carry_its_own_curve() {
1267 let mut s = GeometryStore::new();
1268 let circle =
1269 s.add_curve(CircleCurve::new(Circle::new(Frame::WORLD, 3.0, T).unwrap()).into());
1270 let edge = EdgeData::on_curve(circle, Location::identity(), (0.0, core::f64::consts::TAU));
1271 assert!(edge.curve3d().is_some());
1272 assert_eq!(
1273 edge.curve3d().unwrap().range(),
1274 Some((0.0, core::f64::consts::TAU))
1275 );
1276 assert!(edge.parametric_surfaces().is_empty());
1277 }
1278}