Skip to main content

ogeom_topo/
model.rs

1//! The model: the arenas a shape's handles refer into, and the builder that is
2//! the only way to mutate them.
3//!
4//! A [`Model`] owns the topology nodes, the placement datums and the geometry.
5//! A [`Shape`] is meaningless without one: its handles index into these arenas
6//! and nothing else (`docs/DATA_MODEL.md` §11).
7//!
8//! # One mutation path
9//!
10//! Every structural change goes through [`Model`]'s builder methods. That is
11//! not ceremony: the invariants of `docs/DATA_MODEL.md` (a wire holds edges, a
12//! face's tolerance does not exceed its edges', a node's kind matches its data)
13//! are checkable in one place only if there is one place. Handing out
14//! `&mut TShape` would scatter them across every caller, and the failures they
15//! guard against are silent ones.
16
17use 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/// A document: topology, placements, geometry, and where every entity came
32/// from.
33#[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    /// An empty model, in millimetres.
46    #[must_use]
47    pub fn new() -> Self {
48        Self::with_tolerances(Tolerances::millimetres())
49    }
50
51    /// An empty model at a given unit scale.
52    ///
53    /// A document has a scale, and it is the document's rather than each
54    /// call's: a model authored in metres does not become a model in
55    /// millimetres because one caller passed the default. Algorithms still take
56    /// a [`Tolerances`] argument (that is deliberate, since a caller may want
57    /// to work coarser or finer than the document's own setting for one
58    /// operation), but the document now says what it was built at, so a
59    /// mismatch is visible rather than assumed away.
60    #[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    /// The tolerances this document was built at.
74    #[must_use]
75    pub const fn tolerances(&self) -> Tolerances {
76        self.tolerances
77    }
78
79    /// Assemble a model from parts read back from a file.
80    ///
81    /// The one way into a [`Model`] that does not go through its builders, and
82    /// it exists for one reason: a builder *mints* an identity for every node
83    /// it makes (`docs/DATA_MODEL.md` §8). A document rebuilt through the
84    /// builders is therefore a different document from the one that was
85    /// written (every [`EntityId`] renumbered, every provenance record
86    /// replaced by a fresh `Primitive` one), and every reference into it,
87    /// which is the thing provenance exists to keep alive, is dead. Reading a
88    /// file has to reproduce the document it describes, identities and all.
89    ///
90    /// This is not a hole in "the builder is the sole mutation path". Nothing
91    /// here mutates an existing model; it assembles a new one, and it checks
92    /// the structural invariants the builders check before handing it back,
93    /// so a corrupt file is an error, not a model that answers wrongly.
94    ///
95    /// # Errors
96    ///
97    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a node names a
98    /// child, a datum, a piece of geometry or an identity that is not there;
99    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a node's
100    /// data does not match its kind, or a child is of the wrong kind for its
101    /// parent.
102    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        // Absorbing into an empty model is exactly restoration: every offset
106        // is zero, so each handle keeps the index and generation the file
107        // recorded, which is what makes a document's handles survive a round
108        // trip.
109        model.absorb_core(parts)?;
110        Ok(model)
111    }
112
113    /// Absorb another document's parts into this model.
114    ///
115    /// [`Model::from_parts`] lands a document in a *fresh* model; this lands
116    /// one in a model that already has things in it: the operation behind
117    /// bringing a serialized tool body into a live document so a boolean can
118    /// use it. Everything the parts carry is appended: nodes, datums,
119    /// geometry, provenance and identities all keep their relative structure,
120    /// shifted past what the model already holds. The result behaves exactly
121    /// as if it had been built here, because after the shift it is
122    /// indistinguishable from having been.
123    ///
124    /// `roots` are the shapes the source document named, in the unbound state
125    /// a reader leaves them; they come back bound to this model. The returned
126    /// [`Absorbed::entities`] table says where every source identity landed,
127    /// which is what a caller holding references against the source document
128    /// resolves them through.
129    ///
130    /// Three deliberate refusals, each an error rather than a guess:
131    ///
132    /// - **Units.** A document authored at another scale is refused, not
133    ///   rescaled; rescaling is a real feature with real decisions in it,
134    ///   and silently absorbing metres into millimetres is a wrong model.
135    /// - **Bound handles.** Parts whose handles already name an arena did not
136    ///   come from a reader; absorbing them would alias whatever those
137    ///   handles meant elsewhere. Serialization is the one road in.
138    /// - **A model with holes.** Absorbing appends by offset, which is only
139    ///   sound while the target's arenas have only ever been appended to.
140    ///   Nothing in this crate removes, so this cannot trigger today; it is
141    ///   checked so a future that removes gets an error, not aliasing.
142    ///
143    /// The current operation is left alone: absorb mints no identities, it
144    /// transplants a table, and the absorbed provenance keeps its source
145    /// [`OpId`]s verbatim, meaningful in the source document's rebuild, kept
146    /// because renumbering them would orphan the source's own references.
147    ///
148    /// # Errors
149    ///
150    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) for
151    /// the three refusals above;
152    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the parts
153    /// do not describe themselves: a handle that does not resolve, a
154    /// derivation from an identity no entry issued, a root naming a node that
155    /// is not there.
156    ///
157    /// On an error past the up-front gates, the model's prior contents are
158    /// untouched and still fully usable: an absorbed subgraph is
159    /// self-contained (its shifted handles cannot reach below the append
160    /// line), and no identity is committed until every check has passed. What
161    /// a failed absorb can leave behind is unreachable appended entries,
162    /// which cost memory and mean nothing.
163    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    /// Append parts onto this model's arenas, shifting every handle past what
203    /// is already here. The shared engine of [`Model::from_parts`] (offsets
204    /// all zero) and [`Model::absorb`].
205    ///
206    /// Returns `(node, datum, entity)` offsets: where the parts landed.
207    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        // Every id an entry names must already have been issued, which is
219        // what makes the derivation graph acyclic by construction. Checked
220        // against the parts' own issue order, before any shift.
221        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        // Every handle in `parts` was rebuilt by a reader that had no arenas
280        // to bind them to, so they name no arena at all and resolve nowhere.
281        // Bind the appended subrange now that the arenas exist; what was here
282        // before is already bound.
283        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    /// Refuse parts whose handles are already bound to an arena or carry a
305    /// non-zero generation: the state no reader produces, and the state an
306    /// offset shift would silently mangle.
307    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    /// Bind an unscoped shape to this model.
348    ///
349    /// A handle rebuilt by a reader names no arena, so it resolves nowhere
350    /// until it is told which document it belongs to. This is how a reader says
351    /// so, and it verifies the answer, so a file naming a node that is not
352    /// there is an error rather than a shape that fails mysteriously later.
353    ///
354    /// It will not re-home a shape that already belongs to *another* model.
355    /// That is exactly the mistake scoping exists to catch, and quietly
356    /// relabelling it would hand back a shape that resolves and answers about
357    /// the wrong entity.
358    ///
359    /// # Errors
360    ///
361    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the shape
362    /// already belongs to a different model;
363    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if it does not resolve
364    /// here once bound.
365    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    /// Bind a bare location to this model's datum store.
381    ///
382    /// The persistence path's sibling of [`Model::bind`]: a location read
383    /// from a file names datum handles that are unscoped until the store
384    /// that holds them exists. Binding checks them too: a chain naming a
385    /// datum not in this model is an error here rather than wherever it is
386    /// first resolved.
387    ///
388    /// # Errors
389    ///
390    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the chain
391    /// names a datum not in this model.
392    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    /// Bind every handle in freshly restored nodes to the arena that holds it.
403    ///
404    /// `from` bounds the pass to nodes at that index and past it: restoration
405    /// binds everything from zero, absorption only what it appended.
406    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    /// Verify that restored nodes' handles all resolve and their children are
434    /// of the kinds their parents admit.
435    ///
436    /// `from` bounds the pass the way [`Model::bind_handles`]' does. Children
437    /// still resolve through the full arena, so nothing is under-checked; an
438    /// absorbed subgraph is self-contained and can only point at itself.
439    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            // A compound may hold anything; everything else admits exactly one
461            // kind of child, which is what makes traversal's assumptions safe.
462            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    /// Verify that a node's geometry handles resolve.
497    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    /// Verify that every datum a placement names is interned.
532    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    /// Begin a new operation, and return its identifier.
542    ///
543    /// Every node created from here on is attributed to it until the next call.
544    /// The counter is deterministic (the third operation in a rebuild is
545    /// `OpId(3)` every time), which is what lets provenance survive a parameter
546    /// change (`docs/DATA_MODEL.md` §8).
547    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    /// The operation nodes are currently attributed to.
553    #[must_use]
554    pub const fn current_operation(&self) -> OpId {
555        self.current_op
556    }
557
558    /// The stable identity of a shape's node.
559    ///
560    /// Distinct from its arena handle: the handle says where the data is and
561    /// dies when the shape is rebuilt, while this says what the entity *is* and
562    /// survives.
563    #[must_use]
564    pub fn identity_of(&self, shape: &Shape) -> Option<EntityId> {
565        self.identity.get(&shape.node()).copied()
566    }
567
568    /// Where a shape's node came from.
569    #[must_use]
570    pub fn provenance_of(&self, shape: &Shape) -> Option<&Provenance> {
571        self.provenance.get(self.identity_of(shape)?)
572    }
573
574    /// The provenance table.
575    #[must_use]
576    pub const fn provenance(&self) -> &ProvenanceTable {
577        &self.provenance
578    }
579
580    /// Trace a shape back to the entities it ultimately came from.
581    ///
582    /// How a reference into a rebuilt model is resolved: find what the user
583    /// originally picked, then find what that became.
584    #[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    /// The shape carrying a given identity, if this document has one.
592    ///
593    /// The inverse of [`Model::identity_of`], and the answer to "I kept a
594    /// reference and the document has been saved and reloaded since". A raw
595    /// [`Shape`] cannot survive that: the reloaded document is a new set of
596    /// arenas and [`Model::bind`] refuses a handle from another one, on
597    /// purpose. An [`EntityId`] can, because it names *what the entity is*
598    /// rather than where it sits (`docs/DATA_MODEL.md` §8), and that is the
599    /// whole reason it exists.
600    ///
601    /// Returns the shape in its default placement and orientation. A caller
602    /// that wants a particular occurrence explores from here.
603    #[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    /// Record that a node was derived from other entities.
612    ///
613    /// Overwrites the `Primitive` attribution a builder assigns by default.
614    /// An operation that splits or reshapes existing topology calls this, and
615    /// what it records is what a later rebuild will match against.
616    ///
617    /// # Errors
618    ///
619    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape does not
620    /// resolve in this model.
621    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    /// Record a node's identity as it is created.
637    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    /// The placement datums.
643    #[must_use]
644    pub const fn datums(&self) -> &DatumStore {
645        &self.datums
646    }
647
648    /// The geometry.
649    #[must_use]
650    pub const fn geometry(&self) -> &GeometryStore {
651        &self.geometry
652    }
653
654    /// Mutable access to the geometry, for adding curves and surfaces.
655    #[must_use]
656    pub const fn geometry_mut(&mut self) -> &mut GeometryStore {
657        &mut self.geometry
658    }
659
660    /// Intern a transform for use in placements.
661    pub fn add_datum(&mut self, transform: Transform) -> DatumId {
662        self.datums.insert(transform)
663    }
664
665    /// The node behind a shape's handle.
666    #[must_use]
667    pub fn node(&self, shape: &Shape) -> Option<&TShape> {
668        self.nodes.get(shape.node())
669    }
670
671    /// The node behind a handle.
672    #[must_use]
673    pub fn node_by_id(&self, id: TShapeId) -> Option<&TShape> {
674        self.nodes.get(id)
675    }
676
677    /// Mutable access to the node behind a shape's handle.
678    ///
679    /// For attaching geometry to an entity that already exists: a pcurve
680    /// joining an edge to a face it has just come to bound. Structural change
681    /// still goes through the builders; this reaches the node's *data*, which
682    /// no invariant here constrains on its own.
683    #[must_use]
684    pub fn node_mut(&mut self, shape: &Shape) -> Option<&mut TShape> {
685        self.nodes.get_mut(shape.node())
686    }
687
688    /// What kind of shape this is.
689    ///
690    /// # Errors
691    ///
692    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the handle does not
693    /// resolve in this model.
694    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    /// The tolerance a shape carries, if it carries one.
702    ///
703    /// # Errors
704    ///
705    /// As [`Model::kind_of`].
706    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    /// Number of topology nodes.
714    #[must_use]
715    pub fn node_count(&self) -> usize {
716        self.nodes.len()
717    }
718
719    /// Whether the model holds no topology.
720    #[must_use]
721    pub fn is_empty(&self) -> bool {
722        self.nodes.is_empty()
723    }
724
725    /// Every topology node, with its handle, in arena order.
726    ///
727    /// For writing a document out. Traversal from a root shape reaches only
728    /// what that root bounds; a document is everything in it.
729    pub fn nodes(&self) -> impl Iterator<Item = (TShapeId, &TShape)> {
730        self.nodes.iter()
731    }
732
733    /// Every node that has been given an identity, with it.
734    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    /// Add a vertex.
741    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    /// Add a vertex at `point` with the minimum tolerance.
749    pub fn add_point(&mut self, point: Point) -> Shape {
750        self.add_vertex(VertexData::new(point))
751    }
752
753    /// Add an edge bounded by the given vertices.
754    ///
755    /// The vertices are the edge's ends, in order. A closed edge (a full
756    /// circle) names the same vertex twice rather than once, so that walking
757    /// its boundary yields a start and an end as every other edge does.
758    ///
759    /// # Errors
760    ///
761    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a bound is
762    /// not a vertex, or if there are more than two;
763    /// [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) if a vertex's
764    /// tolerance is tighter than the edge's, breaking the containment rule.
765    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        // A vertex caps an edge, so it must be at least as uncertain as the
775        // edge is; otherwise the cap does not reliably sit on what it caps.
776        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    /// Add a wire from a sequence of edges.
789    ///
790    /// # Errors
791    ///
792    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
793    /// not an edge, or the wire is empty.
794    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    /// Add a face bounded by the given wires.
806    ///
807    /// A face with no wires covers its surface's whole domain, and is recorded
808    /// as naturally restricted.
809    ///
810    /// # Errors
811    ///
812    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a bound is
813    /// not a wire; [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) if the
814    /// containment rule is broken.
815    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        // An edge borders a face, so it must be at least as uncertain as the
821        // face. Walk the wires' edges and widen them where they are not.
822        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    /// Add a shell from a set of faces.
839    ///
840    /// # Errors
841    ///
842    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
843    /// not a face, or the shell is empty.
844    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    /// Add a solid bounded by the given shells.
856    ///
857    /// # Errors
858    ///
859    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
860    /// not a shell, or the solid is empty.
861    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    /// Add a compsolid from solids sharing faces.
873    ///
874    /// # Errors
875    ///
876    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
877    /// not a solid, or it is empty.
878    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    /// Add a compound of arbitrary shapes.
890    ///
891    /// The one container with no type constraint; that is what a compound is
892    /// for. It may be empty, since an empty result is a legitimate answer from
893    /// a boolean and needs somewhere to live.
894    ///
895    /// # Errors
896    ///
897    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a child does not
898    /// resolve in this model.
899    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    /// The direct children of a shape, with this shape's placement and
912    /// orientation composed onto each.
913    ///
914    /// The single most important method on the model, and the reason traversal
915    /// is correct by default rather than by discipline: a child's placement in
916    /// the world is its parent's composed with its own, and its orientation is
917    /// its parent's composed with its own. Returning raw children would leave
918    /// every caller to remember both, and the failure is silent: face normals
919    /// that flip inconsistently, sub-shapes drawn at the origin.
920    ///
921    /// # Errors
922    ///
923    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape does not
924    /// resolve in this model.
925    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    /// A shape's children in *traversal* order.
937    ///
938    /// The same shapes as [`Model::children_of`], but with the list reversed
939    /// when the parent is reversed. Order carries meaning for a wire (its
940    /// edges run head to tail), and reversing a wire has to reverse the walk as
941    /// well as each edge, or consecutive edges stop sharing a vertex and the
942    /// boundary comes apart. For a shell or a solid the order means nothing and
943    /// the reversal is invisible.
944    ///
945    /// [`Model::children_of`] stays the raw accessor: it returns what is
946    /// stored, which is what a rebuild or a comparison wants.
947    ///
948    /// # Errors
949    ///
950    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape does not
951    /// resolve in this model.
952    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    /// Widen a shape's tolerance, and every sub-shape's with it.
961    ///
962    /// The cascade is the point. The containment rule is transitive: a face's
963    /// edges must be no tighter than the face, *and* those edges' vertices no
964    /// tighter than the edges. Widening only one level leaves the rule broken
965    /// two levels down, where nothing will notice until a containment test
966    /// quietly answers about geometry that does not meet.
967    ///
968    /// Tolerances only ever grow, so this is the sanctioned repair: raise what
969    /// bounds, never lower what is bounded.
970    ///
971    /// # Errors
972    ///
973    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape, or
974    /// anything below it, does not resolve in this model.
975    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    /// Check that every child resolves and is of the expected type.
998    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    /// Verify the containment rule across a whole shape tree.
1015    ///
1016    /// `docs/DATA_MODEL.md` §5. Walks parent to child and checks that whatever
1017    /// bounds is no tighter than what it bounds.
1018    ///
1019    /// # Errors
1020    ///
1021    /// [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) at the first
1022    /// violation, naming the two shape types involved.
1023    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            // A boundary is *contained by* what it bounds, so the child (the
1030            // boundary) must be the looser of the two.
1031            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    /// Whether two shapes coincide in position, comparing composed transforms.
1049    ///
1050    /// # Errors
1051    ///
1052    /// As [`Location::composed`](crate::Location::composed).
1053    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    /// A shape placed by an additional transform.
1058    ///
1059    /// Interns the transform and composes it onto the shape's placement, so the
1060    /// underlying node (and all its geometry) is shared rather than copied.
1061    /// Placing ten thousand instances of a part costs ten thousand short chains
1062    /// and one copy of the geometry.
1063    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/// What an absorb produced: the transplanted roots, bound to the model that
1070/// absorbed them, and where every absorbed identity ended up.
1071#[derive(Debug)]
1072pub struct Absorbed {
1073    /// The roots handed in, shifted and bound to the absorbing model.
1074    pub shapes: Vec<Shape>,
1075    /// Source-document identity → identity in the absorbing model, for every
1076    /// entity the parts carried. A caller holding references recorded against
1077    /// the source document resolves them through this.
1078    pub entities: HashMap<EntityId, EntityId>,
1079}
1080
1081/// A model's contents, laid out the way a file holds them.
1082///
1083/// Handed to [`Model::from_parts`]. Every list is in arena order, and a node's
1084/// children name other nodes by their position in `nodes`, so the order is
1085/// load-bearing rather than incidental, and a reader has to preserve it.
1086#[derive(Debug, Default)]
1087pub struct ModelParts {
1088    /// The topology nodes.
1089    pub nodes: Vec<TShape>,
1090    /// The placement datums.
1091    pub datums: Vec<crate::location::Datum>,
1092    /// The geometry, already assembled.
1093    pub geometry: GeometryStore,
1094    /// Every entity's provenance, in the order identities were issued: the
1095    /// first entry is `EntityId(1)`.
1096    pub provenance: Vec<Provenance>,
1097    /// Which identity each node carries.
1098    pub identity: Vec<(TShapeId, EntityId)>,
1099    /// The operation the document was left in.
1100    pub current_op: OpId,
1101    /// The unit scale the document was authored at.
1102    pub tolerances: Tolerances,
1103}
1104
1105/// Which sub-shapes a traversal should yield.
1106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1107pub enum Filter {
1108    /// Every shape of one type.
1109    OfType(ShapeType),
1110    /// Every shape, at every level.
1111    All,
1112}
1113
1114/// Walk a shape's tree, composing placement and orientation on descent.
1115///
1116/// Yields each matching sub-shape with its *effective* placement and
1117/// orientation: the composition of everything from the root down. That is the
1118/// only form in which a sub-shape means anything outside the tree it came from.
1119///
1120/// Sub-shapes reached by more than one route (an edge shared by two faces)
1121/// are yielded once per route, since each occurrence has its own orientation
1122/// and that is usually the point. Deduplicate with
1123/// [`SameKey`](crate::SameKey) when it is not.
1124///
1125/// # Errors
1126///
1127/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if any handle fails to
1128/// resolve in `model`.
1129pub 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        // The node is fetched once and answers both questions. `kind_of` would
1134        // repeat the arena lookup that `children_of` is about to do anyway,
1135        // and this walk is the kernel's most travelled road.
1136        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        // `children_of`'s composition, inline: the parent's placement and
1146        // sense onto each child. Done here so the walk does not allocate a
1147        // `Vec` per node only to drain it.
1148        for child in children.iter().rev() {
1149            stack.push(child.moved(shape.location()).composed(shape.orientation()));
1150        }
1151        if matches {
1152            // `shape` is owned and finished with; cloning it to keep it would
1153            // copy a location chain for nothing.
1154            out.push(shape);
1155        }
1156    }
1157    Ok(out)
1158}
1159
1160/// Walk a shape's tree, yielding every sub-shape of `want` exactly once.
1161///
1162/// Deduplicated by [`Shape::is_same`] (node and placement, ignoring
1163/// orientation), which is what "the distinct edges of this solid" means.
1164///
1165/// # Errors
1166///
1167/// As [`explore`].
1168pub 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
1184/// Every shape in `root` that has `target` among its sub-shapes.
1185///
1186/// The inverse of traversal: "which faces meet at this edge?" is how a boolean
1187/// decides what a split affects, and how a fillet finds what it is blending.
1188///
1189/// # Errors
1190///
1191/// As [`explore`].
1192pub 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    /// A single square face: four vertices, four edges, one wire, one face.
1220    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    /// Parts describing a two-vertex edge on a line, placed by one datum,
1242    /// with a primitive-and-derived provenance chain: every kind of handle a
1243    /// shift must move, in miniature, exactly as a reader would rebuild them.
1244    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        // The absorbed root resolves here and answers as an edge.
1316        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        // Its curve and datum handles were shifted onto this model's arenas.
1326        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        // What was here before is untouched.
1341        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        // The remap table is exactly old-plus-offset.
1358        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        // The derived entry still points at its shifted source, and walking
1364        // back lands on the shifted primitive.
1365        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        // Zero offsets: the handles come out where the file put them, and the
1383        // identities are the file's own.
1384        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        // Reversing each edge without reversing the order breaks the chain:
1469        // edge 1 would end where it started while edge 2 still starts where
1470        // it did, so consecutive edges stop meeting and a face built
1471        // on the wire comes apart along its boundary.
1472        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        // The raw accessor keeps the stored order, which is what a rebuild
1492        // wants and what a traversal must not use.
1493        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        // Four edges of two vertices each, but only four distinct vertices:
1522        // consecutive edges share them.
1523        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        // The invariant traversal exists to guarantee. A vertex reported
1534        // without its parent's placement is a vertex at the wrong point, and
1535        // nothing about the value says so.
1536        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        // The point of composing on descent: the reversal lives entirely in the
1576        // handle, so a shared sub-tree is not disturbed for other users of it.
1577        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        // A compound is the exception, and deliberately so.
1637        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        // An empty result is a legitimate answer from a boolean and needs
1648        // somewhere to live.
1649        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        // A coarse edge must not be capped by a finer vertex. Rather than
1678        // refusing, the builder widens the vertex; tolerances only ever grow,
1679        // so the repair goes upward.
1680        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        // And the cascade reached the vertices under those edges. Stopping one
1718        // level down would leave the rule broken where nothing looks.
1719        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        // The builder maintains the rule, so a violation has to be assembled
1730        // around it, which is exactly what happens when topology arrives from
1731        // a file. The check has to stand on its own, or imported geometry sails
1732        // past it.
1733        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        // And the sanctioned repair fixes it, cascading to the vertex.
1754        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        // Two faces meeting at an edge. Each occurrence carries its own
1786        // orientation, which is usually the point; asking for distinct edges is
1787        // a separate question.
1788        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        // Past the end of `model`, so it genuinely fails to resolve.
1854        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        // Ten thousand fasteners cost ten thousand short chains and one copy of
1867        // the geometry. That is the whole reason placement is a chain.
1868        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        // And they are all in different places.
1884        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        // The same displacement reached twice is the same position, even
1916        // through two different datums.
1917        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        // Guards the test helper itself: a face built on a degenerate plane
1925        // would make every other assertion here meaningless.
1926        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}