1use std::collections::HashMap;
18
19use ogeom_core::{
20 Arena, EntityId, OgeomResult, OpId, Provenance, ProvenanceTable, Role, Tolerance, Tolerances,
21 ogeom_bail,
22};
23use ogeom_math::{Point, Transform};
24
25use crate::entity::{EdgeData, EdgeRepr, FaceData, NodeData, VertexData};
26use crate::location::{DatumId, DatumStore, Location};
27use crate::shape::{Orientation, Shape, ShapeType, TShape, TShapeId};
28
29pub use crate::entity::GeometryStore;
30
31#[derive(Debug, Clone, Default)]
34pub struct Model {
35 nodes: Arena<TShape>,
36 datums: DatumStore,
37 geometry: GeometryStore,
38 provenance: ProvenanceTable,
39 identity: HashMap<TShapeId, EntityId>,
40 current_op: OpId,
41 tolerances: Tolerances,
42}
43
44impl Model {
45 #[must_use]
47 pub fn new() -> Self {
48 Self::with_tolerances(Tolerances::millimetres())
49 }
50
51 #[must_use]
61 pub fn with_tolerances(tolerances: Tolerances) -> Self {
62 Self {
63 nodes: Arena::new(),
64 datums: DatumStore::new(),
65 geometry: GeometryStore::new(),
66 provenance: ProvenanceTable::new(),
67 identity: HashMap::new(),
68 current_op: OpId(0),
69 tolerances,
70 }
71 }
72
73 #[must_use]
75 pub const fn tolerances(&self) -> Tolerances {
76 self.tolerances
77 }
78
79 pub fn from_parts(parts: ModelParts) -> OgeomResult<Self> {
103 let mut model = Self::with_tolerances(parts.tolerances);
104 model.current_op = parts.current_op;
105 model.absorb_core(parts)?;
110 Ok(model)
111 }
112
113 pub fn absorb(&mut self, parts: ModelParts, roots: &[Shape]) -> OgeomResult<Absorbed> {
164 #[allow(clippy::float_cmp, reason = "scales are copied, never computed")]
165 if parts.tolerances.scale() != self.tolerances.scale() {
166 ogeom_bail!(
167 Construction,
168 "these parts were authored at {} mm per unit and this model at \
169 {}; absorbing across scales needs a rescale, which is its own \
170 operation",
171 parts.tolerances.scale(),
172 self.tolerances.scale()
173 );
174 }
175 if !self.nodes.is_dense() || !self.datums.is_dense() || !self.geometry.is_dense() {
176 ogeom_bail!(
177 Construction,
178 "absorb appends by offset, and this model's arenas have holes; \
179 something removed entries, which nothing in this crate does"
180 );
181 }
182 Self::check_parts_unbound(&parts, roots)?;
183
184 let absorbed_entities = parts.provenance.len() as u64;
185 let (node_offset, datum_offset, entity_offset) = self.absorb_core(parts)?;
186
187 let shapes = roots
188 .iter()
189 .map(|root| self.bind(&root.shifted(node_offset, datum_offset)))
190 .collect::<OgeomResult<Vec<Shape>>>()?;
191 let entities = (1..=absorbed_entities)
192 .filter_map(|raw| {
193 Some((
194 EntityId::from_raw(raw)?,
195 EntityId::from_raw(raw + entity_offset)?,
196 ))
197 })
198 .collect();
199 Ok(Absorbed { shapes, entities })
200 }
201
202 fn absorb_core(&mut self, parts: ModelParts) -> OgeomResult<(u32, u32, u64)> {
208 let ModelParts {
209 mut nodes,
210 datums,
211 geometry,
212 provenance,
213 identity,
214 current_op: _,
215 tolerances: _,
216 } = parts;
217
218 for (issued, entry) in provenance.iter().enumerate() {
222 for source in entry.inputs() {
223 if source.get() > issued as u64 {
224 ogeom_bail!(
225 Dangling,
226 "an entity is derived from identity {}, which no entry \
227 before it issued",
228 source.get()
229 );
230 }
231 }
232 }
233
234 let node_offset = crate::entity::arena_len(&self.nodes);
235 let datum_offset = u32::try_from(self.datums.len()).unwrap_or(u32::MAX);
236 let entity_offset = self.provenance.len() as u64;
237 let geometry_offsets = self.geometry.append(geometry);
238
239 for node in &mut nodes {
240 for child in node.children_mut() {
241 *child = child.shifted(node_offset, datum_offset);
242 }
243 match node.data_mut() {
244 NodeData::Edge(edge) => {
245 for repr in &mut edge.representations {
246 repr.shift(&geometry_offsets, datum_offset);
247 }
248 }
249 NodeData::Face(face) => {
250 face.surface =
251 crate::entity::shifted_key(face.surface, geometry_offsets.surfaces);
252 face.triangulation = face.triangulation.map(|mesh| {
253 crate::entity::shifted_key(mesh, geometry_offsets.triangulations)
254 });
255 face.location = face.location.with_datum_offset(datum_offset);
256 }
257 NodeData::Vertex(_) | NodeData::Container => {}
258 }
259 }
260
261 for datum in datums {
262 self.datums.insert(datum);
263 }
264 for mut entry in provenance {
265 if let Provenance::Derived { from, .. } = &mut entry {
266 for source in from.iter_mut() {
267 let Some(shifted) = EntityId::from_raw(source.get() + entity_offset) else {
268 ogeom_bail!(Construction, "an entity id overflowed in the shift");
269 };
270 *source = shifted;
271 }
272 }
273 self.provenance.record(entry);
274 }
275 for node in nodes {
276 self.nodes.insert(node);
277 }
278
279 self.bind_handles(node_offset);
284 let identity: Vec<(TShapeId, EntityId)> = identity
285 .into_iter()
286 .map(|(node, entity)| {
287 let Some(shifted) = EntityId::from_raw(entity.get() + entity_offset) else {
288 ogeom_bail!(Construction, "an entity id overflowed in the shift");
289 };
290 Ok((
291 crate::entity::shifted_key(node, node_offset).with_scope(self.nodes.scope()),
292 shifted,
293 ))
294 })
295 .collect::<OgeomResult<_>>()?;
296
297 self.check_restored(&identity, node_offset)?;
298 for (node, entity) in identity {
299 self.identity.insert(node, entity);
300 }
301 Ok((node_offset, datum_offset, entity_offset))
302 }
303
304 fn check_parts_unbound(parts: &ModelParts, roots: &[Shape]) -> OgeomResult<()> {
308 use crate::entity::key_is_unbound;
309
310 let local_location = |location: &Location| {
311 location
312 .chain()
313 .iter()
314 .all(|&(datum, _)| key_is_unbound(datum))
315 };
316 let local_shape =
317 |shape: &Shape| key_is_unbound(shape.node()) && local_location(shape.location());
318
319 let mut sound = parts.identity.iter().all(|&(node, _)| key_is_unbound(node))
320 && roots.iter().all(local_shape);
321 for node in &parts.nodes {
322 sound = sound && node.children().iter().all(local_shape);
323 match node.data() {
324 NodeData::Edge(edge) => {
325 sound = sound && edge.representations.iter().all(EdgeRepr::is_unbound);
326 }
327 NodeData::Face(face) => {
328 sound = sound
329 && key_is_unbound(face.surface)
330 && face.triangulation.is_none_or(key_is_unbound)
331 && local_location(&face.location);
332 }
333 NodeData::Vertex(_) | NodeData::Container => {}
334 }
335 }
336 if !sound {
337 ogeom_bail!(
338 Construction,
339 "these parts carry handles already bound to an arena, or at a \
340 recycled generation; absorb takes parts exactly as a reader \
341 rebuilt them, and serialization is the one road in"
342 );
343 }
344 Ok(())
345 }
346
347 pub fn bind(&self, shape: &Shape) -> OgeomResult<Shape> {
366 if shape.node().scope() != ogeom_core::UNSCOPED && !self.nodes.issued(shape.node()) {
367 ogeom_bail!(
368 Construction,
369 "this shape belongs to another model; binding it here would \
370 make it resolve and answer about a different entity"
371 );
372 }
373 let bound = shape.rebound(self.nodes.scope(), self.datums.scope());
374 if self.nodes.get(bound.node()).is_none() {
375 ogeom_bail!(Dangling, "shape refers to a node not in this model");
376 }
377 Ok(bound)
378 }
379
380 pub fn bind_location(&self, location: &Location) -> OgeomResult<Location> {
393 let bound = location.with_datum_scope(self.datums.scope());
394 for &(datum, _) in bound.chain() {
395 if self.datums.get(datum).is_none() {
396 ogeom_bail!(Dangling, "location refers to a datum not in this model");
397 }
398 }
399 Ok(bound)
400 }
401
402 fn bind_handles(&mut self, from: u32) {
407 let nodes = self.nodes.scope();
408 let datums = self.datums.scope();
409 let geometry = self.geometry.scopes();
410
411 for (_, node) in self.nodes.iter_mut().filter(|(id, _)| id.index() >= from) {
412 for child in node.children_mut() {
413 *child = child.rebound(nodes, datums);
414 }
415 match node.data_mut() {
416 NodeData::Edge(edge) => {
417 for repr in &mut edge.representations {
418 repr.rebind(&geometry, datums);
419 }
420 }
421 NodeData::Face(face) => {
422 face.surface = face.surface.with_scope(geometry.surfaces);
423 face.triangulation = face
424 .triangulation
425 .map(|mesh| mesh.with_scope(geometry.triangulations));
426 face.location = face.location.with_datum_scope(datums);
427 }
428 NodeData::Vertex(_) | NodeData::Container => {}
429 }
430 }
431 }
432
433 fn check_restored(&self, identity: &[(TShapeId, EntityId)], from: u32) -> OgeomResult<()> {
440 for (id, node) in self.nodes.iter().filter(|(id, _)| id.index() >= from) {
441 let kind = node.kind();
442 match (kind, node.data()) {
443 (ShapeType::Vertex, NodeData::Vertex(_))
444 | (ShapeType::Edge, NodeData::Edge(_))
445 | (ShapeType::Face, NodeData::Face(_)) => {}
446 (
447 ShapeType::Wire
448 | ShapeType::Shell
449 | ShapeType::Solid
450 | ShapeType::CompSolid
451 | ShapeType::Compound,
452 NodeData::Container,
453 ) => {}
454 (kind, data) => {
455 ogeom_bail!(Construction, "node {id:?} is a {kind:?} and holds {data:?}")
456 }
457 }
458 self.check_node_geometry(id, node)?;
459
460 let expected = kind.child_type();
463 for child in node.children() {
464 let Some(below) = self.nodes.get(child.node()) else {
465 ogeom_bail!(Dangling, "node {id:?} names a child that is not there");
466 };
467 if let Some(expected) = expected
468 && kind != ShapeType::Compound
469 && below.kind() != expected
470 {
471 ogeom_bail!(
472 Construction,
473 "a {kind:?} takes {expected:?} children; node {id:?} \
474 names a {:?}",
475 below.kind()
476 );
477 }
478 self.check_location(child.location())?;
479 }
480 }
481 for (node, entity) in identity {
482 if self.nodes.get(*node).is_none() {
483 ogeom_bail!(Dangling, "an identity is bound to a node that is not there");
484 }
485 if entity.get() > self.provenance.len() as u64 {
486 ogeom_bail!(
487 Dangling,
488 "node {node:?} claims identity {}, which was never issued",
489 entity.get()
490 );
491 }
492 }
493 Ok(())
494 }
495
496 fn check_node_geometry(&self, id: TShapeId, node: &TShape) -> OgeomResult<()> {
498 match node.data() {
499 NodeData::Edge(data) => {
500 for repr in &data.representations {
501 if let Some(location) = repr.location() {
502 self.check_location(location)?;
503 }
504 if !self.geometry.holds(repr) {
505 ogeom_bail!(
506 Dangling,
507 "edge {id:?} names geometry that is not in this model"
508 );
509 }
510 }
511 }
512 NodeData::Face(data) => {
513 self.check_location(&data.location)?;
514 if self.geometry.surface(data.surface).is_none() {
515 ogeom_bail!(Dangling, "face {id:?} names a surface that is not there");
516 }
517 if let Some(mesh) = data.triangulation
518 && self.geometry.triangulation(mesh).is_none()
519 {
520 ogeom_bail!(
521 Dangling,
522 "face {id:?} names a triangulation that is not there"
523 );
524 }
525 }
526 NodeData::Vertex(_) | NodeData::Container => {}
527 }
528 Ok(())
529 }
530
531 fn check_location(&self, location: &Location) -> OgeomResult<()> {
533 for &(datum, _) in location.chain() {
534 if self.datums.get(datum).is_none() {
535 ogeom_bail!(Dangling, "a placement names a datum that is not there");
536 }
537 }
538 Ok(())
539 }
540
541 pub const fn begin_operation(&mut self) -> OpId {
548 self.current_op = OpId(self.current_op.0 + 1);
549 self.current_op
550 }
551
552 #[must_use]
554 pub const fn current_operation(&self) -> OpId {
555 self.current_op
556 }
557
558 #[must_use]
564 pub fn identity_of(&self, shape: &Shape) -> Option<EntityId> {
565 self.identity.get(&shape.node()).copied()
566 }
567
568 #[must_use]
570 pub fn provenance_of(&self, shape: &Shape) -> Option<&Provenance> {
571 self.provenance.get(self.identity_of(shape)?)
572 }
573
574 #[must_use]
576 pub const fn provenance(&self) -> &ProvenanceTable {
577 &self.provenance
578 }
579
580 #[must_use]
585 pub fn roots_of(&self, shape: &Shape) -> Vec<EntityId> {
586 self.identity_of(shape)
587 .map(|id| self.provenance.roots(id))
588 .unwrap_or_default()
589 }
590
591 #[must_use]
604 pub fn shape_of(&self, id: EntityId) -> Option<Shape> {
605 self.identity
606 .iter()
607 .find(|(_, entity)| **entity == id)
608 .map(|(node, _)| Shape::of(*node))
609 }
610
611 pub fn set_derived(
622 &mut self,
623 shape: &Shape,
624 from: &[Shape],
625 role: Role,
626 ) -> OgeomResult<EntityId> {
627 if self.node(shape).is_none() {
628 ogeom_bail!(Dangling, "shape refers to a node not in this model");
629 }
630 let sources: Vec<EntityId> = from.iter().filter_map(|s| self.identity_of(s)).collect();
631 let id = self.provenance.derived(self.current_op, sources, role);
632 self.identity.insert(shape.node(), id);
633 Ok(id)
634 }
635
636 fn record_primitive(&mut self, node: TShapeId, role: Role) {
638 let id = self.provenance.primitive(self.current_op, role);
639 self.identity.insert(node, id);
640 }
641
642 #[must_use]
644 pub const fn datums(&self) -> &DatumStore {
645 &self.datums
646 }
647
648 #[must_use]
650 pub const fn geometry(&self) -> &GeometryStore {
651 &self.geometry
652 }
653
654 #[must_use]
656 pub const fn geometry_mut(&mut self) -> &mut GeometryStore {
657 &mut self.geometry
658 }
659
660 pub fn add_datum(&mut self, transform: Transform) -> DatumId {
662 self.datums.insert(transform)
663 }
664
665 #[must_use]
667 pub fn node(&self, shape: &Shape) -> Option<&TShape> {
668 self.nodes.get(shape.node())
669 }
670
671 #[must_use]
673 pub fn node_by_id(&self, id: TShapeId) -> Option<&TShape> {
674 self.nodes.get(id)
675 }
676
677 #[must_use]
684 pub fn node_mut(&mut self, shape: &Shape) -> Option<&mut TShape> {
685 self.nodes.get_mut(shape.node())
686 }
687
688 pub fn kind_of(&self, shape: &Shape) -> OgeomResult<ShapeType> {
695 let Some(node) = self.node(shape) else {
696 ogeom_bail!(Dangling, "shape refers to a node not in this model");
697 };
698 Ok(node.kind())
699 }
700
701 pub fn tolerance_of(&self, shape: &Shape) -> OgeomResult<Option<Tolerance>> {
707 let Some(node) = self.node(shape) else {
708 ogeom_bail!(Dangling, "shape refers to a node not in this model");
709 };
710 Ok(node.data().tolerance())
711 }
712
713 #[must_use]
715 pub fn node_count(&self) -> usize {
716 self.nodes.len()
717 }
718
719 #[must_use]
721 pub fn is_empty(&self) -> bool {
722 self.nodes.is_empty()
723 }
724
725 pub fn nodes(&self) -> impl Iterator<Item = (TShapeId, &TShape)> {
730 self.nodes.iter()
731 }
732
733 pub fn identities(&self) -> impl Iterator<Item = (TShapeId, EntityId)> {
735 self.nodes
736 .iter()
737 .filter_map(|(id, _)| self.identity.get(&id).map(|entity| (id, *entity)))
738 }
739
740 pub fn add_vertex(&mut self, data: VertexData) -> Shape {
742 Shape::of(
743 self.nodes
744 .insert(TShape::leaf(ShapeType::Vertex, NodeData::Vertex(data))),
745 )
746 }
747
748 pub fn add_point(&mut self, point: Point) -> Shape {
750 self.add_vertex(VertexData::new(point))
751 }
752
753 pub fn add_edge(&mut self, data: EdgeData, bounds: &[Shape]) -> OgeomResult<Shape> {
766 if bounds.len() > 2 {
767 ogeom_bail!(
768 Construction,
769 "an edge has at most two bounding vertices, got {}",
770 bounds.len()
771 );
772 }
773 self.check_children(ShapeType::Vertex, bounds)?;
774 for bound in bounds {
777 self.widen(bound, data.tolerance)?;
778 }
779 let node = self.nodes.insert(TShape::new(
780 ShapeType::Edge,
781 NodeData::Edge(Box::new(data)),
782 bounds.to_vec(),
783 ));
784 self.record_primitive(node, Role::SOLE);
785 Ok(Shape::of(node))
786 }
787
788 pub fn add_wire(&mut self, edges: &[Shape]) -> OgeomResult<Shape> {
795 if edges.is_empty() {
796 ogeom_bail!(Construction, "a wire needs at least one edge");
797 }
798 self.check_children(ShapeType::Edge, edges)?;
799 Ok(Shape::of(self.nodes.insert(TShape::container(
800 ShapeType::Wire,
801 edges.to_vec(),
802 ))))
803 }
804
805 pub fn add_face(&mut self, mut data: FaceData, wires: &[Shape]) -> OgeomResult<Shape> {
816 self.check_children(ShapeType::Wire, wires)?;
817 if wires.is_empty() {
818 data.natural_restriction = true;
819 }
820 let face_tolerance = data.tolerance;
823 for wire in wires {
824 let edges = self.children_of(wire)?;
825 for edge in &edges {
826 self.widen(edge, face_tolerance)?;
827 }
828 }
829 let node = self.nodes.insert(TShape::new(
830 ShapeType::Face,
831 NodeData::Face(Box::new(data)),
832 wires.to_vec(),
833 ));
834 self.record_primitive(node, Role::SOLE);
835 Ok(Shape::of(node))
836 }
837
838 pub fn add_shell(&mut self, faces: &[Shape]) -> OgeomResult<Shape> {
845 if faces.is_empty() {
846 ogeom_bail!(Construction, "a shell needs at least one face");
847 }
848 self.check_children(ShapeType::Face, faces)?;
849 Ok(Shape::of(self.nodes.insert(TShape::container(
850 ShapeType::Shell,
851 faces.to_vec(),
852 ))))
853 }
854
855 pub fn add_solid(&mut self, shells: &[Shape]) -> OgeomResult<Shape> {
862 if shells.is_empty() {
863 ogeom_bail!(Construction, "a solid needs at least one shell");
864 }
865 self.check_children(ShapeType::Shell, shells)?;
866 Ok(Shape::of(self.nodes.insert(TShape::container(
867 ShapeType::Solid,
868 shells.to_vec(),
869 ))))
870 }
871
872 pub fn add_compsolid(&mut self, solids: &[Shape]) -> OgeomResult<Shape> {
879 if solids.is_empty() {
880 ogeom_bail!(Construction, "a compsolid needs at least one solid");
881 }
882 self.check_children(ShapeType::Solid, solids)?;
883 Ok(Shape::of(self.nodes.insert(TShape::container(
884 ShapeType::CompSolid,
885 solids.to_vec(),
886 ))))
887 }
888
889 pub fn add_compound(&mut self, shapes: &[Shape]) -> OgeomResult<Shape> {
900 for shape in shapes {
901 if self.node(shape).is_none() {
902 ogeom_bail!(Dangling, "compound member is not in this model");
903 }
904 }
905 Ok(Shape::of(self.nodes.insert(TShape::container(
906 ShapeType::Compound,
907 shapes.to_vec(),
908 ))))
909 }
910
911 pub fn children_of(&self, shape: &Shape) -> OgeomResult<Vec<Shape>> {
926 let Some(node) = self.node(shape) else {
927 ogeom_bail!(Dangling, "shape refers to a node not in this model");
928 };
929 Ok(node
930 .children()
931 .iter()
932 .map(|child| child.moved(shape.location()).composed(shape.orientation()))
933 .collect())
934 }
935
936 pub fn ordered_children_of(&self, shape: &Shape) -> OgeomResult<Vec<Shape>> {
953 let mut children = self.children_of(shape)?;
954 if shape.orientation() == Orientation::Reversed {
955 children.reverse();
956 }
957 Ok(children)
958 }
959
960 pub fn widen(&mut self, shape: &Shape, to: Tolerance) -> OgeomResult<()> {
976 let mut affected = Vec::new();
977 let mut seen = std::collections::HashSet::new();
978 let mut stack = vec![shape.node()];
979 while let Some(id) = stack.pop() {
980 if !seen.insert(id) {
981 continue;
982 }
983 let Some(node) = self.nodes.get(id) else {
984 ogeom_bail!(Dangling, "shape refers to a node not in this model");
985 };
986 affected.push(id);
987 stack.extend(node.children().iter().map(Shape::node));
988 }
989 for id in affected {
990 if let Some(node) = self.nodes.get_mut(id) {
991 node.data_mut().widen(to);
992 }
993 }
994 Ok(())
995 }
996
997 fn check_children(&self, expected: ShapeType, children: &[Shape]) -> OgeomResult<()> {
999 for child in children {
1000 let Some(node) = self.node(child) else {
1001 ogeom_bail!(Dangling, "child refers to a node not in this model");
1002 };
1003 if node.kind() != expected {
1004 ogeom_bail!(
1005 Construction,
1006 "expected a {expected:?} child, got a {:?}",
1007 node.kind()
1008 );
1009 }
1010 }
1011 Ok(())
1012 }
1013
1014 pub fn check_tolerances(&self, root: &Shape) -> OgeomResult<()> {
1024 let Some(node) = self.node(root) else {
1025 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1026 };
1027 let own = node.data().tolerance();
1028 for child in self.children_of(root)? {
1029 if let (Some(parent), Some(child_tolerance)) = (own, self.tolerance_of(&child)?)
1032 && child_tolerance < parent
1033 {
1034 ogeom_bail!(
1035 Invariant,
1036 "a {:?} at tolerance {} bounds a {:?} at {}, which is tighter",
1037 self.kind_of(&child)?,
1038 child_tolerance.get(),
1039 node.kind(),
1040 parent.get()
1041 );
1042 }
1043 self.check_tolerances(&child)?;
1044 }
1045 Ok(())
1046 }
1047
1048 pub fn same_position(&self, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<bool> {
1054 a.is_same_position(b, &self.datums, tol)
1055 }
1056
1057 pub fn placed(&mut self, shape: &Shape, transform: Transform) -> Shape {
1064 let datum = self.add_datum(transform);
1065 shape.moved(&Location::of(datum))
1066 }
1067}
1068
1069#[derive(Debug)]
1072pub struct Absorbed {
1073 pub shapes: Vec<Shape>,
1075 pub entities: HashMap<EntityId, EntityId>,
1079}
1080
1081#[derive(Debug, Default)]
1087pub struct ModelParts {
1088 pub nodes: Vec<TShape>,
1090 pub datums: Vec<crate::location::Datum>,
1092 pub geometry: GeometryStore,
1094 pub provenance: Vec<Provenance>,
1097 pub identity: Vec<(TShapeId, EntityId)>,
1099 pub current_op: OpId,
1101 pub tolerances: Tolerances,
1103}
1104
1105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1107pub enum Filter {
1108 OfType(ShapeType),
1110 All,
1112}
1113
1114pub fn explore(model: &Model, root: &Shape, filter: Filter) -> OgeomResult<Vec<Shape>> {
1130 let mut out = Vec::new();
1131 let mut stack = vec![root.clone()];
1132 while let Some(shape) = stack.pop() {
1133 let Some(node) = model.node(&shape) else {
1137 ogeom_bail!(Dangling, "shape refers to a node not in this model");
1138 };
1139 let matches = match filter {
1140 Filter::OfType(want) => node.kind() == want,
1141 Filter::All => true,
1142 };
1143 let children = node.children();
1144 stack.reserve(children.len());
1145 for child in children.iter().rev() {
1149 stack.push(child.moved(shape.location()).composed(shape.orientation()));
1150 }
1151 if matches {
1152 out.push(shape);
1155 }
1156 }
1157 Ok(out)
1158}
1159
1160pub fn explore_unique(model: &Model, root: &Shape, want: ShapeType) -> OgeomResult<Vec<Shape>> {
1169 use std::collections::HashSet;
1170
1171 use crate::shape::SameKey;
1172
1173 let found = explore(model, root, Filter::OfType(want))?;
1174 let mut seen = HashSet::with_capacity(found.len());
1175 let mut out = Vec::with_capacity(found.len());
1176 for shape in found {
1177 if seen.insert(SameKey(shape.clone())) {
1178 out.push(shape);
1179 }
1180 }
1181 Ok(out)
1182}
1183
1184pub fn ancestors_of(
1193 model: &Model,
1194 root: &Shape,
1195 target: &Shape,
1196 want: ShapeType,
1197) -> OgeomResult<Vec<Shape>> {
1198 let mut out = Vec::new();
1199 for candidate in explore(model, root, Filter::OfType(want))? {
1200 if explore(model, &candidate, Filter::All)?
1201 .iter()
1202 .any(|s| s.is_same(target))
1203 {
1204 out.push(candidate);
1205 }
1206 }
1207 Ok(out)
1208}
1209
1210#[cfg(test)]
1211#[allow(clippy::unwrap_used)]
1212mod tests {
1213 use super::*;
1214 use ogeom_geom::PlaneSurface;
1215 use ogeom_math::{Direction, Frame, Plane, Vector};
1216
1217 const T: Tolerances = Tolerances::millimetres();
1218
1219 fn square(model: &mut Model) -> Shape {
1221 let corners = [
1222 model.add_point(Point::new(0.0, 0.0, 0.0)),
1223 model.add_point(Point::new(1.0, 0.0, 0.0)),
1224 model.add_point(Point::new(1.0, 1.0, 0.0)),
1225 model.add_point(Point::new(0.0, 1.0, 0.0)),
1226 ];
1227 let mut edges = Vec::new();
1228 for i in 0..4 {
1229 let bounds = [corners[i].clone(), corners[(i + 1) % 4].clone()];
1230 edges.push(model.add_edge(EdgeData::new(), &bounds).unwrap());
1231 }
1232 let wire = model.add_wire(&edges).unwrap();
1233 let surface = model
1234 .geometry_mut()
1235 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
1236 model
1237 .add_face(FaceData::new(surface, Location::identity()), &[wire])
1238 .unwrap()
1239 }
1240
1241 fn edge_parts() -> (ModelParts, Vec<Shape>) {
1245 use ogeom_core::Role;
1246
1247 use crate::entity::CurveId;
1248 use crate::location::DatumId;
1249
1250 let mut geometry = GeometryStore::new();
1251 geometry.add_curve(
1252 ogeom_geom::LineCurve::new(ogeom_math::Axis {
1253 location: Point::new(0.0, 0.0, 0.0),
1254 direction: Direction::X,
1255 })
1256 .into(),
1257 );
1258 let ends = [
1259 Shape::of(TShapeId::from_parts(0, 0)),
1260 Shape::of(TShapeId::from_parts(1, 0)).reversed(),
1261 ];
1262 let edge = TShape::new(
1263 ShapeType::Edge,
1264 NodeData::Edge(Box::new(EdgeData::on_curve(
1265 CurveId::from_parts(0, 0),
1266 Location::of(DatumId::from_parts(0, 0)),
1267 (0.0, 2.0),
1268 ))),
1269 ends.to_vec(),
1270 );
1271 let one = EntityId::from_raw(1).unwrap();
1272 let two = EntityId::from_raw(2).unwrap();
1273 let parts = ModelParts {
1274 nodes: vec![
1275 TShape::leaf(
1276 ShapeType::Vertex,
1277 NodeData::Vertex(VertexData::new(Point::new(0.0, 0.0, 0.0))),
1278 ),
1279 TShape::leaf(
1280 ShapeType::Vertex,
1281 NodeData::Vertex(VertexData::new(Point::new(2.0, 0.0, 0.0))),
1282 ),
1283 edge,
1284 ],
1285 datums: vec![Transform::translation(Vector::new(0.0, 0.0, 1.0))],
1286 geometry,
1287 provenance: vec![
1288 Provenance::Primitive {
1289 op: OpId(1),
1290 role: Role::SOLE,
1291 },
1292 Provenance::Derived {
1293 op: OpId(1),
1294 from: [one].into_iter().collect(),
1295 role: Role::SOLE,
1296 },
1297 ],
1298 identity: vec![(TShapeId::from_parts(2, 0), two)],
1299 current_op: OpId(1),
1300 tolerances: T,
1301 };
1302 (parts, vec![Shape::of(TShapeId::from_parts(2, 0))])
1303 }
1304
1305 #[test]
1306 fn absorbing_parts_into_a_live_model_offsets_every_handle() {
1307 let mut model = Model::new();
1308 let face = square(&mut model);
1309
1310 let (parts, roots) = edge_parts();
1311 let absorbed = model.absorb(parts, &roots).unwrap();
1312 assert_eq!(absorbed.shapes.len(), 1);
1313 let edge = &absorbed.shapes[0];
1314
1315 assert_eq!(model.kind_of(edge).unwrap(), ShapeType::Edge);
1317 let vertices = explore(&model, edge, Filter::OfType(ShapeType::Vertex)).unwrap();
1318 assert_eq!(vertices.len(), 2);
1319 let points: Vec<Point> = vertices
1320 .iter()
1321 .map(|v| model.node(v).unwrap().data().as_vertex().unwrap().point)
1322 .collect();
1323 assert!(points.contains(&Point::new(2.0, 0.0, 0.0)), "{points:?}");
1324
1325 let node = model.node(edge).unwrap();
1327 let repr = &node.data().as_edge().unwrap().representations[0];
1328 assert!(
1329 model.geometry().holds(repr),
1330 "the absorbed edge's curve did not land"
1331 );
1332 let EdgeRepr::Curve3d { location, .. } = repr else {
1333 panic!("the representation changed kind in the shift");
1334 };
1335 assert!(
1336 location.composed(model.datums()).is_ok(),
1337 "the absorbed edge's datum did not land"
1338 );
1339
1340 assert_eq!(model.kind_of(&face).unwrap(), ShapeType::Face);
1342 }
1343
1344 #[test]
1345 fn absorbed_identities_keep_their_provenance_under_new_ids() {
1346 let mut model = Model::new();
1347 square(&mut model);
1348 let issued_before = model.provenance().len() as u64;
1349 assert!(
1350 issued_before > 0,
1351 "the square should have minted identities"
1352 );
1353
1354 let (parts, roots) = edge_parts();
1355 let absorbed = model.absorb(parts, &roots).unwrap();
1356
1357 let old = EntityId::from_raw(2).unwrap();
1359 let new = absorbed.entities[&old];
1360 assert_eq!(new.get(), 2 + issued_before);
1361 assert_eq!(model.identity_of(&absorbed.shapes[0]), Some(new));
1362
1363 let entry = model.provenance().get(new).unwrap();
1366 let source = EntityId::from_raw(1 + issued_before).unwrap();
1367 assert_eq!(entry.inputs(), &[source]);
1368 assert_eq!(model.provenance().roots(new), vec![source]);
1369 }
1370
1371 #[test]
1372 fn absorbing_into_an_empty_model_matches_from_parts() {
1373 let (parts, roots) = edge_parts();
1374 let restored = Model::from_parts(parts).unwrap();
1375 let bound = restored.bind(&roots[0]).unwrap();
1376
1377 let (parts, roots) = edge_parts();
1378 let mut empty = Model::new();
1379 let absorbed = empty.absorb(parts, &roots).unwrap();
1380 let shape = &absorbed.shapes[0];
1381
1382 assert_eq!(shape.node().index(), bound.node().index());
1385 assert_eq!(shape.node().generation(), bound.node().generation());
1386 assert_eq!(restored.identity_of(&bound), empty.identity_of(shape));
1387 assert_eq!(restored.provenance().len(), empty.provenance().len());
1388 }
1389
1390 #[test]
1391 fn parts_with_scoped_or_generation_bearing_keys_are_refused() {
1392 let mut model = Model::new();
1393
1394 let (mut parts, roots) = edge_parts();
1395 let child = parts.nodes[2].children()[0].clone();
1396 parts.nodes[2].children_mut()[0] = Shape::new(
1397 child.node().with_scope(7),
1398 Location::identity(),
1399 Orientation::Forward,
1400 );
1401 assert!(
1402 model.absorb(parts, &roots).is_err(),
1403 "a scoped child key should be refused"
1404 );
1405
1406 let (mut parts, roots) = edge_parts();
1407 parts.identity[0].0 = TShapeId::from_parts(2, 1);
1408 assert!(
1409 model.absorb(parts, &roots).is_err(),
1410 "a recycled-generation key should be refused"
1411 );
1412 }
1413
1414 #[test]
1415 fn parts_in_other_units_are_refused() {
1416 let mut model = Model::new();
1417 square(&mut model);
1418 let issued_before = model.provenance().len();
1419
1420 let (mut parts, roots) = edge_parts();
1421 parts.tolerances = Tolerances::metres();
1422 assert!(model.absorb(parts, &roots).is_err());
1423 assert_eq!(
1424 model.provenance().len(),
1425 issued_before,
1426 "a refused absorb should leave the model alone"
1427 );
1428 }
1429
1430 #[test]
1431 fn absorb_leaves_the_current_operation_alone() {
1432 let mut model = Model::new();
1433 model.begin_operation();
1434 model.begin_operation();
1435 let op = model.begin_operation();
1436
1437 let (parts, roots) = edge_parts();
1438 model.absorb(parts, &roots).unwrap();
1439 assert_eq!(model.current_operation(), op);
1440 }
1441
1442 #[test]
1443 fn an_absorbed_root_that_names_a_missing_node_dangles() {
1444 let mut model = Model::new();
1445 let (parts, _) = edge_parts();
1446 let stray = vec![Shape::of(TShapeId::from_parts(99, 0))];
1447 assert!(model.absorb(parts, &stray).is_err());
1448 }
1449
1450 #[test]
1451 fn absorbing_empty_parts_is_a_no_op() {
1452 let mut model = Model::new();
1453 square(&mut model);
1454 let issued_before = model.provenance().len();
1455
1456 let parts = ModelParts {
1457 tolerances: T,
1458 ..ModelParts::default()
1459 };
1460 let absorbed = model.absorb(parts, &[]).unwrap();
1461 assert!(absorbed.shapes.is_empty());
1462 assert!(absorbed.entities.is_empty());
1463 assert_eq!(model.provenance().len(), issued_before);
1464 }
1465
1466 #[test]
1467 fn reversing_a_wire_reverses_the_walk_as_well_as_each_edge() {
1468 let mut model = Model::new();
1473 let face = square(&mut model);
1474 let wire = model.children_of(&face).unwrap()[0].clone();
1475
1476 let forward = model.ordered_children_of(&wire).unwrap();
1477 let backward = model.ordered_children_of(&wire.reversed()).unwrap();
1478
1479 assert_eq!(forward.len(), 4);
1480 assert_eq!(backward.len(), 4);
1481 for (i, edge) in backward.iter().enumerate() {
1482 let partner = &forward[3 - i];
1483 assert!(edge.is_same(partner), "the order did not reverse");
1484 assert_eq!(
1485 edge.orientation(),
1486 Orientation::Reversed.compose(partner.orientation()),
1487 "each edge should also flip"
1488 );
1489 }
1490
1491 let raw = model.children_of(&wire.reversed()).unwrap();
1494 assert!(raw[0].is_same(&forward[0]));
1495 }
1496
1497 #[test]
1498 fn a_built_tree_has_the_expected_shape() {
1499 let mut model = Model::new();
1500 let face = square(&mut model);
1501
1502 assert_eq!(model.kind_of(&face).unwrap(), ShapeType::Face);
1503 assert_eq!(
1504 explore_unique(&model, &face, ShapeType::Wire)
1505 .unwrap()
1506 .len(),
1507 1
1508 );
1509 assert_eq!(
1510 explore_unique(&model, &face, ShapeType::Edge)
1511 .unwrap()
1512 .len(),
1513 4
1514 );
1515 assert_eq!(
1516 explore_unique(&model, &face, ShapeType::Vertex)
1517 .unwrap()
1518 .len(),
1519 4
1520 );
1521 assert_eq!(
1524 explore(&model, &face, Filter::OfType(ShapeType::Vertex))
1525 .unwrap()
1526 .len(),
1527 8
1528 );
1529 }
1530
1531 #[test]
1532 fn children_are_returned_with_the_parents_placement_composed() {
1533 let mut model = Model::new();
1537 let face = square(&mut model);
1538 let moved = model.placed(&face, Transform::translation(Vector::new(10.0, 0.0, 0.0)));
1539
1540 let vertices = explore_unique(&model, &moved, ShapeType::Vertex).unwrap();
1541 assert_eq!(vertices.len(), 4);
1542 for v in &vertices {
1543 let node = model.node(v).unwrap();
1544 let local = node.data().as_vertex().unwrap().point;
1545 let world = v.transform(model.datums()).unwrap().apply(local);
1546 assert!(world.x >= 10.0 - 1e-12, "vertex at {world:?} was not moved");
1547 }
1548 }
1549
1550 #[test]
1551 fn children_are_returned_with_the_parents_orientation_composed() {
1552 let mut model = Model::new();
1553 let face = square(&mut model);
1554 let reversed = face.reversed();
1555
1556 let forward_edges = model
1557 .children_of(&model.children_of(&face).unwrap()[0])
1558 .unwrap();
1559 let reversed_edges = model
1560 .children_of(&model.children_of(&reversed).unwrap()[0])
1561 .unwrap();
1562
1563 for (a, b) in forward_edges.iter().zip(&reversed_edges) {
1564 assert_eq!(
1565 b.orientation(),
1566 a.orientation().reversed(),
1567 "reversing a face must reverse what its edges present, \
1568 without touching a single stored child"
1569 );
1570 }
1571 }
1572
1573 #[test]
1574 fn reversing_a_shape_touches_no_stored_child() {
1575 let mut model = Model::new();
1578 let face = square(&mut model);
1579 let before = model.node(&face).unwrap().clone();
1580 let _ = face.reversed();
1581 assert_eq!(model.node(&face).unwrap(), &before);
1582 }
1583
1584 #[test]
1585 fn placement_composes_through_nesting() {
1586 let mut model = Model::new();
1587 let vertex = model.add_point(Point::new(1.0, 0.0, 0.0));
1588 let edge = model
1589 .add_edge(EdgeData::new(), &[vertex.clone(), vertex.clone()])
1590 .unwrap();
1591 let moved_edge = model.placed(&edge, Transform::translation(Vector::new(10.0, 0.0, 0.0)));
1592 let compound = model.add_compound(&[moved_edge]).unwrap();
1593 let moved_compound = model.placed(
1594 &compound,
1595 Transform::translation(Vector::new(100.0, 0.0, 0.0)),
1596 );
1597
1598 let found = explore_unique(&model, &moved_compound, ShapeType::Vertex).unwrap();
1599 assert_eq!(found.len(), 1);
1600 let local = model
1601 .node(&found[0])
1602 .unwrap()
1603 .data()
1604 .as_vertex()
1605 .unwrap()
1606 .point;
1607 let world = found[0].transform(model.datums()).unwrap().apply(local);
1608 assert!(
1609 world.is_equal(Point::new(111.0, 0.0, 0.0), T),
1610 "expected 1 + 10 + 100, got {world:?}"
1611 );
1612 }
1613
1614 #[test]
1615 fn the_builder_refuses_children_of_the_wrong_type() {
1616 let mut model = Model::new();
1617 let vertex = model.add_point(Point::ORIGIN);
1618 let edge = model
1619 .add_edge(EdgeData::new(), std::slice::from_ref(&vertex))
1620 .unwrap();
1621
1622 assert!(
1623 model.add_wire(std::slice::from_ref(&vertex)).is_err(),
1624 "a wire holds edges"
1625 );
1626 assert!(
1627 model.add_shell(std::slice::from_ref(&edge)).is_err(),
1628 "a shell holds faces"
1629 );
1630 assert!(
1631 model.add_solid(std::slice::from_ref(&edge)).is_err(),
1632 "a solid holds shells"
1633 );
1634 assert!(model.add_wire(&[edge]).is_ok());
1635
1636 assert!(model.add_compound(&[vertex]).is_ok());
1638 }
1639
1640 #[test]
1641 fn empty_containers_are_refused_except_a_compound() {
1642 let mut model = Model::new();
1643 assert!(model.add_wire(&[]).is_err());
1644 assert!(model.add_shell(&[]).is_err());
1645 assert!(model.add_solid(&[]).is_err());
1646 assert!(model.add_compsolid(&[]).is_err());
1647 assert!(model.add_compound(&[]).is_ok());
1650 }
1651
1652 #[test]
1653 fn an_edge_takes_at_most_two_vertices() {
1654 let mut model = Model::new();
1655 let v = model.add_point(Point::ORIGIN);
1656 assert!(model.add_edge(EdgeData::new(), &[]).is_ok(), "unbounded");
1657 assert!(
1658 model
1659 .add_edge(EdgeData::new(), std::slice::from_ref(&v))
1660 .is_ok()
1661 );
1662 assert!(
1663 model
1664 .add_edge(EdgeData::new(), &[v.clone(), v.clone()])
1665 .is_ok()
1666 );
1667 assert!(
1668 model
1669 .add_edge(EdgeData::new(), &[v.clone(), v.clone(), v])
1670 .is_err(),
1671 "three ends is not an edge"
1672 );
1673 }
1674
1675 #[test]
1676 fn building_enforces_the_containment_rule_upward() {
1677 let mut model = Model::new();
1681 let vertex = model.add_point(Point::ORIGIN);
1682 assert_eq!(model.tolerance_of(&vertex).unwrap(), Some(Tolerance::MIN));
1683
1684 let mut edge_data = EdgeData::new();
1685 edge_data.widen(Tolerance::new(1e-3).unwrap());
1686 let edge = model
1687 .add_edge(edge_data, std::slice::from_ref(&vertex))
1688 .unwrap();
1689
1690 assert_eq!(
1691 model.tolerance_of(&vertex).unwrap(),
1692 Some(Tolerance::new(1e-3).unwrap()),
1693 "the vertex was widened to contain its edge"
1694 );
1695 assert!(model.check_tolerances(&edge).is_ok());
1696 }
1697
1698 #[test]
1699 fn a_face_widens_the_edges_it_borders() {
1700 let mut model = Model::new();
1701 let a = model.add_point(Point::ORIGIN);
1702 let b = model.add_point(Point::new(1.0, 0.0, 0.0));
1703 let edge = model.add_edge(EdgeData::new(), &[a.clone(), b]).unwrap();
1704 let wire = model.add_wire(std::slice::from_ref(&edge)).unwrap();
1705
1706 let surface = model
1707 .geometry_mut()
1708 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
1709 let mut face_data = FaceData::new(surface, Location::identity());
1710 face_data.widen(Tolerance::new(1e-2).unwrap());
1711 let face = model.add_face(face_data, &[wire]).unwrap();
1712
1713 assert_eq!(
1714 model.tolerance_of(&edge).unwrap(),
1715 Some(Tolerance::new(1e-2).unwrap())
1716 );
1717 assert_eq!(
1720 model.tolerance_of(&a).unwrap(),
1721 Some(Tolerance::new(1e-2).unwrap()),
1722 "widening a face must reach its edges' vertices, not just its edges"
1723 );
1724 assert!(model.check_tolerances(&face).is_ok());
1725 }
1726
1727 #[test]
1728 fn check_tolerances_catches_a_violation_the_builder_would_never_make() {
1729 let mut model = Model::new();
1734 let vertex = Shape::of(model.nodes.insert(TShape::leaf(
1735 ShapeType::Vertex,
1736 NodeData::Vertex(VertexData::new(Point::ORIGIN)),
1737 )));
1738
1739 let mut edge_data = EdgeData::new();
1740 edge_data.widen(Tolerance::new(1e-1).unwrap());
1741 let edge = Shape::of(model.nodes.insert(TShape::new(
1742 ShapeType::Edge,
1743 NodeData::Edge(Box::new(edge_data)),
1744 vec![vertex.clone()],
1745 )));
1746
1747 let err = model.check_tolerances(&edge).unwrap_err();
1748 assert!(
1749 err.to_string().contains("tighter"),
1750 "unexpected message: {err}"
1751 );
1752
1753 model.widen(&edge, Tolerance::new(1e-1).unwrap()).unwrap();
1755 assert!(model.check_tolerances(&edge).is_ok());
1756 assert_eq!(
1757 model.tolerance_of(&vertex).unwrap(),
1758 Some(Tolerance::new(1e-1).unwrap())
1759 );
1760 }
1761
1762 #[test]
1763 fn a_face_with_no_wires_is_naturally_restricted() {
1764 let mut model = Model::new();
1765 let surface = model
1766 .geometry_mut()
1767 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
1768 let face = model
1769 .add_face(FaceData::new(surface, Location::identity()), &[])
1770 .unwrap();
1771 assert!(
1772 model
1773 .node(&face)
1774 .unwrap()
1775 .data()
1776 .as_face()
1777 .unwrap()
1778 .natural_restriction,
1779 "an untrimmed face needs no point-in-face test at all"
1780 );
1781 }
1782
1783 #[test]
1784 fn a_shared_sub_shape_is_yielded_once_per_route_and_deduplicated_on_request() {
1785 let mut model = Model::new();
1789 let a = model.add_point(Point::ORIGIN);
1790 let b = model.add_point(Point::new(1.0, 0.0, 0.0));
1791 let shared = model.add_edge(EdgeData::new(), &[a, b]).unwrap();
1792
1793 let wire_one = model.add_wire(std::slice::from_ref(&shared)).unwrap();
1794 let wire_two = model.add_wire(&[shared.reversed()]).unwrap();
1795 let surface = model
1796 .geometry_mut()
1797 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
1798 let face_one = model
1799 .add_face(FaceData::new(surface, Location::identity()), &[wire_one])
1800 .unwrap();
1801 let face_two = model
1802 .add_face(FaceData::new(surface, Location::identity()), &[wire_two])
1803 .unwrap();
1804 let shell = model.add_shell(&[face_one, face_two]).unwrap();
1805
1806 let all = explore(&model, &shell, Filter::OfType(ShapeType::Edge)).unwrap();
1807 assert_eq!(all.len(), 2, "one occurrence per route");
1808 assert_ne!(all[0].orientation(), all[1].orientation());
1809
1810 let distinct = explore_unique(&model, &shell, ShapeType::Edge).unwrap();
1811 assert_eq!(distinct.len(), 1, "one edge, seen from two sides");
1812 }
1813
1814 #[test]
1815 fn ancestors_answers_which_faces_meet_at_an_edge() {
1816 let mut model = Model::new();
1817 let a = model.add_point(Point::ORIGIN);
1818 let b = model.add_point(Point::new(1.0, 0.0, 0.0));
1819 let shared = model.add_edge(EdgeData::new(), &[a, b]).unwrap();
1820 let isolated = model.add_point(Point::new(5.0, 5.0, 5.0));
1821 let lone = model.add_edge(EdgeData::new(), &[isolated]).unwrap();
1822
1823 let surface = model
1824 .geometry_mut()
1825 .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
1826 let mut faces = Vec::new();
1827 for _ in 0..2 {
1828 let wire = model.add_wire(std::slice::from_ref(&shared)).unwrap();
1829 faces.push(
1830 model
1831 .add_face(FaceData::new(surface, Location::identity()), &[wire])
1832 .unwrap(),
1833 );
1834 }
1835 let third_wire = model.add_wire(std::slice::from_ref(&lone)).unwrap();
1836 faces.push(
1837 model
1838 .add_face(FaceData::new(surface, Location::identity()), &[third_wire])
1839 .unwrap(),
1840 );
1841 let shell = model.add_shell(&faces).unwrap();
1842
1843 let meeting = ancestors_of(&model, &shell, &shared, ShapeType::Face).unwrap();
1844 assert_eq!(meeting.len(), 2, "two faces meet at the shared edge");
1845 let alone = ancestors_of(&model, &shell, &lone, ShapeType::Face).unwrap();
1846 assert_eq!(alone.len(), 1);
1847 }
1848
1849 #[test]
1850 fn handles_from_another_model_are_reported_rather_than_resolved() {
1851 let mut model = Model::new();
1852 let mut other = Model::new();
1853 let mut foreign = other.add_point(Point::ORIGIN);
1855 for _ in 0..5 {
1856 foreign = other.add_point(Point::ORIGIN);
1857 }
1858 assert!(model.kind_of(&foreign).is_err());
1859 assert!(model.children_of(&foreign).is_err());
1860 assert!(model.add_wire(std::slice::from_ref(&foreign)).is_err());
1861 assert!(model.add_compound(&[foreign]).is_err());
1862 }
1863
1864 #[test]
1865 fn placing_a_shape_shares_its_geometry_rather_than_copying_it() {
1866 let mut model = Model::new();
1869 let face = square(&mut model);
1870 let before = model.node_count();
1871
1872 let mut instances = Vec::new();
1873 for i in 0..100 {
1874 instances.push(model.placed(
1875 &face,
1876 Transform::translation(Vector::new(f64::from(i), 0.0, 0.0)),
1877 ));
1878 }
1879 assert_eq!(model.node_count(), before, "no topology was duplicated");
1880 assert!(instances.iter().all(|s| s.is_partner(&face)));
1881 assert!(instances.iter().all(|s| !s.is_same(&face)));
1882
1883 let a = instances[0].transform(model.datums()).unwrap();
1885 let b = instances[99].transform(model.datums()).unwrap();
1886 assert!(!a.is_equal(&b, T));
1887 }
1888
1889 #[test]
1890 fn an_empty_model_reports_itself_as_empty() {
1891 let model = Model::new();
1892 assert!(model.is_empty());
1893 assert_eq!(model.node_count(), 0);
1894 assert_eq!(model.geometry().counts(), (0, 0, 0));
1895 assert!(model.datums().is_empty());
1896 }
1897
1898 #[test]
1899 fn a_vertex_has_no_children_and_traversal_stops_there() {
1900 let mut model = Model::new();
1901 let v = model.add_point(Point::new(1.0, 2.0, 3.0));
1902 assert!(model.children_of(&v).unwrap().is_empty());
1903 assert_eq!(explore(&model, &v, Filter::All).unwrap().len(), 1);
1904 assert!(model.check_tolerances(&v).is_ok());
1905 }
1906
1907 #[test]
1908 fn shapes_at_different_places_are_not_the_same_position() {
1909 let mut model = Model::new();
1910 let v = model.add_point(Point::ORIGIN);
1911 let moved = model.placed(&v, Transform::translation(Vector::X));
1912 assert!(!model.same_position(&v, &moved, T).unwrap());
1913 assert!(model.same_position(&v, &v.clone(), T).unwrap());
1914
1915 let again = model.placed(&v, Transform::translation(Vector::X));
1918 assert!(!moved.is_same(&again), "structurally different chains");
1919 assert!(model.same_position(&moved, &again, T).unwrap());
1920 }
1921
1922 #[test]
1923 fn a_direction_is_needed_to_build_a_non_trivial_plane() {
1924 let mut model = Model::new();
1927 let surface = model
1928 .geometry_mut()
1929 .add_surface(PlaneSurface::new(Plane::through(Point::ORIGIN, Direction::Z)).into());
1930 assert!(model.geometry().surface(surface).is_some());
1931 }
1932}