1use ogeom_core::{
18 Arena, EntityId, Key, OgeomResult, OpId, Provenance, ProvenanceTable, Role, Tolerance,
19 Tolerances, ogeom_bail,
20};
21use ogeom_math::{Aabb, Point, Transform, TransformKind};
22
23use smallvec::SmallVec;
24
25use crate::entity::{
26 CurveId, EdgeData, EdgeRepr, FaceData, NodeData, PCurveId, SurfaceId, TriangulationId,
27 VertexData,
28};
29use crate::kept::{FaceBoxes, KeptReads, NodeSet};
30use crate::location::{DatumId, DatumStore, Location};
31use crate::shape::{Orientation, Shape, ShapeType, TShape, TShapeId};
32
33pub use crate::entity::GeometryStore;
34
35#[derive(Debug, Clone, Default)]
38pub struct Model {
39 nodes: Nodes,
40 datums: DatumStore,
41 geometry: GeometryStore,
42 provenance: ProvenanceTable,
43 current_op: OpId,
44 tolerances: Tolerances,
45 face_boxes: FaceBoxes,
46 any_held: bool,
48 widened: Option<Vec<(TShapeId, Tolerance)>>,
52}
53
54#[derive(Debug, Clone)]
56struct Node {
57 shape: TShape,
58 identity: Option<EntityId>,
60 face_box: Option<u32>,
62 held_by: SmallVec<[u32; 2]>,
65 held: bool,
69}
70
71#[derive(Debug, Clone, Default)]
74struct Nodes {
75 arena: Arena<Node>,
76 reads: KeptReads,
80}
81
82#[inline]
84const fn arena_key(id: TShapeId) -> Key<Node> {
85 Key::from_parts(id.index(), id.generation()).with_scope(id.scope())
86}
87
88#[inline]
90const fn handle(key: Key<Node>) -> TShapeId {
91 Key::from_parts(key.index(), key.generation()).with_scope(key.scope())
92}
93
94impl Nodes {
95 #[inline]
96 fn entry(&self, id: TShapeId) -> Option<&Node> {
97 self.arena.get(arena_key(id))
98 }
99
100 #[inline]
104 fn entry_mut(&mut self, id: TShapeId) -> Option<&mut Node> {
105 self.arena.get_mut(arena_key(id))
106 }
107
108 #[inline]
109 fn get(&self, id: TShapeId) -> Option<&TShape> {
110 self.entry(id).map(|node| &node.shape)
111 }
112
113 #[inline]
116 fn get_mut(&mut self, id: TShapeId) -> Option<&mut TShape> {
117 self.reads.forget(id.index());
118 self.entry_mut(id).map(|node| &mut node.shape)
119 }
120
121 fn insert(&mut self, node: Node) -> TShapeId {
122 handle(self.arena.insert(node))
123 }
124
125 fn iter(&self) -> impl Iterator<Item = (TShapeId, &Node)> {
126 self.arena.iter().map(|(key, node)| (handle(key), node))
127 }
128
129 fn iter_mut(&mut self) -> impl Iterator<Item = (TShapeId, &mut Node)> {
130 self.arena.iter_mut().map(|(key, node)| (handle(key), node))
131 }
132
133 fn at(&self, index: u32) -> Option<TShapeId> {
135 self.arena.key_at(index).map(handle)
136 }
137
138 const fn scope(&self) -> u32 {
139 self.arena.scope()
140 }
141
142 const fn issued(&self, id: TShapeId) -> bool {
143 id.scope() == self.arena.scope()
144 }
145
146 const fn len(&self) -> usize {
147 self.arena.len()
148 }
149
150 const fn is_empty(&self) -> bool {
151 self.arena.is_empty()
152 }
153}
154
155#[derive(Default)]
157struct Reach {
158 nodes: ogeom_core::FastSet<u32>,
160 curves: ogeom_core::FastSet<CurveId>,
161 pcurves: ogeom_core::FastSet<PCurveId>,
162 surfaces: ogeom_core::FastSet<SurfaceId>,
163 meshes: ogeom_core::FastSet<TriangulationId>,
164 entities: ogeom_core::FastSet<EntityId>,
165}
166
167impl Model {
168 #[must_use]
170 pub fn new() -> Self {
171 Self::with_tolerances(Tolerances::millimetres())
172 }
173
174 #[must_use]
184 pub fn with_tolerances(tolerances: Tolerances) -> Self {
185 Self {
186 nodes: Nodes::default(),
187 datums: DatumStore::new(),
188 geometry: GeometryStore::new(),
189 provenance: ProvenanceTable::new(),
190 current_op: OpId(0),
191 tolerances,
192 face_boxes: FaceBoxes::default(),
193 any_held: false,
194 widened: None,
195 }
196 }
197
198 #[must_use]
200 pub const fn tolerances(&self) -> Tolerances {
201 self.tolerances
202 }
203
204 pub fn from_parts(parts: ModelParts) -> OgeomResult<Self> {
228 let mut model = Self::with_tolerances(parts.tolerances);
229 model.current_op = parts.current_op;
230 model.absorb_core(parts)?;
235 Ok(model)
236 }
237
238 pub fn absorb(&mut self, parts: ModelParts, roots: &[Shape]) -> OgeomResult<Absorbed> {
291 #[allow(clippy::float_cmp, reason = "scales are copied, never computed")]
292 if parts.tolerances.scale() != self.tolerances.scale() {
293 ogeom_bail!(
294 Construction,
295 "these parts were authored at {} mm per unit and this model at \
296 {}; absorbing across scales needs a rescale, which is its own \
297 operation",
298 parts.tolerances.scale(),
299 self.tolerances.scale()
300 );
301 }
302 if self.nodes.arena.next_index().is_none()
303 || !self.datums.is_dense()
304 || !self.geometry.appends()
305 {
306 ogeom_bail!(
307 Construction,
308 "absorb appends by offset, and this model's arenas are set to \
309 refill freed slots, which nothing in this crate does"
310 );
311 }
312 Self::check_parts_unbound(&parts, roots)?;
313
314 let absorbed_entities = parts.provenance.len() as u64;
315 let (node_offset, datum_offset, entity_offset) = self.absorb_core(parts)?;
316
317 let shapes = roots
318 .iter()
319 .map(|root| self.bind(&root.shifted(node_offset, datum_offset)))
320 .collect::<OgeomResult<Vec<Shape>>>()?;
321 let entities = (1..=absorbed_entities)
322 .filter_map(|raw| {
323 Some((
324 EntityId::from_raw(raw)?,
325 EntityId::from_raw(raw + entity_offset)?,
326 ))
327 })
328 .collect();
329 Ok(Absorbed { shapes, entities })
330 }
331
332 fn absorb_core(&mut self, parts: ModelParts) -> OgeomResult<(u32, u32, u64)> {
338 let ModelParts {
339 mut nodes,
340 datums,
341 geometry,
342 provenance,
343 identity,
344 current_op: _,
345 tolerances: _,
346 } = parts;
347
348 for (issued, entry) in provenance.iter().enumerate() {
352 for source in entry.inputs() {
353 if source.get() > issued as u64 {
354 ogeom_bail!(
355 Dangling,
356 "an entity is derived from identity {}, which no entry \
357 before it issued",
358 source.get()
359 );
360 }
361 }
362 }
363
364 let node_offset = crate::entity::arena_len(&self.nodes.arena);
365 let datum_offset = u32::try_from(self.datums.len()).unwrap_or(u32::MAX);
366 let entity_offset = self.provenance.len() as u64;
367 let geometry_offsets = self.geometry.append(geometry);
368
369 for node in &mut nodes {
370 for child in node.children_mut() {
371 *child = child.shifted(node_offset, datum_offset);
372 }
373 match node.data_mut() {
374 NodeData::Edge(edge) => {
375 for repr in &mut edge.representations {
376 repr.shift(&geometry_offsets, datum_offset);
377 }
378 }
379 NodeData::Face(face) => {
380 face.surface =
381 crate::entity::shifted_key(face.surface, geometry_offsets.surfaces);
382 face.triangulation = face.triangulation.map(|mesh| {
383 crate::entity::shifted_key(mesh, geometry_offsets.triangulations)
384 });
385 face.location = face.location.with_datum_offset(datum_offset);
386 }
387 NodeData::Vertex(_) | NodeData::Container => {}
388 }
389 }
390
391 for datum in datums {
392 self.datums.insert(datum);
393 }
394 for mut entry in provenance {
395 if let Provenance::Derived { from, .. } = &mut entry {
396 for source in from.iter_mut() {
397 let Some(shifted) = EntityId::from_raw(source.get() + entity_offset) else {
398 ogeom_bail!(Construction, "an entity id overflowed in the shift");
399 };
400 *source = shifted;
401 }
402 }
403 self.provenance.record(entry);
404 }
405 self.sync_face_boxes();
406 let appended: Vec<TShapeId> = nodes
407 .into_iter()
408 .map(|node| self.insert_unlinked(node))
409 .collect();
410
411 self.bind_handles(node_offset);
417 for id in appended {
418 self.link_children(id, true);
419 }
420 let identity: Vec<(TShapeId, EntityId)> = identity
421 .into_iter()
422 .map(|(node, entity)| {
423 let Some(shifted) = EntityId::from_raw(entity.get() + entity_offset) else {
424 ogeom_bail!(Construction, "an entity id overflowed in the shift");
425 };
426 Ok((
427 crate::entity::shifted_key(node, node_offset).with_scope(self.nodes.scope()),
428 shifted,
429 ))
430 })
431 .collect::<OgeomResult<_>>()?;
432
433 self.check_restored(&identity, node_offset)?;
434 for (node, entity) in identity {
435 if let Some(node) = self.nodes.entry_mut(node) {
436 node.identity = Some(entity);
437 }
438 }
439 Ok((node_offset, datum_offset, entity_offset))
440 }
441
442 fn check_parts_unbound(parts: &ModelParts, roots: &[Shape]) -> OgeomResult<()> {
446 use crate::entity::key_is_unbound;
447
448 let local_location = |location: &Location| {
449 location
450 .chain()
451 .iter()
452 .all(|&(datum, _)| key_is_unbound(datum))
453 };
454 let local_shape =
455 |shape: &Shape| key_is_unbound(shape.node()) && local_location(shape.location());
456
457 let mut sound = parts.identity.iter().all(|&(node, _)| key_is_unbound(node))
458 && roots.iter().all(local_shape);
459 for node in &parts.nodes {
460 sound = sound && node.children().iter().all(local_shape);
461 match node.data() {
462 NodeData::Edge(edge) => {
463 sound = sound && edge.representations.iter().all(EdgeRepr::is_unbound);
464 }
465 NodeData::Face(face) => {
466 sound = sound
467 && key_is_unbound(face.surface)
468 && face.triangulation.is_none_or(key_is_unbound)
469 && local_location(&face.location);
470 }
471 NodeData::Vertex(_) | NodeData::Container => {}
472 }
473 }
474 if !sound {
475 ogeom_bail!(
476 Construction,
477 "these parts carry handles already bound to an arena, or at a \
478 recycled generation; absorb takes parts exactly as a reader \
479 rebuilt them, and serialization is the one road in"
480 );
481 }
482 Ok(())
483 }
484
485 pub fn bind(&self, shape: &Shape) -> OgeomResult<Shape> {
504 if shape.node().scope() != ogeom_core::UNSCOPED && !self.nodes.issued(shape.node()) {
505 ogeom_bail!(
506 Construction,
507 "this shape belongs to another model; binding it here would \
508 make it resolve and answer about a different entity"
509 );
510 }
511 let bound = shape.rebound(self.nodes.scope(), self.datums.scope());
512 if self.nodes.get(bound.node()).is_none() {
513 ogeom_bail!(Dangling, "shape refers to a node not in this model");
514 }
515 Ok(bound)
516 }
517
518 pub fn bind_location(&self, location: &Location) -> OgeomResult<Location> {
531 let bound = location.with_datum_scope(self.datums.scope());
532 for &(datum, _) in bound.chain() {
533 if self.datums.get(datum).is_none() {
534 ogeom_bail!(Dangling, "location refers to a datum not in this model");
535 }
536 }
537 Ok(bound)
538 }
539
540 fn bind_handles(&mut self, from: u32) {
545 let nodes = self.nodes.scope();
546 let datums = self.datums.scope();
547 let geometry = self.geometry.scopes();
548
549 for (_, node) in self.nodes.iter_mut().filter(|(id, _)| id.index() >= from) {
550 let node = &mut node.shape;
551 for child in node.children_mut() {
552 *child = child.rebound(nodes, datums);
553 }
554 match node.data_mut() {
555 NodeData::Edge(edge) => {
556 for repr in &mut edge.representations {
557 repr.rebind(&geometry, datums);
558 }
559 }
560 NodeData::Face(face) => {
561 face.surface = face.surface.with_scope(geometry.surfaces);
562 face.triangulation = face
563 .triangulation
564 .map(|mesh| mesh.with_scope(geometry.triangulations));
565 face.location = face.location.with_datum_scope(datums);
566 }
567 NodeData::Vertex(_) | NodeData::Container => {}
568 }
569 }
570 }
571
572 fn check_restored(&self, identity: &[(TShapeId, EntityId)], from: u32) -> OgeomResult<()> {
579 for (id, node) in self.nodes.iter().filter(|(id, _)| id.index() >= from) {
580 let node = &node.shape;
581 let kind = node.kind();
582 match (kind, node.data()) {
583 (ShapeType::Vertex, NodeData::Vertex(_))
584 | (ShapeType::Edge, NodeData::Edge(_))
585 | (ShapeType::Face, NodeData::Face(_)) => {}
586 (
587 ShapeType::Wire
588 | ShapeType::Shell
589 | ShapeType::Solid
590 | ShapeType::CompSolid
591 | ShapeType::Compound,
592 NodeData::Container,
593 ) => {}
594 (kind, data) => {
595 ogeom_bail!(Construction, "node {id:?} is a {kind:?} and holds {data:?}")
596 }
597 }
598 self.check_node_geometry(id, node)?;
599
600 let expected = kind.child_type();
603 for child in node.children() {
604 let Some(below) = self.nodes.get(child.node()) else {
605 ogeom_bail!(Dangling, "node {id:?} names a child that is not there");
606 };
607 if let Some(expected) = expected
608 && kind != ShapeType::Compound
609 && below.kind() != expected
610 {
611 ogeom_bail!(
612 Construction,
613 "a {kind:?} takes {expected:?} children; node {id:?} \
614 names a {:?}",
615 below.kind()
616 );
617 }
618 self.check_location(child.location())?;
619 }
620 }
621 for (node, entity) in identity {
622 if self.nodes.get(*node).is_none() {
623 ogeom_bail!(Dangling, "an identity is bound to a node that is not there");
624 }
625 if entity.get() > self.provenance.len() as u64 {
626 ogeom_bail!(
627 Dangling,
628 "node {node:?} claims identity {}, which was never issued",
629 entity.get()
630 );
631 }
632 }
633 Ok(())
634 }
635
636 fn check_node_geometry(&self, id: TShapeId, node: &TShape) -> OgeomResult<()> {
638 match node.data() {
639 NodeData::Edge(data) => {
640 for repr in &data.representations {
641 if let Some(location) = repr.location() {
642 self.check_location(location)?;
643 }
644 if !self.geometry.holds(repr) {
645 ogeom_bail!(
646 Dangling,
647 "edge {id:?} names geometry that is not in this model"
648 );
649 }
650 }
651 }
652 NodeData::Face(data) => {
653 self.check_location(&data.location)?;
654 if self.geometry.surface(data.surface).is_none() {
655 ogeom_bail!(Dangling, "face {id:?} names a surface that is not there");
656 }
657 if let Some(mesh) = data.triangulation
658 && self.geometry.triangulation(mesh).is_none()
659 {
660 ogeom_bail!(
661 Dangling,
662 "face {id:?} names a triangulation that is not there"
663 );
664 }
665 }
666 NodeData::Vertex(_) | NodeData::Container => {}
667 }
668 Ok(())
669 }
670
671 fn check_location(&self, location: &Location) -> OgeomResult<()> {
673 for &(datum, _) in location.chain() {
674 if self.datums.get(datum).is_none() {
675 ogeom_bail!(Dangling, "a placement names a datum that is not there");
676 }
677 }
678 Ok(())
679 }
680
681 pub const fn begin_operation(&mut self) -> OpId {
688 self.current_op = OpId(self.current_op.0 + 1);
689 self.current_op
690 }
691
692 #[must_use]
694 pub const fn current_operation(&self) -> OpId {
695 self.current_op
696 }
697
698 #[must_use]
704 pub fn identity_of(&self, shape: &Shape) -> Option<EntityId> {
705 self.nodes.entry(shape.node())?.identity
706 }
707
708 #[must_use]
710 pub fn provenance_of(&self, shape: &Shape) -> Option<&Provenance> {
711 self.provenance.get(self.identity_of(shape)?)
712 }
713
714 #[must_use]
716 pub const fn provenance(&self) -> &ProvenanceTable {
717 &self.provenance
718 }
719
720 pub fn forget_provenance(&mut self, ids: impl IntoIterator<Item = EntityId>) {
728 for id in ids {
729 self.provenance.forget(id);
730 }
731 }
732
733 pub fn retain_reachable(&mut self, roots: &[Shape]) -> OgeomResult<()> {
754 if roots.iter().any(|root| self.node(root).is_none()) {
755 ogeom_bail!(Dangling, "a root to retain is not in this model");
756 }
757 if self.widened.is_some() {
758 ogeom_bail!(
759 Construction,
760 "an operation is noting the tolerances it widens; a model is \
761 retained between operations"
762 );
763 }
764 let reach = self.reach(roots);
765
766 self.nodes
767 .arena
768 .retain(|key, _| reach.nodes.contains(&key.index()));
769 self.nodes
771 .reads
772 .retain(|shape| reach.nodes.contains(&shape.node().index()));
773 let mut kept_boxes: Vec<u32> = Vec::new();
774 self.any_held = false;
775 for (_, node) in self.nodes.iter_mut() {
776 self.any_held |= node.held;
777 node.held_by.retain(|above| reach.nodes.contains(above));
778 if let Some(slot) = node.face_box.as_mut() {
779 kept_boxes.push(*slot);
780 *slot = u32::try_from(kept_boxes.len() - 1).unwrap_or(u32::MAX);
781 }
782 }
783 self.face_boxes.keep_slots(&kept_boxes);
784 self.geometry.retain(
785 |id| reach.curves.contains(&id),
786 |id| reach.pcurves.contains(&id),
787 |id| reach.surfaces.contains(&id),
788 |id| reach.meshes.contains(&id),
789 );
790 self.provenance.retain(|id, _| reach.entities.contains(&id));
791 Ok(())
792 }
793
794 fn reach(&self, roots: &[Shape]) -> Reach {
797 let mut reach = Reach::default();
798 let mut queue: Vec<TShapeId> = Vec::new();
799 for root in roots {
800 if reach.nodes.insert(root.node().index()) {
801 queue.push(root.node());
802 }
803 }
804 let mut ancestry: Vec<EntityId> = Vec::new();
805 while let Some(id) = queue.pop() {
806 let Some(node) = self.nodes.entry(id) else {
807 continue;
808 };
809 if let Some(entity) = node.identity
810 && reach.entities.insert(entity)
811 {
812 ancestry.push(entity);
813 }
814 for child in node.shape.children() {
815 if self.nodes.entry(child.node()).is_some()
816 && reach.nodes.insert(child.node().index())
817 {
818 queue.push(child.node());
819 }
820 }
821 match node.shape.data() {
822 NodeData::Edge(edge) => {
823 for repr in &edge.representations {
824 match repr {
825 EdgeRepr::Curve3d { curve, .. } => {
826 reach.curves.insert(*curve);
827 }
828 EdgeRepr::PCurve { curve, surface, .. } => {
829 reach.pcurves.insert(*curve);
830 reach.surfaces.insert(*surface);
831 }
832 EdgeRepr::Seam {
833 forward,
834 reversed,
835 surface,
836 ..
837 } => {
838 reach.pcurves.insert(*forward);
839 reach.pcurves.insert(*reversed);
840 reach.surfaces.insert(*surface);
841 }
842 EdgeRepr::Polyline { .. } => {}
843 EdgeRepr::PolygonOnTriangulation { triangulation, .. } => {
844 reach.meshes.insert(*triangulation);
845 }
846 }
847 }
848 }
849 NodeData::Face(face) => {
850 reach.surfaces.insert(face.surface);
851 if let Some(mesh) = face.triangulation {
852 reach.meshes.insert(mesh);
853 }
854 }
855 NodeData::Vertex(_) | NodeData::Container => {}
856 }
857 }
858 while let Some(entity) = ancestry.pop() {
859 if let Some(entry) = self.provenance.get(entity) {
860 for &from in entry.inputs() {
861 if reach.entities.insert(from) {
862 ancestry.push(from);
863 }
864 }
865 }
866 }
867 reach
868 }
869
870 #[must_use]
875 pub fn roots_of(&self, shape: &Shape) -> Vec<EntityId> {
876 self.identity_of(shape)
877 .map(|id| self.provenance.roots(id))
878 .unwrap_or_default()
879 }
880
881 #[must_use]
894 pub fn shape_of(&self, id: EntityId) -> Option<Shape> {
895 self.nodes
896 .iter()
897 .find(|(_, node)| node.identity == Some(id))
898 .map(|(node, _)| Shape::of(node))
899 }
900
901 pub fn set_derived(
912 &mut self,
913 shape: &Shape,
914 from: &[Shape],
915 role: Role,
916 ) -> OgeomResult<EntityId> {
917 if self.node(shape).is_none() {
918 ogeom_bail!(Dangling, "shape refers to a node not in this model");
919 }
920 let sources: Vec<EntityId> = from.iter().filter_map(|s| self.identity_of(s)).collect();
921 let id = self.provenance.derived(self.current_op, sources, role);
922 if let Some(node) = self.nodes.entry_mut(shape.node()) {
923 node.identity = Some(id);
924 }
925 Ok(id)
926 }
927
928 fn record_primitive(&mut self, node: TShapeId, role: Role) {
930 let id = self.provenance.primitive(self.current_op, role);
931 if let Some(node) = self.nodes.entry_mut(node) {
932 node.identity = Some(id);
933 }
934 }
935
936 #[must_use]
938 pub const fn datums(&self) -> &DatumStore {
939 &self.datums
940 }
941
942 #[must_use]
944 pub const fn geometry(&self) -> &GeometryStore {
945 &self.geometry
946 }
947
948 #[must_use]
953 pub const fn geometry_mut(&mut self) -> &mut GeometryStore {
954 &mut self.geometry
955 }
956
957 pub fn add_datum(&mut self, transform: Transform) -> DatumId {
959 self.datums.insert(transform)
960 }
961
962 #[must_use]
964 pub fn node(&self, shape: &Shape) -> Option<&TShape> {
965 self.nodes.get(shape.node())
966 }
967
968 #[must_use]
970 pub fn node_by_id(&self, id: TShapeId) -> Option<&TShape> {
971 self.nodes.get(id)
972 }
973
974 #[must_use]
987 pub fn node_mut(&mut self, shape: &Shape) -> Option<&mut TShape> {
988 self.sync_face_boxes();
989 if let Some(node) = self.nodes.get(shape.node()) {
990 if let Some(journal) = &mut self.widened
993 && let Some(was) = node.data().tolerance()
994 {
995 journal.push((shape.node(), was));
996 }
997 }
998 self.forget_boxes_above(shape.node());
999 self.nodes.get_mut(shape.node())
1000 }
1001
1002 fn insert_node(&mut self, node: TShape) -> TShapeId {
1005 self.sync_face_boxes();
1006 let id = self.insert_unlinked(node);
1007 self.link_children(id, true);
1008 id
1009 }
1010
1011 fn insert_unlinked(&mut self, shape: TShape) -> TShapeId {
1013 let face_box = (shape.kind() == ShapeType::Face).then(|| self.face_boxes.add());
1014 self.nodes.insert(Node {
1015 shape,
1016 identity: None,
1017 face_box,
1018 held_by: SmallVec::new(),
1019 held: false,
1020 })
1021 }
1022
1023 fn link_children(&mut self, id: TShapeId, fresh: bool) {
1027 let Some(node) = self.nodes.get(id) else {
1028 return;
1029 };
1030 if !matches!(
1031 node.kind(),
1032 ShapeType::Edge | ShapeType::Wire | ShapeType::Face
1033 ) {
1034 return;
1035 }
1036 let children: SmallVec<[TShapeId; 8]> = node.children().iter().map(Shape::node).collect();
1037 for child in children {
1041 if let Some(below) = self.nodes.entry_mut(child)
1042 && if fresh {
1043 below.held_by.last() != Some(&id.index())
1044 } else {
1045 !below.held_by.contains(&id.index())
1046 }
1047 {
1048 below.held_by.push(id.index());
1049 }
1050 }
1051 }
1052
1053 fn forget_boxes_above(&mut self, id: TShapeId) {
1056 if self.nodes.get(id).is_none() {
1057 return;
1058 }
1059 let mut stack: SmallVec<[u32; 8]> = smallvec::smallvec![id.index()];
1060 while let Some(at) = stack.pop() {
1061 let Some(node) = self.nodes.at(at).and_then(|id| self.nodes.entry(id)) else {
1062 continue;
1063 };
1064 if let Some(slot) = node.face_box {
1065 self.face_boxes.forget(slot);
1066 }
1067 stack.extend(node.held_by.iter().copied());
1068 }
1069 }
1070
1071 fn sync_face_boxes(&mut self) {
1074 let revision = self.geometry.revision();
1075 if self.face_boxes.revision != revision {
1076 self.face_boxes.forget_all();
1077 self.face_boxes.revision = revision;
1078 }
1079 }
1080
1081 fn face_box_slot(&self, face: &Shape) -> Option<&std::sync::OnceLock<Aabb>> {
1084 if self.face_boxes.revision != self.geometry.revision() {
1085 return None;
1086 }
1087 self.nodes
1088 .entry(face.node())
1089 .and_then(|node| node.face_box)
1090 .and_then(|slot| self.face_boxes.slot(slot))
1091 }
1092
1093 #[must_use]
1104 pub fn face_bounds(&self, face: &Shape) -> Option<Aabb> {
1105 let kept = *self.face_box_slot(face)?.get()?;
1106 let tolerance = self.node(face)?.data().tolerance()?.get();
1107 let placement = face.transform(&self.datums).ok()?;
1108 Some(placed_box(&kept, &placement).expanded(tolerance))
1109 }
1110
1111 pub fn kept_face_box(
1125 &self,
1126 face: &Shape,
1127 find: impl FnOnce(&Shape) -> OgeomResult<Aabb>,
1128 ) -> OgeomResult<Aabb> {
1129 if self.node(face).map(TShape::kind) != Some(ShapeType::Face) {
1130 ogeom_bail!(Construction, "only a face of this model keeps a box");
1131 }
1132 let bare = Shape::of(face.node());
1133 let Some(slot) = self.face_box_slot(face) else {
1134 return find(&bare);
1135 };
1136 if let Some(kept) = slot.get() {
1137 return Ok(*kept);
1138 }
1139 let found = find(&bare)?;
1140 Ok(self.face_boxes.keep(slot, found))
1141 }
1142
1143 pub fn kept_read<T: std::any::Any + Send + Sync>(
1166 &self,
1167 shape: &Shape,
1168 same: impl Fn(&T) -> bool,
1169 read: impl FnOnce() -> OgeomResult<(T, bool)>,
1170 ) -> OgeomResult<std::sync::Arc<T>> {
1171 let edits = self.geometry.edits();
1172 let reads = &self.nodes.reads;
1173 if let Some(kept) = reads.find(shape, edits, &same) {
1174 return Ok(kept);
1175 }
1176 let (found, keep) = read()?;
1177 let found = std::sync::Arc::new(found);
1178 if keep && self.node(shape).is_some() {
1179 let below = match reads.below(shape, edits) {
1180 Some(below) => below,
1181 None => std::sync::Arc::new(self.indices_below_kept(shape, &reads.sets(edits))),
1182 };
1183 reads.keep(shape, edits, below, std::sync::Arc::clone(&found), same);
1184 }
1185 Ok(found)
1186 }
1187
1188 #[must_use]
1194 pub fn kept_reads<T: std::any::Any + Send + Sync>(
1195 &self,
1196 same: impl Fn(&T) -> bool,
1197 ) -> Vec<std::sync::Arc<T>> {
1198 self.nodes.reads.all(self.geometry.edits(), same)
1199 }
1200
1201 fn indices_below_kept(&self, shape: &Shape, kept: &[std::sync::Arc<NodeSet>]) -> NodeSet {
1210 if kept.is_empty() {
1211 return self.indices_below(shape);
1212 }
1213 let mut below = NodeSet::default();
1214 let mut joined = vec![false; kept.len()];
1215 let mut stack = vec![shape.node()];
1216 below.insert(shape.node().index());
1217 while let Some(id) = stack.pop() {
1218 let Some(node) = self.nodes.get(id) else {
1219 continue;
1220 };
1221 for child in node.children() {
1222 let index = child.node().index();
1223 if !below.insert(index) {
1224 continue;
1225 }
1226 match kept.iter().position(|set| set.contains(index)) {
1227 Some(at) => joined[at] = true,
1228 None => stack.push(child.node()),
1229 }
1230 }
1231 }
1232 for (set, joined) in kept.iter().zip(joined) {
1233 if joined {
1234 below.union_with(set);
1235 }
1236 }
1237 below
1238 }
1239
1240 fn indices_below(&self, shape: &Shape) -> NodeSet {
1242 let mut below = NodeSet::default();
1243 let mut stack = vec![shape.node()];
1244 below.insert(shape.node().index());
1245 while let Some(id) = stack.pop() {
1246 let Some(node) = self.nodes.get(id) else {
1247 continue;
1248 };
1249 for child in node.children() {
1250 if below.insert(child.node().index()) {
1251 stack.push(child.node());
1252 }
1253 }
1254 }
1255 below
1256 }
1257
1258 pub fn kind_of(&self, shape: &Shape) -> OgeomResult<ShapeType> {
1265 let Some(node) = self.node(shape) else {
1266 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1267 };
1268 Ok(node.kind())
1269 }
1270
1271 pub fn tolerance_of(&self, shape: &Shape) -> OgeomResult<Option<Tolerance>> {
1277 let Some(node) = self.node(shape) else {
1278 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1279 };
1280 Ok(node.data().tolerance())
1281 }
1282
1283 #[must_use]
1285 pub fn node_count(&self) -> usize {
1286 self.nodes.len()
1287 }
1288
1289 #[must_use]
1291 pub fn is_empty(&self) -> bool {
1292 self.nodes.is_empty()
1293 }
1294
1295 pub fn nodes(&self) -> impl Iterator<Item = (TShapeId, &TShape)> {
1300 self.nodes.iter().map(|(id, node)| (id, &node.shape))
1301 }
1302
1303 pub fn identities(&self) -> impl Iterator<Item = (TShapeId, EntityId)> {
1305 self.nodes
1306 .iter()
1307 .filter_map(|(id, node)| Some((id, node.identity?)))
1308 }
1309
1310 pub fn add_vertex(&mut self, data: VertexData) -> Shape {
1312 Shape::of(self.insert_node(TShape::leaf(ShapeType::Vertex, NodeData::Vertex(data))))
1313 }
1314
1315 pub fn add_point(&mut self, point: Point) -> Shape {
1317 self.add_vertex(VertexData::new(point))
1318 }
1319
1320 pub fn add_edge(&mut self, data: EdgeData, bounds: &[Shape]) -> OgeomResult<Shape> {
1333 if bounds.len() > 2 {
1334 ogeom_bail!(
1335 Construction,
1336 "an edge has at most two bounding vertices, got {}",
1337 bounds.len()
1338 );
1339 }
1340 self.check_children(ShapeType::Vertex, bounds)?;
1341 for bound in bounds {
1344 self.widen(bound, data.tolerance)?;
1345 }
1346 let node = self.insert_node(TShape::new(
1347 ShapeType::Edge,
1348 NodeData::Edge(Box::new(data)),
1349 bounds.to_vec(),
1350 ));
1351 self.record_primitive(node, Role::SOLE);
1352 Ok(Shape::of(node))
1353 }
1354
1355 pub fn add_wire(&mut self, edges: &[Shape]) -> OgeomResult<Shape> {
1362 if edges.is_empty() {
1363 ogeom_bail!(Construction, "a wire needs at least one edge");
1364 }
1365 self.check_children(ShapeType::Edge, edges)?;
1366 Ok(Shape::of(self.insert_node(TShape::container(
1367 ShapeType::Wire,
1368 edges.to_vec(),
1369 ))))
1370 }
1371
1372 pub fn add_face(&mut self, mut data: FaceData, wires: &[Shape]) -> OgeomResult<Shape> {
1387 self.check_children(ShapeType::Wire, wires)?;
1388 if wires.is_empty() {
1389 data.natural_restriction = true;
1390 }
1391 let face_tolerance = data.tolerance;
1395 let walk = if face_tolerance.get() > Tolerance::MIN.get() {
1396 wires
1397 } else {
1398 &[]
1399 };
1400 for wire in walk {
1401 let edges = self.children_of(wire)?;
1402 for edge in &edges {
1403 self.widen(edge, face_tolerance)?;
1404 }
1405 }
1406 let node = self.insert_node(TShape::new(
1407 ShapeType::Face,
1408 NodeData::Face(Box::new(data)),
1409 wires.to_vec(),
1410 ));
1411 self.record_primitive(node, Role::SOLE);
1412 Ok(Shape::of(node))
1413 }
1414
1415 pub fn rechart_face(
1432 &mut self,
1433 face: &Shape,
1434 surface: SurfaceId,
1435 wires: &[Shape],
1436 ) -> OgeomResult<()> {
1437 if self.kind_of(face)? != ShapeType::Face {
1438 ogeom_bail!(Construction, "only a face can be re-charted");
1439 }
1440 if wires.is_empty() {
1441 ogeom_bail!(Construction, "a re-charted face needs its boundary");
1442 }
1443 self.check_children(ShapeType::Wire, wires)?;
1444 self.sync_face_boxes();
1445 self.forget_boxes_above(face.node());
1446 let Some(node) = self.nodes.get_mut(face.node()) else {
1447 ogeom_bail!(Dangling, "face is not in this model");
1448 };
1449 let NodeData::Face(data) = node.data_mut() else {
1450 ogeom_bail!(Construction, "face node holds no face data");
1451 };
1452 data.surface = surface;
1453 data.natural_restriction = false;
1454 data.triangulation = None;
1455 *node.children_mut() = wires.to_vec();
1456 self.link_children(face.node(), false);
1457 Ok(())
1458 }
1459
1460 pub fn add_shell(&mut self, faces: &[Shape]) -> OgeomResult<Shape> {
1467 if faces.is_empty() {
1468 ogeom_bail!(Construction, "a shell needs at least one face");
1469 }
1470 self.check_children(ShapeType::Face, faces)?;
1471 Ok(Shape::of(self.insert_node(TShape::container(
1472 ShapeType::Shell,
1473 faces.to_vec(),
1474 ))))
1475 }
1476
1477 pub fn add_solid(&mut self, shells: &[Shape]) -> OgeomResult<Shape> {
1484 if shells.is_empty() {
1485 ogeom_bail!(Construction, "a solid needs at least one shell");
1486 }
1487 self.check_children(ShapeType::Shell, shells)?;
1488 Ok(Shape::of(self.insert_node(TShape::container(
1489 ShapeType::Solid,
1490 shells.to_vec(),
1491 ))))
1492 }
1493
1494 pub fn add_compsolid(&mut self, solids: &[Shape]) -> OgeomResult<Shape> {
1501 if solids.is_empty() {
1502 ogeom_bail!(Construction, "a compsolid needs at least one solid");
1503 }
1504 self.check_children(ShapeType::Solid, solids)?;
1505 Ok(Shape::of(self.insert_node(TShape::container(
1506 ShapeType::CompSolid,
1507 solids.to_vec(),
1508 ))))
1509 }
1510
1511 pub fn add_compound(&mut self, shapes: &[Shape]) -> OgeomResult<Shape> {
1522 for shape in shapes {
1523 if self.node(shape).is_none() {
1524 ogeom_bail!(Dangling, "compound member is not in this model");
1525 }
1526 }
1527 Ok(Shape::of(self.insert_node(TShape::container(
1528 ShapeType::Compound,
1529 shapes.to_vec(),
1530 ))))
1531 }
1532
1533 pub fn children_of(&self, shape: &Shape) -> OgeomResult<Vec<Shape>> {
1548 let Some(node) = self.node(shape) else {
1549 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1550 };
1551 Ok(node
1552 .children()
1553 .iter()
1554 .map(|child| child.beneath(shape.location(), shape.orientation()))
1555 .collect())
1556 }
1557
1558 pub fn ordered_children_of(&self, shape: &Shape) -> OgeomResult<Vec<Shape>> {
1583 let mut children = self.children_of(shape)?;
1584 if shape.orientation() == Orientation::Reversed {
1585 children.reverse();
1586 }
1587 Ok(children)
1588 }
1589
1590 pub fn outer_wire(&self, face: &Shape) -> OgeomResult<Option<Shape>> {
1603 if self.kind_of(face)? != ShapeType::Face {
1604 ogeom_bail!(Construction, "only a face has an outer wire");
1605 }
1606 Ok(self.children_of(face)?.into_iter().next())
1607 }
1608
1609 pub fn widen(&mut self, shape: &Shape, to: Tolerance) -> OgeomResult<()> {
1634 const SHORT: usize = 32;
1637 self.sync_face_boxes();
1638 let mut affected: smallvec::SmallVec<[TShapeId; 8]> = smallvec::SmallVec::new();
1639 let mut seen: Option<ogeom_core::FastSet<TShapeId>> = None;
1640 let mut stack: smallvec::SmallVec<[TShapeId; 8]> = smallvec::smallvec![shape.node()];
1641 while let Some(id) = stack.pop() {
1642 let fresh = match &mut seen {
1643 Some(seen) => seen.insert(id),
1644 None => !affected.contains(&id),
1645 };
1646 if !fresh {
1647 continue;
1648 }
1649 if seen.is_none() && affected.len() >= SHORT {
1650 seen = Some(affected.iter().copied().chain([id]).collect());
1651 }
1652 let Some(node) = self.nodes.get(id) else {
1653 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1654 };
1655 affected.push(id);
1656 stack.extend(node.children().iter().map(Shape::node));
1657 }
1658 for id in affected {
1659 let grows = self
1662 .nodes
1663 .get(id)
1664 .and_then(|node| node.data().tolerance())
1665 .is_some_and(|was| was.get() < to.get());
1666 if grows && let Some(node) = self.nodes.get_mut(id) {
1667 if let Some(journal) = &mut self.widened
1668 && let Some(was) = node.data().tolerance()
1669 {
1670 journal.push((id, was));
1671 }
1672 node.data_mut().widen(to);
1673 }
1674 }
1675 Ok(())
1676 }
1677
1678 pub fn note_widened(&mut self) -> WidenMark {
1683 match &self.widened {
1684 Some(journal) => WidenMark {
1685 from: journal.len(),
1686 opened: false,
1687 },
1688 None => {
1689 self.widened = Some(Vec::new());
1690 WidenMark {
1691 from: 0,
1692 opened: true,
1693 }
1694 }
1695 }
1696 }
1697
1698 pub fn undo_widened(&mut self, mark: WidenMark) {
1701 let Some(journal) = &mut self.widened else {
1702 return;
1703 };
1704 let undone: Vec<(TShapeId, Tolerance)> =
1705 journal.drain(mark.from.min(journal.len())..).collect();
1706 for (id, was) in undone.into_iter().rev() {
1708 if let Some(node) = self.nodes.get_mut(id) {
1709 node.data_mut().set_tolerance(was);
1710 }
1711 }
1712 if mark.opened {
1713 self.widened = None;
1714 }
1715 }
1716
1717 pub fn keep_widened(&mut self, mark: WidenMark) {
1720 if mark.opened {
1721 self.widened = None;
1722 }
1723 }
1724
1725 fn is_held(&self, id: TShapeId) -> bool {
1727 self.nodes.entry(id).is_some_and(|node| node.held)
1728 }
1729
1730 pub fn note_held(&mut self, result: &Shape, since: usize) -> OgeomResult<()> {
1744 let made = self.nodes.len().saturating_sub(since);
1747 let next = self.nodes.arena.next_index().unwrap_or(u32::MAX);
1748 let since = next.saturating_sub(u32::try_from(made).unwrap_or(u32::MAX));
1749 let mut seen = vec![false; made];
1750 let mut stack: smallvec::SmallVec<[TShapeId; 16]> = smallvec::smallvec![result.node()];
1751 while let Some(id) = stack.pop() {
1752 let Some(node) = self.nodes.get(id) else {
1753 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1754 };
1755 let Some(at) = id.index().checked_sub(since) else {
1756 if let Some(node) = self.nodes.entry_mut(id) {
1757 node.held = true;
1758 self.any_held = true;
1759 }
1760 continue;
1761 };
1762 let Some(slot) = seen.get_mut(at as usize) else {
1763 continue;
1764 };
1765 if core::mem::replace(slot, true) {
1766 continue;
1767 }
1768 stack.extend(node.children().iter().map(Shape::node));
1769 }
1770 Ok(())
1771 }
1772
1773 pub fn unshare(&mut self, root: &Shape) -> OgeomResult<Vec<(TShapeId, TShapeId)>> {
1791 if !self.any_held || self.is_held(root.node()) {
1792 return Ok(Vec::new());
1793 }
1794 let mut pairs = Vec::new();
1795 self.unshare_below(std::slice::from_ref(root), true, &mut pairs)?;
1796 Ok(pairs)
1797 }
1798
1799 pub fn unshare_each(&mut self, shapes: &[Shape]) -> OgeomResult<Unshared> {
1813 if !self.any_held {
1814 return Ok(Unshared {
1815 shapes: shapes.to_vec(),
1816 copies: Vec::new(),
1817 });
1818 }
1819 let since = self.nodes.len();
1820 let mut pairs = Vec::new();
1821 let copies = self.unshare_below(shapes, false, &mut pairs)?;
1822 let mut out = Vec::with_capacity(shapes.len());
1823 for shape in shapes {
1824 match copies.get(&shape.node()) {
1825 Some(©) => {
1826 let copy = Shape::new(copy, shape.location().clone(), shape.orientation());
1827 self.note_held(©, since)?;
1828 out.push(copy);
1829 }
1830 None => out.push(shape.clone()),
1831 }
1832 }
1833 Ok(Unshared {
1834 shapes: out,
1835 copies: pairs,
1836 })
1837 }
1838
1839 fn unshare_below(
1844 &mut self,
1845 roots: &[Shape],
1846 repoint: bool,
1847 pairs: &mut Vec<(TShapeId, TShapeId)>,
1848 ) -> OgeomResult<ogeom_core::FastMap<TShapeId, TShapeId>> {
1849 let mut held: ogeom_core::FastMap<TShapeId, bool> = ogeom_core::FastMap::default();
1852 let mut order: Vec<TShapeId> = Vec::new();
1853 let mut stack: Vec<(TShapeId, bool)> =
1854 roots.iter().rev().map(|r| (r.node(), false)).collect();
1855 let mut any = false;
1856 while let Some((id, above)) = stack.pop() {
1857 let is_held = above || self.is_held(id);
1858 match held.get(&id) {
1859 Some(&was) if was || !is_held => continue,
1860 Some(_) => {}
1861 None => order.push(id),
1862 }
1863 held.insert(id, is_held);
1864 any |= is_held;
1865 let Some(node) = self.nodes.get(id) else {
1866 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1867 };
1868 stack.extend(node.children().iter().rev().map(|c| (c.node(), is_held)));
1869 }
1870 let mut copies: ogeom_core::FastMap<TShapeId, TShapeId> = ogeom_core::FastMap::default();
1871 if !any {
1872 return Ok(copies);
1873 }
1874 let mut copied: ogeom_core::FastSet<TShapeId> = held
1877 .iter()
1878 .filter(|(_, h)| **h)
1879 .map(|(id, _)| *id)
1880 .collect();
1881 if !repoint {
1882 loop {
1883 let mut grew = false;
1884 for &id in order.iter().rev() {
1885 if copied.contains(&id) {
1886 continue;
1887 }
1888 let below = self
1889 .nodes
1890 .get(id)
1891 .is_some_and(|n| n.children().iter().any(|c| copied.contains(&c.node())));
1892 if below {
1893 copied.insert(id);
1894 grew = true;
1895 }
1896 }
1897 if !grew {
1898 break;
1899 }
1900 }
1901 }
1902 self.sync_face_boxes();
1903 for root in roots {
1904 if copied.contains(&root.node()) {
1905 self.copy_below(root.node(), &copied, &mut copies, pairs)?;
1906 }
1907 }
1908 if !repoint {
1909 return Ok(copies);
1910 }
1911 for parent in order {
1912 if copied.contains(&parent) {
1913 continue;
1914 }
1915 let Some(node) = self.nodes.get(parent) else {
1916 continue;
1917 };
1918 let children = node.children().to_vec();
1919 let mut changed = false;
1920 let mut repointed = Vec::with_capacity(children.len());
1921 for child in children {
1922 if copied.contains(&child.node()) {
1923 let copy = self.copy_below(child.node(), &copied, &mut copies, pairs)?;
1924 repointed.push(Shape::new(
1925 copy,
1926 child.location().clone(),
1927 child.orientation(),
1928 ));
1929 changed = true;
1930 } else {
1931 repointed.push(child);
1932 }
1933 }
1934 if changed && let Some(node) = self.nodes.get_mut(parent) {
1935 *node.children_mut() = repointed;
1936 self.link_children(parent, false);
1937 }
1938 }
1939 Ok(copies)
1940 }
1941
1942 fn copy_below(
1945 &mut self,
1946 id: TShapeId,
1947 copied: &ogeom_core::FastSet<TShapeId>,
1948 copies: &mut ogeom_core::FastMap<TShapeId, TShapeId>,
1949 pairs: &mut Vec<(TShapeId, TShapeId)>,
1950 ) -> OgeomResult<TShapeId> {
1951 let mut stack: Vec<(TShapeId, bool)> = vec![(id, false)];
1954 while let Some((at, ready)) = stack.pop() {
1955 if copies.contains_key(&at) {
1956 continue;
1957 }
1958 let Some(node) = self.nodes.get(at) else {
1959 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1960 };
1961 if !ready {
1962 stack.push((at, true));
1963 stack.extend(
1964 node.children()
1965 .iter()
1966 .rev()
1967 .filter(|c| copied.contains(&c.node()) && !copies.contains_key(&c.node()))
1968 .map(|c| (c.node(), false)),
1969 );
1970 continue;
1971 }
1972 let children = node
1973 .children()
1974 .iter()
1975 .map(|c| {
1976 let copy = copies.get(&c.node()).copied().unwrap_or(c.node());
1977 Shape::new(copy, c.location().clone(), c.orientation())
1978 })
1979 .collect();
1980 let copy = TShape::new(node.kind(), node.data().clone(), children);
1981 let kept = self
1982 .face_box_slot(&Shape::of(at))
1983 .and_then(|slot| slot.get().copied());
1984 let made = self.insert_node(copy);
1985 let identity = self.nodes.entry(at).and_then(|node| node.identity);
1986 if let Some(node) = self.nodes.entry_mut(made) {
1987 node.identity = identity;
1988 }
1989 if let (Some(kept), Some(slot)) = (kept, self.face_box_slot(&Shape::of(made))) {
1990 self.face_boxes.keep(slot, kept);
1991 }
1992 copies.insert(at, made);
1993 pairs.push((at, made));
1994 }
1995 copies.get(&id).copied().ok_or_else(|| {
1996 ogeom_core::ogeom_err!(Dangling, "shape refers to a node not in this model")
1997 })
1998 }
1999
2000 fn check_children(&self, expected: ShapeType, children: &[Shape]) -> OgeomResult<()> {
2002 for child in children {
2003 let Some(node) = self.node(child) else {
2004 ogeom_bail!(Dangling, "child refers to a node not in this model");
2005 };
2006 if node.kind() != expected {
2007 ogeom_bail!(
2008 Construction,
2009 "expected a {expected:?} child, got a {:?}",
2010 node.kind()
2011 );
2012 }
2013 }
2014 Ok(())
2015 }
2016
2017 pub fn check_tolerances(&self, root: &Shape) -> OgeomResult<()> {
2027 let mut stack: Vec<(Shape, Option<(ogeom_core::Tolerance, ShapeType)>)> =
2032 vec![(root.clone(), None)];
2033 let mut seen = ogeom_core::FastSet::default();
2034 while let Some((shape, bound)) = stack.pop() {
2035 let Some(node) = self.node(&shape) else {
2036 ogeom_bail!(Dangling, "shape refers to a node not in this model");
2037 };
2038 if !seen.insert((shape.node(), bound.map(|(t, _)| t.get().to_bits()))) {
2039 continue;
2040 }
2041 let own = node.data().tolerance();
2042 if let (Some((parent, parent_kind)), Some(child_tolerance)) = (bound, own)
2045 && child_tolerance < parent
2046 {
2047 ogeom_bail!(
2048 Invariant,
2049 "a {:?} at tolerance {} bounds a {:?} at {}, which is tighter",
2050 node.kind(),
2051 child_tolerance.get(),
2052 parent_kind,
2053 parent.get()
2054 );
2055 }
2056 let below = own.map(|t| (t, node.kind())).or(bound);
2057 for child in node.children() {
2058 stack.push((child.clone(), below));
2059 }
2060 }
2061 Ok(())
2062 }
2063
2064 pub fn same_position(&self, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<bool> {
2070 a.is_same_position(b, &self.datums, tol)
2071 }
2072
2073 pub fn placed(&mut self, shape: &Shape, transform: Transform) -> Shape {
2088 let datum = self.add_datum(transform);
2089 shape.moved(&Location::of(datum))
2090 }
2091}
2092
2093fn placed_box(kept: &Aabb, placement: &Transform) -> Aabb {
2096 match placement.kind() {
2097 TransformKind::Identity => *kept,
2098 TransformKind::Translation => match (kept.low(), kept.high()) {
2099 (Some(low), Some(high)) => {
2100 Aabb::of_corners(placement.apply(low), placement.apply(high))
2101 }
2102 _ => *kept,
2103 },
2104 _ => kept.transformed(placement),
2105 }
2106}
2107
2108#[derive(Debug, Clone, Copy)]
2110#[must_use]
2111pub struct WidenMark {
2112 from: usize,
2113 opened: bool,
2114}
2115
2116#[derive(Debug, Clone)]
2118pub struct Unshared {
2119 pub shapes: Vec<Shape>,
2122 pub copies: Vec<(TShapeId, TShapeId)>,
2124}
2125
2126#[derive(Debug)]
2129pub struct Absorbed {
2130 pub shapes: Vec<Shape>,
2132 #[allow(
2136 clippy::disallowed_types,
2137 reason = "public field since 0.1; a std map keeps the API"
2138 )]
2139 pub entities: std::collections::HashMap<EntityId, EntityId>,
2140}
2141
2142#[derive(Debug, Default)]
2148pub struct ModelParts {
2149 pub nodes: Vec<TShape>,
2151 pub datums: Vec<crate::location::Datum>,
2153 pub geometry: GeometryStore,
2155 pub provenance: Vec<Provenance>,
2158 pub identity: Vec<(TShapeId, EntityId)>,
2160 pub current_op: OpId,
2162 pub tolerances: Tolerances,
2164}
2165
2166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2168pub enum Filter {
2169 OfType(ShapeType),
2171 All,
2173}
2174
2175pub fn explore(model: &Model, root: &Shape, filter: Filter) -> OgeomResult<Vec<Shape>> {
2191 let mut out = Vec::new();
2192 let mut stack = vec![root.clone()];
2193 while let Some(shape) = stack.pop() {
2194 let Some(node) = model.node(&shape) else {
2198 ogeom_bail!(Dangling, "shape refers to a node not in this model");
2199 };
2200 let (matches, descend) = match filter {
2201 Filter::OfType(want) => (
2206 node.kind() == want,
2207 node.kind() == ShapeType::Compound || node.kind() > want,
2208 ),
2209 Filter::All => (true, true),
2210 };
2211 let children = if descend { node.children() } else { &[] };
2212 stack.reserve(children.len());
2213 for child in children.iter().rev() {
2217 stack.push(child.beneath(shape.location(), shape.orientation()));
2218 }
2219 if matches {
2220 out.push(shape);
2223 }
2224 }
2225 Ok(out)
2226}
2227
2228pub fn explore_unique(model: &Model, root: &Shape, want: ShapeType) -> OgeomResult<Vec<Shape>> {
2237 use crate::shape::SameKey;
2238
2239 let found = explore(model, root, Filter::OfType(want))?;
2240 let mut seen = ogeom_core::FastSet::with_capacity_and_hasher(found.len(), Default::default());
2241 let mut out = Vec::with_capacity(found.len());
2242 for shape in found {
2243 if seen.insert(SameKey(shape.clone())) {
2244 out.push(shape);
2245 }
2246 }
2247 Ok(out)
2248}
2249
2250pub fn ancestors_of(
2259 model: &Model,
2260 root: &Shape,
2261 target: &Shape,
2262 want: ShapeType,
2263) -> OgeomResult<Vec<Shape>> {
2264 let kind = model.kind_of(target)?;
2267 let mut out = Vec::new();
2268 for candidate in explore(model, root, Filter::OfType(want))? {
2269 if explore(model, &candidate, Filter::OfType(kind))?
2270 .iter()
2271 .any(|s| s.is_same(target))
2272 {
2273 out.push(candidate);
2274 }
2275 }
2276 Ok(out)
2277}
2278
2279#[cfg(test)]
2280#[allow(clippy::unwrap_used)]
2281mod tests {
2282 use super::*;
2283 use ogeom_geom::PlaneSurface;
2284 use ogeom_math::{Direction, Frame, Plane, Vector};
2285
2286 const T: Tolerances = Tolerances::millimetres();
2287
2288 fn square(model: &mut Model) -> Shape {
2290 let corners = [
2291 model.add_point(Point::new(0.0, 0.0, 0.0)),
2292 model.add_point(Point::new(1.0, 0.0, 0.0)),
2293 model.add_point(Point::new(1.0, 1.0, 0.0)),
2294 model.add_point(Point::new(0.0, 1.0, 0.0)),
2295 ];
2296 let mut edges = Vec::new();
2297 for i in 0..4 {
2298 let bounds = [corners[i].clone(), corners[(i + 1) % 4].clone()];
2299 edges.push(model.add_edge(EdgeData::new(), &bounds).unwrap());
2300 }
2301 let wire = model.add_wire(&edges).unwrap();
2302 let surface = model
2303 .geometry_mut()
2304 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
2305 model
2306 .add_face(FaceData::new(surface, Location::identity()), &[wire])
2307 .unwrap()
2308 }
2309
2310 fn edge_parts() -> (ModelParts, Vec<Shape>) {
2314 use ogeom_core::Role;
2315
2316 use crate::entity::CurveId;
2317 use crate::location::DatumId;
2318
2319 let mut geometry = GeometryStore::new();
2320 geometry.add_curve(
2321 ogeom_geom::LineCurve::new(ogeom_math::Axis {
2322 location: Point::new(0.0, 0.0, 0.0),
2323 direction: Direction::X,
2324 })
2325 .into(),
2326 );
2327 let ends = [
2328 Shape::of(TShapeId::from_parts(0, 0)),
2329 Shape::of(TShapeId::from_parts(1, 0)).reversed(),
2330 ];
2331 let edge = TShape::new(
2332 ShapeType::Edge,
2333 NodeData::Edge(Box::new(EdgeData::on_curve(
2334 CurveId::from_parts(0, 0),
2335 Location::of(DatumId::from_parts(0, 0)),
2336 (0.0, 2.0),
2337 ))),
2338 ends.to_vec(),
2339 );
2340 let one = EntityId::from_raw(1).unwrap();
2341 let two = EntityId::from_raw(2).unwrap();
2342 let parts = ModelParts {
2343 nodes: vec![
2344 TShape::leaf(
2345 ShapeType::Vertex,
2346 NodeData::Vertex(VertexData::new(Point::new(0.0, 0.0, 0.0))),
2347 ),
2348 TShape::leaf(
2349 ShapeType::Vertex,
2350 NodeData::Vertex(VertexData::new(Point::new(2.0, 0.0, 0.0))),
2351 ),
2352 edge,
2353 ],
2354 datums: vec![Transform::translation(Vector::new(0.0, 0.0, 1.0))],
2355 geometry,
2356 provenance: vec![
2357 Provenance::Primitive {
2358 op: OpId(1),
2359 role: Role::SOLE,
2360 },
2361 Provenance::Derived {
2362 op: OpId(1),
2363 from: [one].into_iter().collect(),
2364 role: Role::SOLE,
2365 },
2366 ],
2367 identity: vec![(TShapeId::from_parts(2, 0), two)],
2368 current_op: OpId(1),
2369 tolerances: T,
2370 };
2371 (parts, vec![Shape::of(TShapeId::from_parts(2, 0))])
2372 }
2373
2374 #[test]
2375 fn absorbing_parts_into_a_live_model_offsets_every_handle() {
2376 let mut model = Model::new();
2377 let face = square(&mut model);
2378
2379 let (parts, roots) = edge_parts();
2380 let absorbed = model.absorb(parts, &roots).unwrap();
2381 assert_eq!(absorbed.shapes.len(), 1);
2382 let edge = &absorbed.shapes[0];
2383
2384 assert_eq!(model.kind_of(edge).unwrap(), ShapeType::Edge);
2386 let vertices = explore(&model, edge, Filter::OfType(ShapeType::Vertex)).unwrap();
2387 assert_eq!(vertices.len(), 2);
2388 let points: Vec<Point> = vertices
2389 .iter()
2390 .map(|v| model.node(v).unwrap().data().as_vertex().unwrap().point)
2391 .collect();
2392 assert!(points.contains(&Point::new(2.0, 0.0, 0.0)), "{points:?}");
2393
2394 let node = model.node(edge).unwrap();
2396 let repr = &node.data().as_edge().unwrap().representations[0];
2397 assert!(
2398 model.geometry().holds(repr),
2399 "the absorbed edge's curve did not land"
2400 );
2401 let EdgeRepr::Curve3d { location, .. } = repr else {
2402 panic!("the representation changed kind in the shift");
2403 };
2404 assert!(
2405 location.composed(model.datums()).is_ok(),
2406 "the absorbed edge's datum did not land"
2407 );
2408
2409 assert_eq!(model.kind_of(&face).unwrap(), ShapeType::Face);
2411 }
2412
2413 #[test]
2414 fn absorbed_identities_keep_their_provenance_under_new_ids() {
2415 let mut model = Model::new();
2416 square(&mut model);
2417 let issued_before = model.provenance().len() as u64;
2418 assert!(
2419 issued_before > 0,
2420 "the square should have minted identities"
2421 );
2422
2423 let (parts, roots) = edge_parts();
2424 let absorbed = model.absorb(parts, &roots).unwrap();
2425
2426 let old = EntityId::from_raw(2).unwrap();
2428 let new = absorbed.entities[&old];
2429 assert_eq!(new.get(), 2 + issued_before);
2430 assert_eq!(model.identity_of(&absorbed.shapes[0]), Some(new));
2431
2432 let entry = model.provenance().get(new).unwrap();
2435 let source = EntityId::from_raw(1 + issued_before).unwrap();
2436 assert_eq!(entry.inputs(), &[source]);
2437 assert_eq!(model.provenance().roots(new), vec![source]);
2438 }
2439
2440 #[test]
2441 fn absorbing_into_an_empty_model_matches_from_parts() {
2442 let (parts, roots) = edge_parts();
2443 let restored = Model::from_parts(parts).unwrap();
2444 let bound = restored.bind(&roots[0]).unwrap();
2445
2446 let (parts, roots) = edge_parts();
2447 let mut empty = Model::new();
2448 let absorbed = empty.absorb(parts, &roots).unwrap();
2449 let shape = &absorbed.shapes[0];
2450
2451 assert_eq!(shape.node().index(), bound.node().index());
2454 assert_eq!(shape.node().generation(), bound.node().generation());
2455 assert_eq!(restored.identity_of(&bound), empty.identity_of(shape));
2456 assert_eq!(restored.provenance().len(), empty.provenance().len());
2457 }
2458
2459 #[test]
2460 fn parts_with_scoped_or_generation_bearing_keys_are_refused() {
2461 let mut model = Model::new();
2462
2463 let (mut parts, roots) = edge_parts();
2464 let child = parts.nodes[2].children()[0].clone();
2465 parts.nodes[2].children_mut()[0] = Shape::new(
2466 child.node().with_scope(7),
2467 Location::identity(),
2468 Orientation::Forward,
2469 );
2470 assert!(
2471 model.absorb(parts, &roots).is_err(),
2472 "a scoped child key should be refused"
2473 );
2474
2475 let (mut parts, roots) = edge_parts();
2476 parts.identity[0].0 = TShapeId::from_parts(2, 1);
2477 assert!(
2478 model.absorb(parts, &roots).is_err(),
2479 "a recycled-generation key should be refused"
2480 );
2481 }
2482
2483 #[test]
2484 fn parts_in_other_units_are_refused() {
2485 let mut model = Model::new();
2486 square(&mut model);
2487 let issued_before = model.provenance().len();
2488
2489 let (mut parts, roots) = edge_parts();
2490 parts.tolerances = Tolerances::metres();
2491 assert!(model.absorb(parts, &roots).is_err());
2492 assert_eq!(
2493 model.provenance().len(),
2494 issued_before,
2495 "a refused absorb should leave the model alone"
2496 );
2497 }
2498
2499 #[test]
2500 fn absorb_leaves_the_current_operation_alone() {
2501 let mut model = Model::new();
2502 model.begin_operation();
2503 model.begin_operation();
2504 let op = model.begin_operation();
2505
2506 let (parts, roots) = edge_parts();
2507 model.absorb(parts, &roots).unwrap();
2508 assert_eq!(model.current_operation(), op);
2509 }
2510
2511 #[test]
2512 fn an_absorbed_root_that_names_a_missing_node_dangles() {
2513 let mut model = Model::new();
2514 let (parts, _) = edge_parts();
2515 let stray = vec![Shape::of(TShapeId::from_parts(99, 0))];
2516 assert!(model.absorb(parts, &stray).is_err());
2517 }
2518
2519 #[test]
2520 fn absorbing_empty_parts_is_a_no_op() {
2521 let mut model = Model::new();
2522 square(&mut model);
2523 let issued_before = model.provenance().len();
2524
2525 let parts = ModelParts {
2526 tolerances: T,
2527 ..ModelParts::default()
2528 };
2529 let absorbed = model.absorb(parts, &[]).unwrap();
2530 assert!(absorbed.shapes.is_empty());
2531 assert!(absorbed.entities.is_empty());
2532 assert_eq!(model.provenance().len(), issued_before);
2533 }
2534
2535 #[test]
2536 fn reversing_a_wire_reverses_the_walk_as_well_as_each_edge() {
2537 let mut model = Model::new();
2542 let face = square(&mut model);
2543 let wire = model.children_of(&face).unwrap()[0].clone();
2544
2545 let forward = model.ordered_children_of(&wire).unwrap();
2546 let backward = model.ordered_children_of(&wire.reversed()).unwrap();
2547
2548 assert_eq!(forward.len(), 4);
2549 assert_eq!(backward.len(), 4);
2550 for (i, edge) in backward.iter().enumerate() {
2551 let partner = &forward[3 - i];
2552 assert!(edge.is_same(partner), "the order did not reverse");
2553 assert_eq!(
2554 edge.orientation(),
2555 Orientation::Reversed.compose(partner.orientation()),
2556 "each edge should also flip"
2557 );
2558 }
2559
2560 let raw = model.children_of(&wire.reversed()).unwrap();
2563 assert!(raw[0].is_same(&forward[0]));
2564 }
2565
2566 #[test]
2567 fn a_built_tree_has_the_expected_shape() {
2568 let mut model = Model::new();
2569 let face = square(&mut model);
2570
2571 assert_eq!(model.kind_of(&face).unwrap(), ShapeType::Face);
2572 assert_eq!(
2573 explore_unique(&model, &face, ShapeType::Wire)
2574 .unwrap()
2575 .len(),
2576 1
2577 );
2578 assert_eq!(
2579 explore_unique(&model, &face, ShapeType::Edge)
2580 .unwrap()
2581 .len(),
2582 4
2583 );
2584 assert_eq!(
2585 explore_unique(&model, &face, ShapeType::Vertex)
2586 .unwrap()
2587 .len(),
2588 4
2589 );
2590 assert_eq!(
2593 explore(&model, &face, Filter::OfType(ShapeType::Vertex))
2594 .unwrap()
2595 .len(),
2596 8
2597 );
2598 }
2599
2600 #[test]
2601 fn children_are_returned_with_the_parents_placement_composed() {
2602 let mut model = Model::new();
2606 let face = square(&mut model);
2607 let moved = model.placed(&face, Transform::translation(Vector::new(10.0, 0.0, 0.0)));
2608
2609 let vertices = explore_unique(&model, &moved, ShapeType::Vertex).unwrap();
2610 assert_eq!(vertices.len(), 4);
2611 for v in &vertices {
2612 let node = model.node(v).unwrap();
2613 let local = node.data().as_vertex().unwrap().point;
2614 let world = v.transform(model.datums()).unwrap().apply(local);
2615 assert!(world.x >= 10.0 - 1e-12, "vertex at {world:?} was not moved");
2616 }
2617 }
2618
2619 #[test]
2620 fn children_are_returned_with_the_parents_orientation_composed() {
2621 let mut model = Model::new();
2622 let face = square(&mut model);
2623 let reversed = face.reversed();
2624
2625 let forward_edges = model
2626 .children_of(&model.children_of(&face).unwrap()[0])
2627 .unwrap();
2628 let reversed_edges = model
2629 .children_of(&model.children_of(&reversed).unwrap()[0])
2630 .unwrap();
2631
2632 for (a, b) in forward_edges.iter().zip(&reversed_edges) {
2633 assert_eq!(
2634 b.orientation(),
2635 a.orientation().reversed(),
2636 "reversing a face must reverse what its edges present, \
2637 without touching a single stored child"
2638 );
2639 }
2640 }
2641
2642 #[test]
2643 fn reversing_a_shape_touches_no_stored_child() {
2644 let mut model = Model::new();
2647 let face = square(&mut model);
2648 let before = model.node(&face).unwrap().clone();
2649 let _ = face.reversed();
2650 assert_eq!(model.node(&face).unwrap(), &before);
2651 }
2652
2653 #[test]
2654 fn placement_composes_through_nesting() {
2655 let mut model = Model::new();
2656 let vertex = model.add_point(Point::new(1.0, 0.0, 0.0));
2657 let edge = model
2658 .add_edge(EdgeData::new(), &[vertex.clone(), vertex.clone()])
2659 .unwrap();
2660 let moved_edge = model.placed(&edge, Transform::translation(Vector::new(10.0, 0.0, 0.0)));
2661 let compound = model.add_compound(&[moved_edge]).unwrap();
2662 let moved_compound = model.placed(
2663 &compound,
2664 Transform::translation(Vector::new(100.0, 0.0, 0.0)),
2665 );
2666
2667 let found = explore_unique(&model, &moved_compound, ShapeType::Vertex).unwrap();
2668 assert_eq!(found.len(), 1);
2669 let local = model
2670 .node(&found[0])
2671 .unwrap()
2672 .data()
2673 .as_vertex()
2674 .unwrap()
2675 .point;
2676 let world = found[0].transform(model.datums()).unwrap().apply(local);
2677 assert!(
2678 world.is_equal(Point::new(111.0, 0.0, 0.0), T),
2679 "expected 1 + 10 + 100, got {world:?}"
2680 );
2681 }
2682
2683 #[test]
2684 fn the_builder_refuses_children_of_the_wrong_type() {
2685 let mut model = Model::new();
2686 let vertex = model.add_point(Point::ORIGIN);
2687 let edge = model
2688 .add_edge(EdgeData::new(), std::slice::from_ref(&vertex))
2689 .unwrap();
2690
2691 assert!(
2692 model.add_wire(std::slice::from_ref(&vertex)).is_err(),
2693 "a wire holds edges"
2694 );
2695 assert!(
2696 model.add_shell(std::slice::from_ref(&edge)).is_err(),
2697 "a shell holds faces"
2698 );
2699 assert!(
2700 model.add_solid(std::slice::from_ref(&edge)).is_err(),
2701 "a solid holds shells"
2702 );
2703 assert!(model.add_wire(&[edge]).is_ok());
2704
2705 assert!(model.add_compound(&[vertex]).is_ok());
2707 }
2708
2709 #[test]
2710 fn empty_containers_are_refused_except_a_compound() {
2711 let mut model = Model::new();
2712 assert!(model.add_wire(&[]).is_err());
2713 assert!(model.add_shell(&[]).is_err());
2714 assert!(model.add_solid(&[]).is_err());
2715 assert!(model.add_compsolid(&[]).is_err());
2716 assert!(model.add_compound(&[]).is_ok());
2719 }
2720
2721 #[test]
2722 fn an_edge_takes_at_most_two_vertices() {
2723 let mut model = Model::new();
2724 let v = model.add_point(Point::ORIGIN);
2725 assert!(model.add_edge(EdgeData::new(), &[]).is_ok(), "unbounded");
2726 assert!(
2727 model
2728 .add_edge(EdgeData::new(), std::slice::from_ref(&v))
2729 .is_ok()
2730 );
2731 assert!(
2732 model
2733 .add_edge(EdgeData::new(), &[v.clone(), v.clone()])
2734 .is_ok()
2735 );
2736 assert!(
2737 model
2738 .add_edge(EdgeData::new(), &[v.clone(), v.clone(), v])
2739 .is_err(),
2740 "three ends is not an edge"
2741 );
2742 }
2743
2744 #[test]
2745 fn building_enforces_the_containment_rule_upward() {
2746 let mut model = Model::new();
2750 let vertex = model.add_point(Point::ORIGIN);
2751 assert_eq!(model.tolerance_of(&vertex).unwrap(), Some(Tolerance::MIN));
2752
2753 let mut edge_data = EdgeData::new();
2754 edge_data.widen(Tolerance::new(1e-3).unwrap());
2755 let edge = model
2756 .add_edge(edge_data, std::slice::from_ref(&vertex))
2757 .unwrap();
2758
2759 assert_eq!(
2760 model.tolerance_of(&vertex).unwrap(),
2761 Some(Tolerance::new(1e-3).unwrap()),
2762 "the vertex was widened to contain its edge"
2763 );
2764 assert!(model.check_tolerances(&edge).is_ok());
2765 }
2766
2767 #[test]
2768 fn a_kept_face_box_is_forgotten_when_what_the_face_is_made_of_changes() {
2769 let unit = Aabb::of_corners(Point::ORIGIN, Point::new(1.0, 1.0, 0.0));
2770 let keep = |model: &Model, face: &Shape| {
2771 model.kept_face_box(face, |_| Ok(unit)).unwrap();
2772 };
2773 let mut model = Model::new();
2774 let face = square(&mut model);
2775 assert_eq!(model.face_bounds(&face), None);
2776 keep(&model, &face);
2777 let kept = model.face_bounds(&face).unwrap();
2778 assert!(kept.contains_box(&unit));
2779
2780 let lift = model.placed(&face, Transform::translation(ogeom_math::Vector::Z));
2782 let lifted = model.face_bounds(&lift).unwrap();
2783 assert!((lifted.low().unwrap().z - 1.0).abs() < 1e-6);
2784 model.widen(&face, Tolerance::new(0.25).unwrap()).unwrap();
2785 let wide = model.face_bounds(&face).unwrap();
2786 assert!((wide.low().unwrap().x + 0.25).abs() < 1e-12);
2787
2788 let vertex = explore_unique(&model, &face, ShapeType::Vertex).unwrap()[0].clone();
2790 let _ = model.node_mut(&vertex);
2791 assert_eq!(model.face_bounds(&face), None);
2792
2793 keep(&model, &face);
2796 let _ = model.node_mut(&face);
2797 assert_eq!(model.face_bounds(&face), None);
2798 keep(&model, &face);
2799 let Some(NodeData::Face(data)) = model.node(&face).map(TShape::data) else {
2800 panic!("a face holds face data");
2801 };
2802 let surface = data.surface;
2803 let _ = model.geometry_mut().surface_mut(surface);
2804 assert_eq!(model.face_bounds(&face), None);
2805 let _ = model.add_point(Point::ORIGIN);
2806 keep(&model, &face);
2807 assert!(model.face_bounds(&face).is_some());
2808
2809 let alone = model.add_point(Point::ORIGIN);
2811 let _ = model.node_mut(&alone);
2812 assert!(model.face_bounds(&face).is_some());
2813 }
2814
2815 fn kept_walk(model: &Model, shape: &Shape) -> (Vec<u32>, f64, bool) {
2818 let asked = std::cell::Cell::new(false);
2819 let kept = model
2820 .kept_read(
2821 shape,
2822 |_: &(Vec<u32>, f64)| true,
2823 || {
2824 asked.set(true);
2825 let set = model.indices_below(shape);
2826 let mut below: Vec<u32> = model
2827 .nodes
2828 .iter()
2829 .map(|(id, _)| id.index())
2830 .filter(|&i| set.contains(i))
2831 .collect();
2832 below.sort_unstable();
2833 let loosest = below
2834 .iter()
2835 .filter_map(|&i| model.nodes.at(i))
2836 .filter_map(|id| model.node_by_id(id)?.data().tolerance())
2837 .fold(0.0, |m: f64, t| m.max(t.get()));
2838 Ok(((below, loosest), true))
2839 },
2840 )
2841 .unwrap();
2842 (kept.0.clone(), kept.1, asked.get())
2843 }
2844
2845 #[test]
2846 fn a_kept_read_is_read_again_after_any_edit_below_its_shape() {
2847 let mut model = Model::new();
2848 let face = square(&mut model);
2849 let read = |model: &Model| kept_walk(model, &face);
2850 let first = read(&model);
2851 assert!(first.2);
2852 assert_eq!(read(&model), (first.0.clone(), first.1, false));
2854 let alone = model.add_point(Point::ORIGIN);
2855 let _ = model.node_mut(&alone);
2856 model.widen(&alone, Tolerance::new(1.0).unwrap()).unwrap();
2857 assert!(!read(&model).2);
2858
2859 let vertex = explore_unique(&model, &face, ShapeType::Vertex).unwrap()[0].clone();
2861 if let Some(node) = model.node_mut(&vertex)
2862 && let NodeData::Vertex(data) = node.data_mut()
2863 {
2864 data.tolerance = Tolerance::new(0.5).unwrap();
2865 }
2866 assert_eq!(read(&model).1, 0.5);
2867 assert!(!read(&model).2);
2868
2869 model.widen(&vertex, Tolerance::new(0.75).unwrap()).unwrap();
2871 assert_eq!(read(&model).1, 0.75);
2872 model.widen(&vertex, Tolerance::new(0.25).unwrap()).unwrap();
2873 assert!(!read(&model).2);
2874
2875 let mark = model.note_widened();
2877 model.widen(&face, Tolerance::new(0.9).unwrap()).unwrap();
2878 assert_eq!(read(&model).1, 0.9);
2879 model.undo_widened(mark);
2880 assert_eq!(read(&model), (first.0.clone(), 0.75, true));
2881
2882 let Some(NodeData::Face(data)) = model.node(&face).map(TShape::data) else {
2884 panic!("a face holds face data");
2885 };
2886 let surface = data.surface;
2887 let _ = model.geometry_mut().surface_mut(surface);
2888 assert!(read(&model).2);
2889 assert!(!read(&model).2);
2890 let line = ogeom_geom::PlanarCurve::Line(
2891 ogeom_geom::Line2d::segment(
2892 ogeom_math::Point2::new(0.0, 0.0),
2893 ogeom_math::Point2::new(1.0, 0.0),
2894 T,
2895 )
2896 .unwrap(),
2897 );
2898 let pcurve = model.geometry_mut().add_pcurve(line);
2899 let _ = model.geometry_mut().pcurve_mut(pcurve);
2900 assert!(read(&model).2);
2901
2902 let mut copy = model.clone();
2904 assert!(!kept_walk(©, &face).2);
2905 let _ = copy.node_mut(&vertex);
2906 assert!(kept_walk(©, &face).2);
2907 assert!(!read(&model).2);
2908
2909 model.retain_reachable(std::slice::from_ref(&face)).unwrap();
2911 assert!(!read(&model).2);
2912 }
2913
2914 #[test]
2915 fn a_kept_read_is_read_again_where_a_child_is_pointed_at_a_copy() {
2916 let mut model = Model::new();
2917 let face = square(&mut model);
2918 let since = model.node_count();
2919 let shell = model.add_shell(std::slice::from_ref(&face)).unwrap();
2920 model.note_held(&shell, since).unwrap();
2921 let before = kept_walk(&model, &shell);
2922 assert!(before.2);
2923 assert!(!kept_walk(&model, &shell).2);
2924 let copies = model.unshare(&shell).unwrap();
2925 assert!(!copies.is_empty());
2926 let after = kept_walk(&model, &shell);
2927 assert!(after.2);
2928 assert_ne!(after.0, before.0);
2929 }
2930
2931 #[test]
2935 fn a_kept_read_beside_another_is_read_again_after_an_edit_below_any_part() {
2936 let mut built = Model::new();
2937 let face = square(&mut built);
2938 let other = square(&mut built);
2939 let shell = built.add_shell(&[face.clone(), other.clone()]).unwrap();
2940 for edited in [&face, &other] {
2941 let mut model = built.clone();
2942 assert!(kept_walk(&model, &face).2);
2943 assert!(kept_walk(&model, &shell).2);
2944 assert!(!kept_walk(&model, &shell).2);
2945 let edits = model.geometry.edits();
2946 let kept = model.nodes.reads.below(&shell, edits).unwrap();
2947 let walked = model.indices_below(&shell);
2948 let all: Vec<u32> = model.nodes.iter().map(|(id, _)| id.index()).collect();
2949 assert!(all.iter().all(|&i| !walked.contains(i) || kept.contains(i)));
2950 let vertex = explore_unique(&model, edited, ShapeType::Vertex).unwrap()[0].clone();
2951 let _ = model.node_mut(&vertex);
2952 assert!(kept_walk(&model, &shell).2);
2953 }
2954 }
2955
2956 #[test]
2957 fn a_face_widens_the_edges_it_borders() {
2958 let mut model = Model::new();
2959 let a = model.add_point(Point::ORIGIN);
2960 let b = model.add_point(Point::new(1.0, 0.0, 0.0));
2961 let edge = model.add_edge(EdgeData::new(), &[a.clone(), b]).unwrap();
2962 let wire = model.add_wire(std::slice::from_ref(&edge)).unwrap();
2963
2964 let surface = model
2965 .geometry_mut()
2966 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
2967 let mut face_data = FaceData::new(surface, Location::identity());
2968 face_data.widen(Tolerance::new(1e-2).unwrap());
2969 let face = model.add_face(face_data, &[wire]).unwrap();
2970
2971 assert_eq!(
2972 model.tolerance_of(&edge).unwrap(),
2973 Some(Tolerance::new(1e-2).unwrap())
2974 );
2975 assert_eq!(
2978 model.tolerance_of(&a).unwrap(),
2979 Some(Tolerance::new(1e-2).unwrap()),
2980 "widening a face must reach its edges' vertices, not just its edges"
2981 );
2982 assert!(model.check_tolerances(&face).is_ok());
2983 }
2984
2985 #[test]
2986 fn check_tolerances_catches_a_violation_the_builder_would_never_make() {
2987 let mut model = Model::new();
2992 let vertex = Shape::of(model.insert_node(TShape::leaf(
2993 ShapeType::Vertex,
2994 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
2995 )));
2996
2997 let mut edge_data = EdgeData::new();
2998 edge_data.widen(Tolerance::new(1e-1).unwrap());
2999 let edge = Shape::of(model.insert_node(TShape::new(
3000 ShapeType::Edge,
3001 NodeData::Edge(Box::new(edge_data)),
3002 vec![vertex.clone()],
3003 )));
3004
3005 let err = model.check_tolerances(&edge).unwrap_err();
3006 assert!(
3007 err.to_string().contains("tighter"),
3008 "unexpected message: {err}"
3009 );
3010
3011 model.widen(&edge, Tolerance::new(1e-1).unwrap()).unwrap();
3013 assert!(model.check_tolerances(&edge).is_ok());
3014 assert_eq!(
3015 model.tolerance_of(&vertex).unwrap(),
3016 Some(Tolerance::new(1e-1).unwrap())
3017 );
3018 }
3019
3020 #[test]
3023 fn check_tolerances_holds_a_face_to_its_edges_across_the_wire() {
3024 let mut model = Model::new();
3025 let vertex = Shape::of(model.insert_node(TShape::leaf(
3026 ShapeType::Vertex,
3027 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
3028 )));
3029 let edge = Shape::of(model.insert_node(TShape::new(
3030 ShapeType::Edge,
3031 NodeData::Edge(Box::default()),
3032 vec![vertex],
3033 )));
3034 let wire =
3035 Shape::of(model.insert_node(TShape::container(ShapeType::Wire, vec![edge.clone()])));
3036 let surface = model
3037 .geometry_mut()
3038 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3039 let mut face_data = FaceData::new(surface, Location::identity());
3040 face_data.widen(Tolerance::new(1e-2).unwrap());
3041 let face = Shape::of(model.insert_node(TShape::new(
3042 ShapeType::Face,
3043 NodeData::Face(Box::new(face_data)),
3044 vec![wire],
3045 )));
3046 let err = model.check_tolerances(&face).unwrap_err();
3047 assert!(err.to_string().contains("tighter"), "{err}");
3048 model.widen(&face, Tolerance::new(1e-2).unwrap()).unwrap();
3049 assert!(model.check_tolerances(&face).is_ok());
3050 }
3051
3052 #[test]
3053 fn a_face_with_no_wires_is_naturally_restricted() {
3054 let mut model = Model::new();
3055 let surface = model
3056 .geometry_mut()
3057 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3058 let face = model
3059 .add_face(FaceData::new(surface, Location::identity()), &[])
3060 .unwrap();
3061 assert!(
3062 model
3063 .node(&face)
3064 .unwrap()
3065 .data()
3066 .as_face()
3067 .unwrap()
3068 .natural_restriction,
3069 "an untrimmed face needs no point-in-face test at all"
3070 );
3071 }
3072
3073 #[test]
3074 fn a_shared_sub_shape_is_yielded_once_per_route_and_deduplicated_on_request() {
3075 let mut model = Model::new();
3079 let a = model.add_point(Point::ORIGIN);
3080 let b = model.add_point(Point::new(1.0, 0.0, 0.0));
3081 let shared = model.add_edge(EdgeData::new(), &[a, b]).unwrap();
3082
3083 let wire_one = model.add_wire(std::slice::from_ref(&shared)).unwrap();
3084 let wire_two = model.add_wire(&[shared.reversed()]).unwrap();
3085 let surface = model
3086 .geometry_mut()
3087 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3088 let face_one = model
3089 .add_face(FaceData::new(surface, Location::identity()), &[wire_one])
3090 .unwrap();
3091 let face_two = model
3092 .add_face(FaceData::new(surface, Location::identity()), &[wire_two])
3093 .unwrap();
3094 let shell = model.add_shell(&[face_one, face_two]).unwrap();
3095
3096 let all = explore(&model, &shell, Filter::OfType(ShapeType::Edge)).unwrap();
3097 assert_eq!(all.len(), 2, "one occurrence per route");
3098 assert_ne!(all[0].orientation(), all[1].orientation());
3099
3100 let distinct = explore_unique(&model, &shell, ShapeType::Edge).unwrap();
3101 assert_eq!(distinct.len(), 1, "one edge, seen from two sides");
3102 }
3103
3104 #[test]
3105 fn ancestors_answers_which_faces_meet_at_an_edge() {
3106 let mut model = Model::new();
3107 let a = model.add_point(Point::ORIGIN);
3108 let b = model.add_point(Point::new(1.0, 0.0, 0.0));
3109 let shared = model.add_edge(EdgeData::new(), &[a, b]).unwrap();
3110 let isolated = model.add_point(Point::new(5.0, 5.0, 5.0));
3111 let lone = model.add_edge(EdgeData::new(), &[isolated]).unwrap();
3112
3113 let surface = model
3114 .geometry_mut()
3115 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3116 let mut faces = Vec::new();
3117 for _ in 0..2 {
3118 let wire = model.add_wire(std::slice::from_ref(&shared)).unwrap();
3119 faces.push(
3120 model
3121 .add_face(FaceData::new(surface, Location::identity()), &[wire])
3122 .unwrap(),
3123 );
3124 }
3125 let third_wire = model.add_wire(std::slice::from_ref(&lone)).unwrap();
3126 faces.push(
3127 model
3128 .add_face(FaceData::new(surface, Location::identity()), &[third_wire])
3129 .unwrap(),
3130 );
3131 let shell = model.add_shell(&faces).unwrap();
3132
3133 let meeting = ancestors_of(&model, &shell, &shared, ShapeType::Face).unwrap();
3134 assert_eq!(meeting.len(), 2, "two faces meet at the shared edge");
3135 let alone = ancestors_of(&model, &shell, &lone, ShapeType::Face).unwrap();
3136 assert_eq!(alone.len(), 1);
3137 }
3138
3139 #[test]
3140 fn handles_from_another_model_are_reported_rather_than_resolved() {
3141 let mut model = Model::new();
3142 let mut other = Model::new();
3143 let mut foreign = other.add_point(Point::ORIGIN);
3145 for _ in 0..5 {
3146 foreign = other.add_point(Point::ORIGIN);
3147 }
3148 assert!(model.kind_of(&foreign).is_err());
3149 assert!(model.children_of(&foreign).is_err());
3150 assert!(model.add_wire(std::slice::from_ref(&foreign)).is_err());
3151 assert!(model.add_compound(&[foreign]).is_err());
3152 }
3153
3154 #[test]
3155 fn placing_a_shape_shares_its_geometry_rather_than_copying_it() {
3156 let mut model = Model::new();
3159 let face = square(&mut model);
3160 let before = model.node_count();
3161
3162 let mut instances = Vec::new();
3163 for i in 0..100 {
3164 instances.push(model.placed(
3165 &face,
3166 Transform::translation(Vector::new(f64::from(i), 0.0, 0.0)),
3167 ));
3168 }
3169 assert_eq!(model.node_count(), before, "no topology was duplicated");
3170 assert!(instances.iter().all(|s| s.is_partner(&face)));
3171 assert!(instances.iter().all(|s| !s.is_same(&face)));
3172
3173 let a = instances[0].transform(model.datums()).unwrap();
3175 let b = instances[99].transform(model.datums()).unwrap();
3176 assert!(!a.is_equal(&b, T));
3177 }
3178
3179 #[test]
3180 fn what_a_failed_attempt_widened_is_put_back() {
3181 let mut model = Model::new();
3182 let tolerance =
3183 |model: &Model, v: &Shape| model.node(v).unwrap().data().tolerance().unwrap().get();
3184 let v = model.add_point(Point::new(0.0, 0.0, 0.0));
3185 let w = model.add_point(Point::new(1.0, 0.0, 0.0));
3186 let was = tolerance(&model, &v);
3187 let outer = model.note_widened();
3188 model.widen(&v, Tolerance::new(0.5).unwrap()).unwrap();
3189 let inner = model.note_widened();
3191 model.widen(&v, Tolerance::new(2.0).unwrap()).unwrap();
3192 model.widen(&w, Tolerance::new(3.0).unwrap()).unwrap();
3193 model.undo_widened(inner);
3194 assert_eq!(tolerance(&model, &v), 0.5);
3195 assert_eq!(tolerance(&model, &w), was);
3196 model.undo_widened(outer);
3197 assert_eq!(tolerance(&model, &v), was);
3198 let edit = model.note_widened();
3200 if let Some(node) = model.node_mut(&w)
3201 && let NodeData::Vertex(data) = node.data_mut()
3202 {
3203 data.tolerance = Tolerance::new(4.0).unwrap();
3204 }
3205 model.undo_widened(edit);
3206 assert_eq!(tolerance(&model, &w), was);
3207 let kept = model.note_widened();
3209 model.widen(&v, Tolerance::new(0.25).unwrap()).unwrap();
3210 model.keep_widened(kept);
3211 let after = model.note_widened();
3212 model.undo_widened(after);
3213 assert_eq!(tolerance(&model, &v), 0.25);
3214 }
3215
3216 #[test]
3217 fn an_empty_model_reports_itself_as_empty() {
3218 let model = Model::new();
3219 assert!(model.is_empty());
3220 assert_eq!(model.node_count(), 0);
3221 assert_eq!(model.geometry().counts(), (0, 0, 0));
3222 assert!(model.datums().is_empty());
3223 }
3224
3225 #[test]
3226 fn a_vertex_has_no_children_and_traversal_stops_there() {
3227 let mut model = Model::new();
3228 let v = model.add_point(Point::new(1.0, 2.0, 3.0));
3229 assert!(model.children_of(&v).unwrap().is_empty());
3230 assert_eq!(explore(&model, &v, Filter::All).unwrap().len(), 1);
3231 assert!(model.check_tolerances(&v).is_ok());
3232 }
3233
3234 #[test]
3235 fn shapes_at_different_places_are_not_the_same_position() {
3236 let mut model = Model::new();
3237 let v = model.add_point(Point::ORIGIN);
3238 let moved = model.placed(&v, Transform::translation(Vector::X));
3239 assert!(!model.same_position(&v, &moved, T).unwrap());
3240 assert!(model.same_position(&v, &v.clone(), T).unwrap());
3241
3242 let again = model.placed(&v, Transform::translation(Vector::X));
3245 assert!(!moved.is_same(&again), "structurally different chains");
3246 assert!(model.same_position(&moved, &again, T).unwrap());
3247 }
3248
3249 #[test]
3250 fn a_direction_is_needed_to_build_a_non_trivial_plane() {
3251 let mut model = Model::new();
3254 let surface = model
3255 .geometry_mut()
3256 .add_surface(PlaneSurface::new(Plane::through(Point::ORIGIN, Direction::Z)).into());
3257 assert!(model.geometry().surface(surface).is_some());
3258 }
3259}