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 ogeom_core::{
18    Arena, EntityId, Key, OgeomResult, OpId, Provenance, ProvenanceTable, Role, Tolerance,
19    Tolerances, ogeom_bail,
20};
21use ogeom_math::{Aabb, Point, Transform, TransformKind};
22
23use smallvec::SmallVec;
24
25use crate::entity::{
26    CurveId, EdgeData, EdgeRepr, FaceData, NodeData, PCurveId, SurfaceId, TriangulationId,
27    VertexData,
28};
29use crate::kept::{FaceBoxes, KeptReads, NodeSet};
30use crate::location::{DatumId, DatumStore, Location};
31use crate::shape::{Orientation, Shape, ShapeType, TShape, TShapeId};
32
33pub use crate::entity::GeometryStore;
34
35/// A document: topology, placements, geometry, and where every entity came
36/// from.
37#[derive(Debug, Clone, Default)]
38pub struct Model {
39    nodes: Nodes,
40    datums: DatumStore,
41    geometry: GeometryStore,
42    provenance: ProvenanceTable,
43    current_op: OpId,
44    tolerances: Tolerances,
45    face_boxes: FaceBoxes,
46    /// Whether any node is marked held ([`Model::note_held`]).
47    any_held: bool,
48    /// While a journal is open, each node [`Model::widen`] grew or
49    /// [`Model::node_mut`] handed out, and its tolerance before: what
50    /// [`Model::undo_widened`] puts back.
51    widened: Option<Vec<(TShapeId, Tolerance)>>,
52}
53
54/// A topology node and what the model keeps beside it.
55#[derive(Debug, Clone)]
56struct Node {
57    shape: TShape,
58    /// The identity the node carries.
59    identity: Option<EntityId>,
60    /// The node's slot among the kept face boxes, if it is a face.
61    face_box: Option<u32>,
62    /// The node indices of the edges, wires and faces that hold it: the way
63    /// up from a vertex, an edge or a wire to the faces it bounds.
64    held_by: SmallVec<[u32; 2]>,
65    /// Whether an operation passed the node through into its result from
66    /// the shapes it was given, which those shapes still hold. What lies
67    /// below a held node is held with it.
68    held: bool,
69}
70
71/// The topology nodes, looked up by the handles shapes carry, and what
72/// readers keep of the shapes they make up.
73#[derive(Debug, Clone, Default)]
74struct Nodes {
75    arena: Arena<Node>,
76    /// Forgotten for every shape holding a node handed out by
77    /// [`Nodes::get_mut`]: once a node is bound on arrival, its data and
78    /// children change only there.
79    reads: KeptReads,
80}
81
82/// A node's key in the arena, from its handle.
83#[inline]
84const fn arena_key(id: TShapeId) -> Key<Node> {
85    Key::from_parts(id.index(), id.generation()).with_scope(id.scope())
86}
87
88/// A node's handle, from its key in the arena.
89#[inline]
90const fn handle(key: Key<Node>) -> TShapeId {
91    Key::from_parts(key.index(), key.generation()).with_scope(key.scope())
92}
93
94impl Nodes {
95    #[inline]
96    fn entry(&self, id: TShapeId) -> Option<&Node> {
97        self.arena.get(arena_key(id))
98    }
99
100    /// The node with what the model keeps beside it, for that bookkeeping:
101    /// its identity, its marks and its links up. Its data and children are
102    /// changed through [`Nodes::get_mut`].
103    #[inline]
104    fn entry_mut(&mut self, id: TShapeId) -> Option<&mut Node> {
105        self.arena.get_mut(arena_key(id))
106    }
107
108    #[inline]
109    fn get(&self, id: TShapeId) -> Option<&TShape> {
110        self.entry(id).map(|node| &node.shape)
111    }
112
113    /// The node, for changing its data or its children: what is kept of
114    /// every shape holding it is forgotten.
115    #[inline]
116    fn get_mut(&mut self, id: TShapeId) -> Option<&mut TShape> {
117        self.reads.forget(id.index());
118        self.entry_mut(id).map(|node| &mut node.shape)
119    }
120
121    fn insert(&mut self, node: Node) -> TShapeId {
122        handle(self.arena.insert(node))
123    }
124
125    fn iter(&self) -> impl Iterator<Item = (TShapeId, &Node)> {
126        self.arena.iter().map(|(key, node)| (handle(key), node))
127    }
128
129    fn iter_mut(&mut self) -> impl Iterator<Item = (TShapeId, &mut Node)> {
130        self.arena.iter_mut().map(|(key, node)| (handle(key), node))
131    }
132
133    /// The live handle at node index `index`.
134    fn at(&self, index: u32) -> Option<TShapeId> {
135        self.arena.key_at(index).map(handle)
136    }
137
138    const fn scope(&self) -> u32 {
139        self.arena.scope()
140    }
141
142    const fn issued(&self, id: TShapeId) -> bool {
143        id.scope() == self.arena.scope()
144    }
145
146    const fn len(&self) -> usize {
147        self.arena.len()
148    }
149
150    const fn is_empty(&self) -> bool {
151        self.arena.is_empty()
152    }
153}
154
155/// What the roots of [`Model::retain_reachable`] reach.
156#[derive(Default)]
157struct Reach {
158    /// Node indices.
159    nodes: ogeom_core::FastSet<u32>,
160    curves: ogeom_core::FastSet<CurveId>,
161    pcurves: ogeom_core::FastSet<PCurveId>,
162    surfaces: ogeom_core::FastSet<SurfaceId>,
163    meshes: ogeom_core::FastSet<TriangulationId>,
164    entities: ogeom_core::FastSet<EntityId>,
165}
166
167impl Model {
168    /// An empty model, in millimetres.
169    #[must_use]
170    pub fn new() -> Self {
171        Self::with_tolerances(Tolerances::millimetres())
172    }
173
174    /// An empty model at a given unit scale.
175    ///
176    /// A document has a scale, and it is the document's rather than each
177    /// call's: a model authored in metres does not become a model in
178    /// millimetres because one caller passed the default. Algorithms still take
179    /// a [`Tolerances`] argument (that is deliberate, since a caller may want
180    /// to work coarser or finer than the document's own setting for one
181    /// operation), but the document says what it was built at, so a
182    /// mismatch is visible rather than assumed away.
183    #[must_use]
184    pub fn with_tolerances(tolerances: Tolerances) -> Self {
185        Self {
186            nodes: Nodes::default(),
187            datums: DatumStore::new(),
188            geometry: GeometryStore::new(),
189            provenance: ProvenanceTable::new(),
190            current_op: OpId(0),
191            tolerances,
192            face_boxes: FaceBoxes::default(),
193            any_held: false,
194            widened: None,
195        }
196    }
197
198    /// The tolerances this document was built at.
199    #[must_use]
200    pub const fn tolerances(&self) -> Tolerances {
201        self.tolerances
202    }
203
204    /// Assemble a model from parts read back from a file.
205    ///
206    /// The one way into a [`Model`] that does not go through its builders, and
207    /// it exists for one reason: a builder *mints* an identity for every node
208    /// it makes (`docs/DATA_MODEL.md` §8). A document rebuilt through the
209    /// builders is therefore a different document from the one that was
210    /// written (every [`EntityId`] renumbered, every provenance record
211    /// replaced by a fresh `Primitive` one), and every reference into it,
212    /// which is the thing provenance exists to keep alive, is dead. Reading a
213    /// file has to reproduce the document it describes, identities and all.
214    ///
215    /// This is not a hole in "the builder is the sole mutation path". Nothing
216    /// here mutates an existing model; it assembles a new one, and it checks
217    /// the structural invariants the builders check before handing it back,
218    /// so a corrupt file is an error, not a model that answers wrongly.
219    ///
220    /// # Errors
221    ///
222    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a node names a
223    /// child, a datum, a piece of geometry or an identity that is not there;
224    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a node's
225    /// data does not match its kind, or a child is of the wrong kind for its
226    /// parent.
227    pub fn from_parts(parts: ModelParts) -> OgeomResult<Self> {
228        let mut model = Self::with_tolerances(parts.tolerances);
229        model.current_op = parts.current_op;
230        // Absorbing into an empty model is exactly restoration: every offset
231        // is zero, so each handle keeps the index and generation the file
232        // recorded, which is what makes a document's handles survive a round
233        // trip.
234        model.absorb_core(parts)?;
235        Ok(model)
236    }
237
238    /// Absorb another document's parts into this model.
239    ///
240    /// [`Model::from_parts`] lands a document in a *fresh* model; this lands
241    /// one in a model that already has things in it: the operation behind
242    /// bringing a serialized tool body into a live document so a boolean can
243    /// use it. Everything the parts carry is appended: nodes, datums,
244    /// geometry, provenance and identities all keep their relative structure,
245    /// shifted past what the model already holds. The result behaves exactly
246    /// as if it had been built here, because after the shift it is
247    /// indistinguishable from having been.
248    ///
249    /// `roots` are the shapes the source document named, in the unbound state
250    /// a reader leaves them; they come back bound to this model. The returned
251    /// [`Absorbed::entities`] table says where every source identity landed,
252    /// which is what a caller holding references against the source document
253    /// resolves them through.
254    ///
255    /// Three deliberate refusals, each an error rather than a guess:
256    ///
257    /// - **Units.** A document authored at another scale is refused, not
258    ///   rescaled; rescaling is a real feature with real decisions in it,
259    ///   and silently absorbing metres into millimetres is a wrong model.
260    /// - **Bound handles.** Parts whose handles already name an arena did not
261    ///   come from a reader; absorbing them would alias whatever those
262    ///   handles meant elsewhere. Serialization is the one road in.
263    /// - **A model that reuses slots.** Absorbing appends by offset, which
264    ///   is only sound while each of the target's arenas hands out fresh
265    ///   slots in order. [`Model::retain_reachable`] leaves slots empty and
266    ///   never hands them out again, so a retained model absorbs; an arena
267    ///   waiting to refill a freed slot is refused, and the check is what
268    ///   keeps that refill from becoming aliasing.
269    ///
270    /// The current operation is left alone: absorb mints no identities, it
271    /// transplants a table, and the absorbed provenance keeps its source
272    /// [`OpId`]s verbatim, meaningful in the source document's rebuild, kept
273    /// because renumbering them would orphan the source's own references.
274    ///
275    /// # Errors
276    ///
277    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) for
278    /// the three refusals above;
279    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the parts
280    /// do not describe themselves: a handle that does not resolve, a
281    /// derivation from an identity no entry issued, a root naming a node that
282    /// is not there.
283    ///
284    /// On an error past the up-front gates, the model's prior contents are
285    /// untouched and still fully usable: an absorbed subgraph is
286    /// self-contained (its shifted handles cannot reach below the append
287    /// line), and no identity is committed until every check has passed. What
288    /// a failed absorb can leave behind is unreachable appended entries,
289    /// which cost memory and mean nothing.
290    pub fn absorb(&mut self, parts: ModelParts, roots: &[Shape]) -> OgeomResult<Absorbed> {
291        #[allow(clippy::float_cmp, reason = "scales are copied, never computed")]
292        if parts.tolerances.scale() != self.tolerances.scale() {
293            ogeom_bail!(
294                Construction,
295                "these parts were authored at {} mm per unit and this model at \
296                 {}; absorbing across scales needs a rescale, which is its own \
297                 operation",
298                parts.tolerances.scale(),
299                self.tolerances.scale()
300            );
301        }
302        if self.nodes.arena.next_index().is_none()
303            || !self.datums.is_dense()
304            || !self.geometry.appends()
305        {
306            ogeom_bail!(
307                Construction,
308                "absorb appends by offset, and this model's arenas are set to \
309                 refill freed slots, which nothing in this crate does"
310            );
311        }
312        Self::check_parts_unbound(&parts, roots)?;
313
314        let absorbed_entities = parts.provenance.len() as u64;
315        let (node_offset, datum_offset, entity_offset) = self.absorb_core(parts)?;
316
317        let shapes = roots
318            .iter()
319            .map(|root| self.bind(&root.shifted(node_offset, datum_offset)))
320            .collect::<OgeomResult<Vec<Shape>>>()?;
321        let entities = (1..=absorbed_entities)
322            .filter_map(|raw| {
323                Some((
324                    EntityId::from_raw(raw)?,
325                    EntityId::from_raw(raw + entity_offset)?,
326                ))
327            })
328            .collect();
329        Ok(Absorbed { shapes, entities })
330    }
331
332    /// Append parts onto this model's arenas, shifting every handle past what
333    /// is already here. The shared engine of [`Model::from_parts`] (offsets
334    /// all zero) and [`Model::absorb`].
335    ///
336    /// Returns `(node, datum, entity)` offsets: where the parts landed.
337    fn absorb_core(&mut self, parts: ModelParts) -> OgeomResult<(u32, u32, u64)> {
338        let ModelParts {
339            mut nodes,
340            datums,
341            geometry,
342            provenance,
343            identity,
344            current_op: _,
345            tolerances: _,
346        } = parts;
347
348        // Every id an entry names must already have been issued, which is
349        // what makes the derivation graph acyclic by construction. Checked
350        // against the parts' own issue order, before any shift.
351        for (issued, entry) in provenance.iter().enumerate() {
352            for source in entry.inputs() {
353                if source.get() > issued as u64 {
354                    ogeom_bail!(
355                        Dangling,
356                        "an entity is derived from identity {}, which no entry \
357                         before it issued",
358                        source.get()
359                    );
360                }
361            }
362        }
363
364        let node_offset = crate::entity::arena_len(&self.nodes.arena);
365        let datum_offset = u32::try_from(self.datums.len()).unwrap_or(u32::MAX);
366        let entity_offset = self.provenance.len() as u64;
367        let geometry_offsets = self.geometry.append(geometry);
368
369        for node in &mut nodes {
370            for child in node.children_mut() {
371                *child = child.shifted(node_offset, datum_offset);
372            }
373            match node.data_mut() {
374                NodeData::Edge(edge) => {
375                    for repr in &mut edge.representations {
376                        repr.shift(&geometry_offsets, datum_offset);
377                    }
378                }
379                NodeData::Face(face) => {
380                    face.surface =
381                        crate::entity::shifted_key(face.surface, geometry_offsets.surfaces);
382                    face.triangulation = face.triangulation.map(|mesh| {
383                        crate::entity::shifted_key(mesh, geometry_offsets.triangulations)
384                    });
385                    face.location = face.location.with_datum_offset(datum_offset);
386                }
387                NodeData::Vertex(_) | NodeData::Container => {}
388            }
389        }
390
391        for datum in datums {
392            self.datums.insert(datum);
393        }
394        for mut entry in provenance {
395            if let Provenance::Derived { from, .. } = &mut entry {
396                for source in from.iter_mut() {
397                    let Some(shifted) = EntityId::from_raw(source.get() + entity_offset) else {
398                        ogeom_bail!(Construction, "an entity id overflowed in the shift");
399                    };
400                    *source = shifted;
401                }
402            }
403            self.provenance.record(entry);
404        }
405        self.sync_face_boxes();
406        let appended: Vec<TShapeId> = nodes
407            .into_iter()
408            .map(|node| self.insert_unlinked(node))
409            .collect();
410
411        // Every handle in `parts` was rebuilt by a reader that had no arenas
412        // to bind them to, so they name no arena at all and resolve nowhere.
413        // Bind the appended subrange now that the arenas exist; what was here
414        // before is already bound. A child may come after its parent in the
415        // parts, so the links up from children are made once all are bound.
416        self.bind_handles(node_offset);
417        for id in appended {
418            self.link_children(id, true);
419        }
420        let identity: Vec<(TShapeId, EntityId)> = identity
421            .into_iter()
422            .map(|(node, entity)| {
423                let Some(shifted) = EntityId::from_raw(entity.get() + entity_offset) else {
424                    ogeom_bail!(Construction, "an entity id overflowed in the shift");
425                };
426                Ok((
427                    crate::entity::shifted_key(node, node_offset).with_scope(self.nodes.scope()),
428                    shifted,
429                ))
430            })
431            .collect::<OgeomResult<_>>()?;
432
433        self.check_restored(&identity, node_offset)?;
434        for (node, entity) in identity {
435            if let Some(node) = self.nodes.entry_mut(node) {
436                node.identity = Some(entity);
437            }
438        }
439        Ok((node_offset, datum_offset, entity_offset))
440    }
441
442    /// Refuse parts whose handles are already bound to an arena or carry a
443    /// non-zero generation: the state no reader produces, and the state an
444    /// offset shift would silently mangle.
445    fn check_parts_unbound(parts: &ModelParts, roots: &[Shape]) -> OgeomResult<()> {
446        use crate::entity::key_is_unbound;
447
448        let local_location = |location: &Location| {
449            location
450                .chain()
451                .iter()
452                .all(|&(datum, _)| key_is_unbound(datum))
453        };
454        let local_shape =
455            |shape: &Shape| key_is_unbound(shape.node()) && local_location(shape.location());
456
457        let mut sound = parts.identity.iter().all(|&(node, _)| key_is_unbound(node))
458            && roots.iter().all(local_shape);
459        for node in &parts.nodes {
460            sound = sound && node.children().iter().all(local_shape);
461            match node.data() {
462                NodeData::Edge(edge) => {
463                    sound = sound && edge.representations.iter().all(EdgeRepr::is_unbound);
464                }
465                NodeData::Face(face) => {
466                    sound = sound
467                        && key_is_unbound(face.surface)
468                        && face.triangulation.is_none_or(key_is_unbound)
469                        && local_location(&face.location);
470                }
471                NodeData::Vertex(_) | NodeData::Container => {}
472            }
473        }
474        if !sound {
475            ogeom_bail!(
476                Construction,
477                "these parts carry handles already bound to an arena, or at a \
478                 recycled generation; absorb takes parts exactly as a reader \
479                 rebuilt them, and serialization is the one road in"
480            );
481        }
482        Ok(())
483    }
484
485    /// Bind an unscoped shape to this model.
486    ///
487    /// A handle rebuilt by a reader names no arena, so it resolves nowhere
488    /// until it is told which document it belongs to. This is how a reader says
489    /// so, and it verifies the answer, so a file naming a node that is not
490    /// there is an error rather than a shape that fails mysteriously later.
491    ///
492    /// It will not re-home a shape that already belongs to *another* model.
493    /// That is exactly the mistake scoping exists to catch, and quietly
494    /// relabelling it would hand back a shape that resolves and answers about
495    /// the wrong entity.
496    ///
497    /// # Errors
498    ///
499    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the shape
500    /// already belongs to a different model;
501    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if it does not resolve
502    /// here once bound.
503    pub fn bind(&self, shape: &Shape) -> OgeomResult<Shape> {
504        if shape.node().scope() != ogeom_core::UNSCOPED && !self.nodes.issued(shape.node()) {
505            ogeom_bail!(
506                Construction,
507                "this shape belongs to another model; binding it here would \
508                 make it resolve and answer about a different entity"
509            );
510        }
511        let bound = shape.rebound(self.nodes.scope(), self.datums.scope());
512        if self.nodes.get(bound.node()).is_none() {
513            ogeom_bail!(Dangling, "shape refers to a node not in this model");
514        }
515        Ok(bound)
516    }
517
518    /// Bind a bare location to this model's datum store.
519    ///
520    /// The persistence path's sibling of [`Model::bind`]: a location read
521    /// from a file names datum handles that are unscoped until the store
522    /// that holds them exists. Binding checks them too: a chain naming a
523    /// datum not in this model is an error here rather than wherever it is
524    /// first resolved.
525    ///
526    /// # Errors
527    ///
528    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the chain
529    /// names a datum not in this model.
530    pub fn bind_location(&self, location: &Location) -> OgeomResult<Location> {
531        let bound = location.with_datum_scope(self.datums.scope());
532        for &(datum, _) in bound.chain() {
533            if self.datums.get(datum).is_none() {
534                ogeom_bail!(Dangling, "location refers to a datum not in this model");
535            }
536        }
537        Ok(bound)
538    }
539
540    /// Bind every handle in freshly restored nodes to the arena that holds it.
541    ///
542    /// `from` bounds the pass to nodes at that index and past it: restoration
543    /// binds everything from zero, absorption only what it appended.
544    fn bind_handles(&mut self, from: u32) {
545        let nodes = self.nodes.scope();
546        let datums = self.datums.scope();
547        let geometry = self.geometry.scopes();
548
549        for (_, node) in self.nodes.iter_mut().filter(|(id, _)| id.index() >= from) {
550            let node = &mut node.shape;
551            for child in node.children_mut() {
552                *child = child.rebound(nodes, datums);
553            }
554            match node.data_mut() {
555                NodeData::Edge(edge) => {
556                    for repr in &mut edge.representations {
557                        repr.rebind(&geometry, datums);
558                    }
559                }
560                NodeData::Face(face) => {
561                    face.surface = face.surface.with_scope(geometry.surfaces);
562                    face.triangulation = face
563                        .triangulation
564                        .map(|mesh| mesh.with_scope(geometry.triangulations));
565                    face.location = face.location.with_datum_scope(datums);
566                }
567                NodeData::Vertex(_) | NodeData::Container => {}
568            }
569        }
570    }
571
572    /// Verify that restored nodes' handles all resolve and their children are
573    /// of the kinds their parents admit.
574    ///
575    /// `from` bounds the pass the way [`Model::bind_handles`]' does. Children
576    /// still resolve through the full arena, so nothing is under-checked; an
577    /// absorbed subgraph is self-contained and can only point at itself.
578    fn check_restored(&self, identity: &[(TShapeId, EntityId)], from: u32) -> OgeomResult<()> {
579        for (id, node) in self.nodes.iter().filter(|(id, _)| id.index() >= from) {
580            let node = &node.shape;
581            let kind = node.kind();
582            match (kind, node.data()) {
583                (ShapeType::Vertex, NodeData::Vertex(_))
584                | (ShapeType::Edge, NodeData::Edge(_))
585                | (ShapeType::Face, NodeData::Face(_)) => {}
586                (
587                    ShapeType::Wire
588                    | ShapeType::Shell
589                    | ShapeType::Solid
590                    | ShapeType::CompSolid
591                    | ShapeType::Compound,
592                    NodeData::Container,
593                ) => {}
594                (kind, data) => {
595                    ogeom_bail!(Construction, "node {id:?} is a {kind:?} and holds {data:?}")
596                }
597            }
598            self.check_node_geometry(id, node)?;
599
600            // A compound may hold anything; everything else admits exactly one
601            // kind of child, which is what makes traversal's assumptions safe.
602            let expected = kind.child_type();
603            for child in node.children() {
604                let Some(below) = self.nodes.get(child.node()) else {
605                    ogeom_bail!(Dangling, "node {id:?} names a child that is not there");
606                };
607                if let Some(expected) = expected
608                    && kind != ShapeType::Compound
609                    && below.kind() != expected
610                {
611                    ogeom_bail!(
612                        Construction,
613                        "a {kind:?} takes {expected:?} children; node {id:?} \
614                         names a {:?}",
615                        below.kind()
616                    );
617                }
618                self.check_location(child.location())?;
619            }
620        }
621        for (node, entity) in identity {
622            if self.nodes.get(*node).is_none() {
623                ogeom_bail!(Dangling, "an identity is bound to a node that is not there");
624            }
625            if entity.get() > self.provenance.len() as u64 {
626                ogeom_bail!(
627                    Dangling,
628                    "node {node:?} claims identity {}, which was never issued",
629                    entity.get()
630                );
631            }
632        }
633        Ok(())
634    }
635
636    /// Verify that a node's geometry handles resolve.
637    fn check_node_geometry(&self, id: TShapeId, node: &TShape) -> OgeomResult<()> {
638        match node.data() {
639            NodeData::Edge(data) => {
640                for repr in &data.representations {
641                    if let Some(location) = repr.location() {
642                        self.check_location(location)?;
643                    }
644                    if !self.geometry.holds(repr) {
645                        ogeom_bail!(
646                            Dangling,
647                            "edge {id:?} names geometry that is not in this model"
648                        );
649                    }
650                }
651            }
652            NodeData::Face(data) => {
653                self.check_location(&data.location)?;
654                if self.geometry.surface(data.surface).is_none() {
655                    ogeom_bail!(Dangling, "face {id:?} names a surface that is not there");
656                }
657                if let Some(mesh) = data.triangulation
658                    && self.geometry.triangulation(mesh).is_none()
659                {
660                    ogeom_bail!(
661                        Dangling,
662                        "face {id:?} names a triangulation that is not there"
663                    );
664                }
665            }
666            NodeData::Vertex(_) | NodeData::Container => {}
667        }
668        Ok(())
669    }
670
671    /// Verify that every datum a placement names is interned.
672    fn check_location(&self, location: &Location) -> OgeomResult<()> {
673        for &(datum, _) in location.chain() {
674            if self.datums.get(datum).is_none() {
675                ogeom_bail!(Dangling, "a placement names a datum that is not there");
676            }
677        }
678        Ok(())
679    }
680
681    /// Begin a new operation, and return its identifier.
682    ///
683    /// Every node created from here on is attributed to it until the next call.
684    /// The counter is deterministic (the third operation in a rebuild is
685    /// `OpId(3)` every time), which is what lets provenance survive a parameter
686    /// change (`docs/DATA_MODEL.md` §8).
687    pub const fn begin_operation(&mut self) -> OpId {
688        self.current_op = OpId(self.current_op.0 + 1);
689        self.current_op
690    }
691
692    /// The operation nodes are currently attributed to.
693    #[must_use]
694    pub const fn current_operation(&self) -> OpId {
695        self.current_op
696    }
697
698    /// The stable identity of a shape's node.
699    ///
700    /// Distinct from its arena handle: the handle says where the data is and
701    /// dies when the shape is rebuilt, while this says what the entity *is* and
702    /// survives.
703    #[must_use]
704    pub fn identity_of(&self, shape: &Shape) -> Option<EntityId> {
705        self.nodes.entry(shape.node())?.identity
706    }
707
708    /// Where a shape's node came from.
709    #[must_use]
710    pub fn provenance_of(&self, shape: &Shape) -> Option<&Provenance> {
711        self.provenance.get(self.identity_of(shape)?)
712    }
713
714    /// The provenance table.
715    #[must_use]
716    pub const fn provenance(&self) -> &ProvenanceTable {
717        &self.provenance
718    }
719
720    /// Drop the provenance entries of `ids`, keeping the ids issued.
721    ///
722    /// What a document reader does with the entities its file left out: no
723    /// shape the file carries names them and no kept entry derives from
724    /// them, so they answer `None` as an id from another document does, and
725    /// no later entity can take their numbers. Ids without an entry are
726    /// passed over.
727    pub fn forget_provenance(&mut self, ids: impl IntoIterator<Item = EntityId>) {
728        for id in ids {
729            self.provenance.forget(id);
730        }
731    }
732
733    /// Drop everything `roots` do not reach, in place.
734    ///
735    /// Kept: every node below a root, the curves, pcurves, surfaces and
736    /// triangulations those nodes name, and the provenance entries of the
737    /// identities they carry with every entry those derive from. Every
738    /// handle into what is kept resolves as before and means the same
739    /// thing, and a lineage query on a kept shape answers as before. A
740    /// handle into what is dropped fails to resolve, and no later entity
741    /// takes its slot or its id, so it never answers about something else.
742    /// Datums are kept whole: a placement a caller holds keeps resolving.
743    ///
744    /// What is dropped stops costing memory, and a clone costs what is
745    /// kept. For a model that runs many operations and keeps one result:
746    /// call it with that result once the model has grown well past what
747    /// the result reaches.
748    ///
749    /// # Errors
750    ///
751    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a root
752    /// does not resolve in this model; nothing is dropped then.
753    pub fn retain_reachable(&mut self, roots: &[Shape]) -> OgeomResult<()> {
754        if roots.iter().any(|root| self.node(root).is_none()) {
755            ogeom_bail!(Dangling, "a root to retain is not in this model");
756        }
757        if self.widened.is_some() {
758            ogeom_bail!(
759                Construction,
760                "an operation is noting the tolerances it widens; a model is \
761                 retained between operations"
762            );
763        }
764        let reach = self.reach(roots);
765
766        self.nodes
767            .arena
768            .retain(|key, _| reach.nodes.contains(&key.index()));
769        // A shape kept holds every node below it, which it reaches.
770        self.nodes
771            .reads
772            .retain(|shape| reach.nodes.contains(&shape.node().index()));
773        let mut kept_boxes: Vec<u32> = Vec::new();
774        self.any_held = false;
775        for (_, node) in self.nodes.iter_mut() {
776            self.any_held |= node.held;
777            node.held_by.retain(|above| reach.nodes.contains(above));
778            if let Some(slot) = node.face_box.as_mut() {
779                kept_boxes.push(*slot);
780                *slot = u32::try_from(kept_boxes.len() - 1).unwrap_or(u32::MAX);
781            }
782        }
783        self.face_boxes.keep_slots(&kept_boxes);
784        self.geometry.retain(
785            |id| reach.curves.contains(&id),
786            |id| reach.pcurves.contains(&id),
787            |id| reach.surfaces.contains(&id),
788            |id| reach.meshes.contains(&id),
789        );
790        self.provenance.retain(|id, _| reach.entities.contains(&id));
791        Ok(())
792    }
793
794    /// Everything `roots` reach: their nodes, the geometry those name, and
795    /// the identities those carry with their ancestry.
796    fn reach(&self, roots: &[Shape]) -> Reach {
797        let mut reach = Reach::default();
798        let mut queue: Vec<TShapeId> = Vec::new();
799        for root in roots {
800            if reach.nodes.insert(root.node().index()) {
801                queue.push(root.node());
802            }
803        }
804        let mut ancestry: Vec<EntityId> = Vec::new();
805        while let Some(id) = queue.pop() {
806            let Some(node) = self.nodes.entry(id) else {
807                continue;
808            };
809            if let Some(entity) = node.identity
810                && reach.entities.insert(entity)
811            {
812                ancestry.push(entity);
813            }
814            for child in node.shape.children() {
815                if self.nodes.entry(child.node()).is_some()
816                    && reach.nodes.insert(child.node().index())
817                {
818                    queue.push(child.node());
819                }
820            }
821            match node.shape.data() {
822                NodeData::Edge(edge) => {
823                    for repr in &edge.representations {
824                        match repr {
825                            EdgeRepr::Curve3d { curve, .. } => {
826                                reach.curves.insert(*curve);
827                            }
828                            EdgeRepr::PCurve { curve, surface, .. } => {
829                                reach.pcurves.insert(*curve);
830                                reach.surfaces.insert(*surface);
831                            }
832                            EdgeRepr::Seam {
833                                forward,
834                                reversed,
835                                surface,
836                                ..
837                            } => {
838                                reach.pcurves.insert(*forward);
839                                reach.pcurves.insert(*reversed);
840                                reach.surfaces.insert(*surface);
841                            }
842                            EdgeRepr::Polyline { .. } => {}
843                            EdgeRepr::PolygonOnTriangulation { triangulation, .. } => {
844                                reach.meshes.insert(*triangulation);
845                            }
846                        }
847                    }
848                }
849                NodeData::Face(face) => {
850                    reach.surfaces.insert(face.surface);
851                    if let Some(mesh) = face.triangulation {
852                        reach.meshes.insert(mesh);
853                    }
854                }
855                NodeData::Vertex(_) | NodeData::Container => {}
856            }
857        }
858        while let Some(entity) = ancestry.pop() {
859            if let Some(entry) = self.provenance.get(entity) {
860                for &from in entry.inputs() {
861                    if reach.entities.insert(from) {
862                        ancestry.push(from);
863                    }
864                }
865            }
866        }
867        reach
868    }
869
870    /// Trace a shape back to the entities it ultimately came from.
871    ///
872    /// How a reference into a rebuilt model is resolved: find what the user
873    /// originally picked, then find what that became.
874    #[must_use]
875    pub fn roots_of(&self, shape: &Shape) -> Vec<EntityId> {
876        self.identity_of(shape)
877            .map(|id| self.provenance.roots(id))
878            .unwrap_or_default()
879    }
880
881    /// The shape carrying a given identity, if this document has one.
882    ///
883    /// The inverse of [`Model::identity_of`], and the answer to "I kept a
884    /// reference and the document has been saved and reloaded since". A raw
885    /// [`Shape`] cannot survive that: the reloaded document is a new set of
886    /// arenas and [`Model::bind`] refuses a handle from another one, on
887    /// purpose. An [`EntityId`] can, because it names *what the entity is*
888    /// rather than where it sits (`docs/DATA_MODEL.md` §8), and that is the
889    /// whole reason it exists.
890    ///
891    /// Returns the shape in its default placement and orientation. A caller
892    /// that wants a particular occurrence explores from here.
893    #[must_use]
894    pub fn shape_of(&self, id: EntityId) -> Option<Shape> {
895        self.nodes
896            .iter()
897            .find(|(_, node)| node.identity == Some(id))
898            .map(|(node, _)| Shape::of(node))
899    }
900
901    /// Record that a node was derived from other entities.
902    ///
903    /// Overwrites the `Primitive` attribution a builder assigns by default.
904    /// An operation that splits or reshapes existing topology calls this, and
905    /// what it records is what a later rebuild will match against.
906    ///
907    /// # Errors
908    ///
909    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape does not
910    /// resolve in this model.
911    pub fn set_derived(
912        &mut self,
913        shape: &Shape,
914        from: &[Shape],
915        role: Role,
916    ) -> OgeomResult<EntityId> {
917        if self.node(shape).is_none() {
918            ogeom_bail!(Dangling, "shape refers to a node not in this model");
919        }
920        let sources: Vec<EntityId> = from.iter().filter_map(|s| self.identity_of(s)).collect();
921        let id = self.provenance.derived(self.current_op, sources, role);
922        if let Some(node) = self.nodes.entry_mut(shape.node()) {
923            node.identity = Some(id);
924        }
925        Ok(id)
926    }
927
928    /// Record a node's identity as it is created.
929    fn record_primitive(&mut self, node: TShapeId, role: Role) {
930        let id = self.provenance.primitive(self.current_op, role);
931        if let Some(node) = self.nodes.entry_mut(node) {
932            node.identity = Some(id);
933        }
934    }
935
936    /// The placement datums.
937    #[must_use]
938    pub const fn datums(&self) -> &DatumStore {
939        &self.datums
940    }
941
942    /// The geometry.
943    #[must_use]
944    pub const fn geometry(&self) -> &GeometryStore {
945        &self.geometry
946    }
947
948    /// Mutable access to the geometry, for adding curves and surfaces.
949    ///
950    /// A surface rewritten in place through it forgets every face's kept
951    /// box ([`Model::face_bounds`]).
952    #[must_use]
953    pub const fn geometry_mut(&mut self) -> &mut GeometryStore {
954        &mut self.geometry
955    }
956
957    /// Intern a transform for use in placements.
958    pub fn add_datum(&mut self, transform: Transform) -> DatumId {
959        self.datums.insert(transform)
960    }
961
962    /// The node behind a shape's handle.
963    #[must_use]
964    pub fn node(&self, shape: &Shape) -> Option<&TShape> {
965        self.nodes.get(shape.node())
966    }
967
968    /// The node behind a handle.
969    #[must_use]
970    pub fn node_by_id(&self, id: TShapeId) -> Option<&TShape> {
971        self.nodes.get(id)
972    }
973
974    /// Mutable access to the node behind a shape's handle.
975    ///
976    /// For attaching geometry to an entity that already exists: a pcurve
977    /// joining an edge to a face it has just come to bound. Structural change
978    /// still goes through the builders; this reaches the node's *data*, which
979    /// no invariant here constrains on its own.
980    ///
981    /// The node may be changed in any way, so the box kept for every face
982    /// it is part of ([`Model::face_bounds`]) is forgotten, and so is what
983    /// is kept of every shape holding it ([`Model::kept_read`]). Every shape
984    /// holding the node sees the change; an editor starting from a shape it
985    /// was given copies what other shapes hold first ([`Model::unshare`]).
986    #[must_use]
987    pub fn node_mut(&mut self, shape: &Shape) -> Option<&mut TShape> {
988        self.sync_face_boxes();
989        if let Some(node) = self.nodes.get(shape.node()) {
990            // The tolerance as it stands, which an edit through the node
991            // may grow: what an attempt that fails puts back.
992            if let Some(journal) = &mut self.widened
993                && let Some(was) = node.data().tolerance()
994            {
995                journal.push((shape.node(), was));
996            }
997        }
998        self.forget_boxes_above(shape.node());
999        self.nodes.get_mut(shape.node())
1000    }
1001
1002    /// Insert a node, with a slot for its box if it is a face and its links
1003    /// up from the children it holds.
1004    fn insert_node(&mut self, node: TShape) -> TShapeId {
1005        self.sync_face_boxes();
1006        let id = self.insert_unlinked(node);
1007        self.link_children(id, true);
1008        id
1009    }
1010
1011    /// Insert a node, with a slot for its box if it is a face.
1012    fn insert_unlinked(&mut self, shape: TShape) -> TShapeId {
1013        let face_box = (shape.kind() == ShapeType::Face).then(|| self.face_boxes.add());
1014        self.nodes.insert(Node {
1015            shape,
1016            identity: None,
1017            face_box,
1018            held_by: SmallVec::new(),
1019            held: false,
1020        })
1021    }
1022
1023    /// Record node `id` with each child it holds, where it is part of a
1024    /// face: the link [`Model::forget_boxes_above`] climbs. `fresh` where
1025    /// `id` was linked to nothing before.
1026    fn link_children(&mut self, id: TShapeId, fresh: bool) {
1027        let Some(node) = self.nodes.get(id) else {
1028            return;
1029        };
1030        if !matches!(
1031            node.kind(),
1032            ShapeType::Edge | ShapeType::Wire | ShapeType::Face
1033        ) {
1034            return;
1035        }
1036        let children: SmallVec<[TShapeId; 8]> = node.children().iter().map(Shape::node).collect();
1037        // A child already linked to a fresh `id` was linked by this loop,
1038        // as the last of its holders: a child held by many faces in turn is
1039        // not searched through them all.
1040        for child in children {
1041            if let Some(below) = self.nodes.entry_mut(child)
1042                && if fresh {
1043                    below.held_by.last() != Some(&id.index())
1044                } else {
1045                    !below.held_by.contains(&id.index())
1046                }
1047            {
1048                below.held_by.push(id.index());
1049            }
1050        }
1051    }
1052
1053    /// Forget the box of every face that holds node `id`, the node itself
1054    /// included where it is a face.
1055    fn forget_boxes_above(&mut self, id: TShapeId) {
1056        if self.nodes.get(id).is_none() {
1057            return;
1058        }
1059        let mut stack: SmallVec<[u32; 8]> = smallvec::smallvec![id.index()];
1060        while let Some(at) = stack.pop() {
1061            let Some(node) = self.nodes.at(at).and_then(|id| self.nodes.entry(id)) else {
1062                continue;
1063            };
1064            if let Some(slot) = node.face_box {
1065                self.face_boxes.forget(slot);
1066            }
1067            stack.extend(node.held_by.iter().copied());
1068        }
1069    }
1070
1071    /// Forget every kept face box once the geometry has been rewritten in
1072    /// place since they were found.
1073    fn sync_face_boxes(&mut self) {
1074        let revision = self.geometry.revision();
1075        if self.face_boxes.revision != revision {
1076            self.face_boxes.forget_all();
1077            self.face_boxes.revision = revision;
1078        }
1079    }
1080
1081    /// The slot holding a face's box, while the geometry is as it was found
1082    /// against; `None` for anything else.
1083    fn face_box_slot(&self, face: &Shape) -> Option<&std::sync::OnceLock<Aabb>> {
1084        if self.face_boxes.revision != self.geometry.revision() {
1085            return None;
1086        }
1087        self.nodes
1088            .entry(face.node())
1089            .and_then(|node| node.face_box)
1090            .and_then(|slot| self.face_boxes.slot(slot))
1091    }
1092
1093    /// The box kept for a face: every point of the face, its tolerance
1094    /// included, in the face's placement. `None` where `face` is not a face
1095    /// of this model or no box has been kept for it yet; a kept box is
1096    /// found by `ogeom_algo::face_bounds`, and by the operations that read
1097    /// face boxes, and is kept until the face, its wires, edges or vertices,
1098    /// or the geometry in place change.
1099    ///
1100    /// The box is kept in the face's own frame. Under a placement that
1101    /// turns the face off the axes it is the box of the turned box, which
1102    /// holds the face but stands clear of it.
1103    #[must_use]
1104    pub fn face_bounds(&self, face: &Shape) -> Option<Aabb> {
1105        let kept = *self.face_box_slot(face)?.get()?;
1106        let tolerance = self.node(face)?.data().tolerance()?.get();
1107        let placement = face.transform(&self.datums).ok()?;
1108        Some(placed_box(&kept, &placement).expanded(tolerance))
1109    }
1110
1111    /// The box kept for a face in its own frame, without its placement or
1112    /// its tolerance; where none is kept, `find` is asked for it with the
1113    /// face unplaced, and its answer is kept.
1114    ///
1115    /// `find` must hold every point of the face it is given, and is what
1116    /// [`Model::face_bounds`] reports from then on. Where the face cannot
1117    /// keep a box (the geometry is being rewritten), `find`'s answer is
1118    /// returned and not kept.
1119    ///
1120    /// # Errors
1121    ///
1122    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
1123    /// if `face` is not a face of this model; as `find` reports.
1124    pub fn kept_face_box(
1125        &self,
1126        face: &Shape,
1127        find: impl FnOnce(&Shape) -> OgeomResult<Aabb>,
1128    ) -> OgeomResult<Aabb> {
1129        if self.node(face).map(TShape::kind) != Some(ShapeType::Face) {
1130            ogeom_bail!(Construction, "only a face of this model keeps a box");
1131        }
1132        let bare = Shape::of(face.node());
1133        let Some(slot) = self.face_box_slot(face) else {
1134            return find(&bare);
1135        };
1136        if let Some(kept) = slot.get() {
1137            return Ok(*kept);
1138        }
1139        let found = find(&bare)?;
1140        Ok(self.face_boxes.keep(slot, found))
1141    }
1142
1143    /// What `read` finds of `shape`, kept beside the model and handed out
1144    /// again until anything it was read from changes.
1145    ///
1146    /// A read of type `T` kept for `shape` that `same` accepts is returned
1147    /// as it is; otherwise `read` is asked, and its answer kept where it
1148    /// says to keep it. `read` must depend only on what lies below `shape`
1149    /// (the nodes' data and children, the geometry and placements they
1150    /// name, the face boxes kept), and `same` must accept only a read made
1151    /// with the inputs the caller would give `read` now.
1152    ///
1153    /// What is kept for a shape is forgotten when a node below it is
1154    /// changed ([`Model::node_mut`], [`Model::widen`] growing a tolerance,
1155    /// [`Model::undo_widened`], [`Model::unshare`] repointing a child),
1156    /// when a surface or a pcurve is rewritten in place, and when
1157    /// [`Model::retain_reachable`] drops the shape. A few shapes keep their
1158    /// reads at once, the oldest forgotten first. Keeping walks every node
1159    /// below the shape once, so it pays for a read that costs more than
1160    /// that walk.
1161    ///
1162    /// # Errors
1163    ///
1164    /// As `read` reports.
1165    pub fn kept_read<T: std::any::Any + Send + Sync>(
1166        &self,
1167        shape: &Shape,
1168        same: impl Fn(&T) -> bool,
1169        read: impl FnOnce() -> OgeomResult<(T, bool)>,
1170    ) -> OgeomResult<std::sync::Arc<T>> {
1171        let edits = self.geometry.edits();
1172        let reads = &self.nodes.reads;
1173        if let Some(kept) = reads.find(shape, edits, &same) {
1174            return Ok(kept);
1175        }
1176        let (found, keep) = read()?;
1177        let found = std::sync::Arc::new(found);
1178        if keep && self.node(shape).is_some() {
1179            let below = match reads.below(shape, edits) {
1180                Some(below) => below,
1181                None => std::sync::Arc::new(self.indices_below_kept(shape, &reads.sets(edits))),
1182            };
1183            reads.keep(shape, edits, below, std::sync::Arc::clone(&found), same);
1184        }
1185        Ok(found)
1186    }
1187
1188    /// Every read of type `T` kept now, for any shape, that `same`
1189    /// accepts, the shape kept latest first: what [`Model::kept_read`]
1190    /// would hand back for its shape. Each was read from its shape's nodes
1191    /// as they stand, so a part of one that depends only on the nodes below
1192    /// a face holds for that face in any shape holding it.
1193    #[must_use]
1194    pub fn kept_reads<T: std::any::Any + Send + Sync>(
1195        &self,
1196        same: impl Fn(&T) -> bool,
1197    ) -> Vec<std::sync::Arc<T>> {
1198        self.nodes.reads.all(self.geometry.edits(), same)
1199    }
1200
1201    /// The node index of `shape` and of every node below it, with every
1202    /// node of each set in `kept` that holds one of them.
1203    ///
1204    /// A node in a set kept for a shape has every node below it in that
1205    /// set: none of them has changed since the set was made, as a change
1206    /// forgets it, and no node's index is handed out again. The walk stops
1207    /// there and takes the set whole; what it takes beyond `shape` only
1208    /// forgets a read sooner.
1209    fn indices_below_kept(&self, shape: &Shape, kept: &[std::sync::Arc<NodeSet>]) -> NodeSet {
1210        if kept.is_empty() {
1211            return self.indices_below(shape);
1212        }
1213        let mut below = NodeSet::default();
1214        let mut joined = vec![false; kept.len()];
1215        let mut stack = vec![shape.node()];
1216        below.insert(shape.node().index());
1217        while let Some(id) = stack.pop() {
1218            let Some(node) = self.nodes.get(id) else {
1219                continue;
1220            };
1221            for child in node.children() {
1222                let index = child.node().index();
1223                if !below.insert(index) {
1224                    continue;
1225                }
1226                match kept.iter().position(|set| set.contains(index)) {
1227                    Some(at) => joined[at] = true,
1228                    None => stack.push(child.node()),
1229                }
1230            }
1231        }
1232        for (set, joined) in kept.iter().zip(joined) {
1233            if joined {
1234                below.union_with(set);
1235            }
1236        }
1237        below
1238    }
1239
1240    /// The node index of `shape` and of every node below it.
1241    fn indices_below(&self, shape: &Shape) -> NodeSet {
1242        let mut below = NodeSet::default();
1243        let mut stack = vec![shape.node()];
1244        below.insert(shape.node().index());
1245        while let Some(id) = stack.pop() {
1246            let Some(node) = self.nodes.get(id) else {
1247                continue;
1248            };
1249            for child in node.children() {
1250                if below.insert(child.node().index()) {
1251                    stack.push(child.node());
1252                }
1253            }
1254        }
1255        below
1256    }
1257
1258    /// What kind of shape this is.
1259    ///
1260    /// # Errors
1261    ///
1262    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the handle does not
1263    /// resolve in this model.
1264    pub fn kind_of(&self, shape: &Shape) -> OgeomResult<ShapeType> {
1265        let Some(node) = self.node(shape) else {
1266            ogeom_bail!(Dangling, "shape refers to a node not in this model");
1267        };
1268        Ok(node.kind())
1269    }
1270
1271    /// The tolerance a shape carries, if it carries one.
1272    ///
1273    /// # Errors
1274    ///
1275    /// As [`Model::kind_of`].
1276    pub fn tolerance_of(&self, shape: &Shape) -> OgeomResult<Option<Tolerance>> {
1277        let Some(node) = self.node(shape) else {
1278            ogeom_bail!(Dangling, "shape refers to a node not in this model");
1279        };
1280        Ok(node.data().tolerance())
1281    }
1282
1283    /// Number of topology nodes.
1284    #[must_use]
1285    pub fn node_count(&self) -> usize {
1286        self.nodes.len()
1287    }
1288
1289    /// Whether the model holds no topology.
1290    #[must_use]
1291    pub fn is_empty(&self) -> bool {
1292        self.nodes.is_empty()
1293    }
1294
1295    /// Every topology node, with its handle, in arena order.
1296    ///
1297    /// For writing a document out. Traversal from a root shape reaches only
1298    /// what that root bounds; a document is everything in it.
1299    pub fn nodes(&self) -> impl Iterator<Item = (TShapeId, &TShape)> {
1300        self.nodes.iter().map(|(id, node)| (id, &node.shape))
1301    }
1302
1303    /// Every node that has been given an identity, with it.
1304    pub fn identities(&self) -> impl Iterator<Item = (TShapeId, EntityId)> {
1305        self.nodes
1306            .iter()
1307            .filter_map(|(id, node)| Some((id, node.identity?)))
1308    }
1309
1310    /// Add a vertex.
1311    pub fn add_vertex(&mut self, data: VertexData) -> Shape {
1312        Shape::of(self.insert_node(TShape::leaf(ShapeType::Vertex, NodeData::Vertex(data))))
1313    }
1314
1315    /// Add a vertex at `point` with the minimum tolerance.
1316    pub fn add_point(&mut self, point: Point) -> Shape {
1317        self.add_vertex(VertexData::new(point))
1318    }
1319
1320    /// Add an edge bounded by the given vertices.
1321    ///
1322    /// The vertices are the edge's ends, in order. A closed edge (a full
1323    /// circle) names the same vertex twice rather than once, so that walking
1324    /// its boundary yields a start and an end as every other edge does.
1325    ///
1326    /// # Errors
1327    ///
1328    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a bound is
1329    /// not a vertex, or if there are more than two;
1330    /// [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) if a vertex's
1331    /// tolerance is tighter than the edge's, breaking the containment rule.
1332    pub fn add_edge(&mut self, data: EdgeData, bounds: &[Shape]) -> OgeomResult<Shape> {
1333        if bounds.len() > 2 {
1334            ogeom_bail!(
1335                Construction,
1336                "an edge has at most two bounding vertices, got {}",
1337                bounds.len()
1338            );
1339        }
1340        self.check_children(ShapeType::Vertex, bounds)?;
1341        // A vertex caps an edge, so it must be at least as uncertain as the
1342        // edge is; otherwise the cap does not reliably sit on what it caps.
1343        for bound in bounds {
1344            self.widen(bound, data.tolerance)?;
1345        }
1346        let node = self.insert_node(TShape::new(
1347            ShapeType::Edge,
1348            NodeData::Edge(Box::new(data)),
1349            bounds.to_vec(),
1350        ));
1351        self.record_primitive(node, Role::SOLE);
1352        Ok(Shape::of(node))
1353    }
1354
1355    /// Add a wire from a sequence of edges.
1356    ///
1357    /// # Errors
1358    ///
1359    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
1360    /// not an edge, or the wire is empty.
1361    pub fn add_wire(&mut self, edges: &[Shape]) -> OgeomResult<Shape> {
1362        if edges.is_empty() {
1363            ogeom_bail!(Construction, "a wire needs at least one edge");
1364        }
1365        self.check_children(ShapeType::Edge, edges)?;
1366        Ok(Shape::of(self.insert_node(TShape::container(
1367            ShapeType::Wire,
1368            edges.to_vec(),
1369        ))))
1370    }
1371
1372    /// Add a face bounded by the given wires.
1373    ///
1374    /// The first wire is the outer boundary and any others are holes, and
1375    /// the face keeps them in that order whichever way it is later used; see
1376    /// [`Model::outer_wire`].
1377    ///
1378    /// A face with no wires covers its surface's whole domain, and is recorded
1379    /// as naturally restricted.
1380    ///
1381    /// # Errors
1382    ///
1383    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a bound is
1384    /// not a wire; [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) if the
1385    /// containment rule is broken.
1386    pub fn add_face(&mut self, mut data: FaceData, wires: &[Shape]) -> OgeomResult<Shape> {
1387        self.check_children(ShapeType::Wire, wires)?;
1388        if wires.is_empty() {
1389            data.natural_restriction = true;
1390        }
1391        // An edge borders a face, so it must be at least as uncertain as the
1392        // face. Walk the wires' edges and widen them where they are not; no
1393        // edge is tighter than the least tolerance.
1394        let face_tolerance = data.tolerance;
1395        let walk = if face_tolerance.get() > Tolerance::MIN.get() {
1396            wires
1397        } else {
1398            &[]
1399        };
1400        for wire in walk {
1401            let edges = self.children_of(wire)?;
1402            for edge in &edges {
1403                self.widen(edge, face_tolerance)?;
1404            }
1405        }
1406        let node = self.insert_node(TShape::new(
1407            ShapeType::Face,
1408            NodeData::Face(Box::new(data)),
1409            wires.to_vec(),
1410        ));
1411        self.record_primitive(node, Role::SOLE);
1412        Ok(Shape::of(node))
1413    }
1414
1415    /// Restate `face` in place on `surface`, bounded by `wires`: the same
1416    /// face read on another chart of the same points, held by every shape
1417    /// that held it. For a builder that has re-charted a face (a sphere
1418    /// face turned so its poles stand clear of its trim) and whose shells,
1419    /// documents and tables already name the face: none of them has to
1420    /// learn a new one.
1421    ///
1422    /// The face's kept triangulation and box are forgotten, as is what is
1423    /// kept of every shape holding it.
1424    ///
1425    /// # Errors
1426    ///
1427    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
1428    /// if `face` is not a face, a wire is not a wire, or `wires` is empty;
1429    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if `face`
1430    /// is not in this model.
1431    pub fn rechart_face(
1432        &mut self,
1433        face: &Shape,
1434        surface: SurfaceId,
1435        wires: &[Shape],
1436    ) -> OgeomResult<()> {
1437        if self.kind_of(face)? != ShapeType::Face {
1438            ogeom_bail!(Construction, "only a face can be re-charted");
1439        }
1440        if wires.is_empty() {
1441            ogeom_bail!(Construction, "a re-charted face needs its boundary");
1442        }
1443        self.check_children(ShapeType::Wire, wires)?;
1444        self.sync_face_boxes();
1445        self.forget_boxes_above(face.node());
1446        let Some(node) = self.nodes.get_mut(face.node()) else {
1447            ogeom_bail!(Dangling, "face is not in this model");
1448        };
1449        let NodeData::Face(data) = node.data_mut() else {
1450            ogeom_bail!(Construction, "face node holds no face data");
1451        };
1452        data.surface = surface;
1453        data.natural_restriction = false;
1454        data.triangulation = None;
1455        *node.children_mut() = wires.to_vec();
1456        self.link_children(face.node(), false);
1457        Ok(())
1458    }
1459
1460    /// Add a shell from a set of faces.
1461    ///
1462    /// # Errors
1463    ///
1464    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
1465    /// not a face, or the shell is empty.
1466    pub fn add_shell(&mut self, faces: &[Shape]) -> OgeomResult<Shape> {
1467        if faces.is_empty() {
1468            ogeom_bail!(Construction, "a shell needs at least one face");
1469        }
1470        self.check_children(ShapeType::Face, faces)?;
1471        Ok(Shape::of(self.insert_node(TShape::container(
1472            ShapeType::Shell,
1473            faces.to_vec(),
1474        ))))
1475    }
1476
1477    /// Add a solid bounded by the given shells.
1478    ///
1479    /// # Errors
1480    ///
1481    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
1482    /// not a shell, or the solid is empty.
1483    pub fn add_solid(&mut self, shells: &[Shape]) -> OgeomResult<Shape> {
1484        if shells.is_empty() {
1485            ogeom_bail!(Construction, "a solid needs at least one shell");
1486        }
1487        self.check_children(ShapeType::Shell, shells)?;
1488        Ok(Shape::of(self.insert_node(TShape::container(
1489            ShapeType::Solid,
1490            shells.to_vec(),
1491        ))))
1492    }
1493
1494    /// Add a compsolid from solids sharing faces.
1495    ///
1496    /// # Errors
1497    ///
1498    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a child is
1499    /// not a solid, or it is empty.
1500    pub fn add_compsolid(&mut self, solids: &[Shape]) -> OgeomResult<Shape> {
1501        if solids.is_empty() {
1502            ogeom_bail!(Construction, "a compsolid needs at least one solid");
1503        }
1504        self.check_children(ShapeType::Solid, solids)?;
1505        Ok(Shape::of(self.insert_node(TShape::container(
1506            ShapeType::CompSolid,
1507            solids.to_vec(),
1508        ))))
1509    }
1510
1511    /// Add a compound of arbitrary shapes.
1512    ///
1513    /// The one container with no type constraint; that is what a compound is
1514    /// for. It may be empty, since an empty result is a legitimate answer from
1515    /// a boolean and needs somewhere to live.
1516    ///
1517    /// # Errors
1518    ///
1519    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if a child does not
1520    /// resolve in this model.
1521    pub fn add_compound(&mut self, shapes: &[Shape]) -> OgeomResult<Shape> {
1522        for shape in shapes {
1523            if self.node(shape).is_none() {
1524                ogeom_bail!(Dangling, "compound member is not in this model");
1525            }
1526        }
1527        Ok(Shape::of(self.insert_node(TShape::container(
1528            ShapeType::Compound,
1529            shapes.to_vec(),
1530        ))))
1531    }
1532
1533    /// The direct children of a shape, with this shape's placement and
1534    /// orientation composed onto each.
1535    ///
1536    /// The reason traversal is correct by default rather than by discipline:
1537    /// a child's placement in the world is its parent's composed with its
1538    /// own, and its orientation is its parent's composed with its own.
1539    /// Returning raw children would leave every caller to remember both, and
1540    /// the failure is silent: face normals that flip inconsistently,
1541    /// sub-shapes drawn at the origin.
1542    ///
1543    /// # Errors
1544    ///
1545    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape does not
1546    /// resolve in this model.
1547    pub fn children_of(&self, shape: &Shape) -> OgeomResult<Vec<Shape>> {
1548        let Some(node) = self.node(shape) else {
1549            ogeom_bail!(Dangling, "shape refers to a node not in this model");
1550        };
1551        Ok(node
1552            .children()
1553            .iter()
1554            .map(|child| child.beneath(shape.location(), shape.orientation()))
1555            .collect())
1556    }
1557
1558    /// A shape's children in *traversal* order.
1559    ///
1560    /// The same shapes as [`Model::children_of`], but with the list reversed
1561    /// when the parent is reversed. Order carries meaning for a wire (its
1562    /// edges run head to tail), and reversing a wire has to reverse the walk as
1563    /// well as each edge, or consecutive edges stop sharing a vertex and the
1564    /// boundary comes apart. For a shell or a solid the order means nothing and
1565    /// the reversal is invisible.
1566    ///
1567    /// [`Model::children_of`] stays the raw accessor: it returns what is
1568    /// stored, which is what a rebuild or a comparison wants.
1569    ///
1570    /// A reversed face's wires come back reversed too, so its holes are
1571    /// listed before its outer wire. A face stores its outer wire first, and
1572    /// that is the only place the order means anything: code that wants the
1573    /// outer wire asks [`Model::outer_wire`], and code that wants every wire
1574    /// walked under the face's sense, outer first, asks
1575    /// [`Model::children_of`], whose wires carry the face's orientation in
1576    /// stored order.
1577    ///
1578    /// # Errors
1579    ///
1580    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape does not
1581    /// resolve in this model.
1582    pub fn ordered_children_of(&self, shape: &Shape) -> OgeomResult<Vec<Shape>> {
1583        let mut children = self.children_of(shape)?;
1584        if shape.orientation() == Orientation::Reversed {
1585            children.reverse();
1586        }
1587        Ok(children)
1588    }
1589
1590    /// A face's outer wire, carrying the face's placement and orientation,
1591    /// or `None` for a face with no wires (one covering its whole surface).
1592    ///
1593    /// The outer wire is the first one the face stores, whichever way the
1594    /// face is used. [`Model::ordered_children_of`] on a reversed face lists
1595    /// the holes first, so its first wire is not this one.
1596    ///
1597    /// # Errors
1598    ///
1599    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the face does not
1600    /// resolve in this model; [`OgeomError::Construction`](ogeom_core::OgeomError::Construction)
1601    /// if the shape is not a face.
1602    pub fn outer_wire(&self, face: &Shape) -> OgeomResult<Option<Shape>> {
1603        if self.kind_of(face)? != ShapeType::Face {
1604            ogeom_bail!(Construction, "only a face has an outer wire");
1605        }
1606        Ok(self.children_of(face)?.into_iter().next())
1607    }
1608
1609    /// Widen a shape's tolerance, and every sub-shape's with it.
1610    ///
1611    /// The cascade is the point. The containment rule is transitive: a face's
1612    /// edges must be no tighter than the face, *and* those edges' vertices no
1613    /// tighter than the edges. Widening only one level leaves the rule broken
1614    /// two levels down, where nothing will notice until a containment test
1615    /// quietly answers about geometry that does not meet.
1616    ///
1617    /// Tolerances only ever grow, so this is the sanctioned repair: raise what
1618    /// bounds, never lower what is bounded.
1619    ///
1620    /// The nodes are widened where they stand, held by other shapes as well
1621    /// or not ([`Model::note_held`]): a boolean widens a vertex its rebuilt
1622    /// edges end on though a face it set aside, and so the operand, holds
1623    /// it too. Only a tolerance grows: every shape holding the node keeps
1624    /// its geometry and its pcurves, and a looser bound still bounds what
1625    /// it bounded, so each stays valid. An editor starting from a shape it
1626    /// was given copies what is held first ([`Model::unshare`]), and widens
1627    /// no other shape.
1628    ///
1629    /// # Errors
1630    ///
1631    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape, or
1632    /// anything below it, does not resolve in this model.
1633    pub fn widen(&mut self, shape: &Shape, to: Tolerance) -> OgeomResult<()> {
1634        // An edge and its vertices, the usual call, are searched in a short
1635        // list; a larger shape's nodes are hashed.
1636        const SHORT: usize = 32;
1637        self.sync_face_boxes();
1638        let mut affected: smallvec::SmallVec<[TShapeId; 8]> = smallvec::SmallVec::new();
1639        let mut seen: Option<ogeom_core::FastSet<TShapeId>> = None;
1640        let mut stack: smallvec::SmallVec<[TShapeId; 8]> = smallvec::smallvec![shape.node()];
1641        while let Some(id) = stack.pop() {
1642            let fresh = match &mut seen {
1643                Some(seen) => seen.insert(id),
1644                None => !affected.contains(&id),
1645            };
1646            if !fresh {
1647                continue;
1648            }
1649            if seen.is_none() && affected.len() >= SHORT {
1650                seen = Some(affected.iter().copied().chain([id]).collect());
1651            }
1652            let Some(node) = self.nodes.get(id) else {
1653                ogeom_bail!(Dangling, "shape refers to a node not in this model");
1654            };
1655            affected.push(id);
1656            stack.extend(node.children().iter().map(Shape::node));
1657        }
1658        for id in affected {
1659            // A node already as wide is left as it stands, and what is kept
1660            // of the shapes holding it with it.
1661            let grows = self
1662                .nodes
1663                .get(id)
1664                .and_then(|node| node.data().tolerance())
1665                .is_some_and(|was| was.get() < to.get());
1666            if grows && let Some(node) = self.nodes.get_mut(id) {
1667                if let Some(journal) = &mut self.widened
1668                    && let Some(was) = node.data().tolerance()
1669                {
1670                    journal.push((id, was));
1671                }
1672                node.data_mut().widen(to);
1673            }
1674        }
1675        Ok(())
1676    }
1677
1678    /// Start noting the tolerances [`Model::widen`] grows and edits through
1679    /// [`Model::node_mut`] may grow, so an attempt that fails can put them
1680    /// back with [`Model::undo_widened`]. Where a journal is
1681    /// open already, it goes on and the mark is where this attempt began.
1682    pub fn note_widened(&mut self) -> WidenMark {
1683        match &self.widened {
1684            Some(journal) => WidenMark {
1685                from: journal.len(),
1686                opened: false,
1687            },
1688            None => {
1689                self.widened = Some(Vec::new());
1690                WidenMark {
1691                    from: 0,
1692                    opened: true,
1693                }
1694            }
1695        }
1696    }
1697
1698    /// Put back every tolerance noted since `mark`, and
1699    /// close the journal where `mark` opened it.
1700    pub fn undo_widened(&mut self, mark: WidenMark) {
1701        let Some(journal) = &mut self.widened else {
1702            return;
1703        };
1704        let undone: Vec<(TShapeId, Tolerance)> =
1705            journal.drain(mark.from.min(journal.len())..).collect();
1706        // Latest first, so a node grown twice ends at what it was first.
1707        for (id, was) in undone.into_iter().rev() {
1708            if let Some(node) = self.nodes.get_mut(id) {
1709                node.data_mut().set_tolerance(was);
1710            }
1711        }
1712        if mark.opened {
1713            self.widened = None;
1714        }
1715    }
1716
1717    /// Keep the tolerances noted since `mark` as they now stand, and close
1718    /// the journal where `mark` opened it.
1719    pub fn keep_widened(&mut self, mark: WidenMark) {
1720        if mark.opened {
1721            self.widened = None;
1722        }
1723    }
1724
1725    /// Whether node `id` is marked held ([`Model::note_held`]).
1726    fn is_held(&self, id: TShapeId) -> bool {
1727        self.nodes.entry(id).is_some_and(|node| node.held)
1728    }
1729
1730    /// Record that `result` holds nodes made before the model held `since`
1731    /// nodes: what an operation passed through into its result from the
1732    /// shapes it was given, which those shapes still hold.
1733    ///
1734    /// The walk goes down from `result` through the nodes made since, and
1735    /// marks each older node where it first meets one; what lies below a
1736    /// marked node is held with it. It costs what the operation made.
1737    /// [`Model::unshare`] reads the marks.
1738    ///
1739    /// # Errors
1740    ///
1741    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the
1742    /// shape, or anything below it, does not resolve in this model.
1743    pub fn note_held(&mut self, result: &Shape, since: usize) -> OgeomResult<()> {
1744        // Nodes are only appended between counting `since` and here, so the
1745        // ones made since are the last `made` slots handed out.
1746        let made = self.nodes.len().saturating_sub(since);
1747        let next = self.nodes.arena.next_index().unwrap_or(u32::MAX);
1748        let since = next.saturating_sub(u32::try_from(made).unwrap_or(u32::MAX));
1749        let mut seen = vec![false; made];
1750        let mut stack: smallvec::SmallVec<[TShapeId; 16]> = smallvec::smallvec![result.node()];
1751        while let Some(id) = stack.pop() {
1752            let Some(node) = self.nodes.get(id) else {
1753                ogeom_bail!(Dangling, "shape refers to a node not in this model");
1754            };
1755            let Some(at) = id.index().checked_sub(since) else {
1756                if let Some(node) = self.nodes.entry_mut(id) {
1757                    node.held = true;
1758                    self.any_held = true;
1759                }
1760                continue;
1761            };
1762            let Some(slot) = seen.get_mut(at as usize) else {
1763                continue;
1764            };
1765            if core::mem::replace(slot, true) {
1766                continue;
1767            }
1768            stack.extend(node.children().iter().map(Shape::node));
1769        }
1770        Ok(())
1771    }
1772
1773    /// Make every node below `root` the root's alone, so that an edit in
1774    /// place below it changes no other shape.
1775    ///
1776    /// Each node an operation passed through into `root` from the shapes
1777    /// it was given ([`Model::note_held`]), and everything below it, is
1778    /// copied: the same kind, data and identity, and the same kept face
1779    /// box. The nodes `root` holds alone that held one now hold its copy,
1780    /// at the same placement and orientation. What is held is found from
1781    /// `root` down; a `root` that is itself held is left as it stands,
1782    /// with everything below it.
1783    ///
1784    /// Returns each node copied, with its copy, in the order copied.
1785    ///
1786    /// # Errors
1787    ///
1788    /// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the
1789    /// shape, or anything below it, does not resolve in this model.
1790    pub fn unshare(&mut self, root: &Shape) -> OgeomResult<Vec<(TShapeId, TShapeId)>> {
1791        if !self.any_held || self.is_held(root.node()) {
1792            return Ok(Vec::new());
1793        }
1794        let mut pairs = Vec::new();
1795        self.unshare_below(std::slice::from_ref(root), true, &mut pairs)?;
1796        Ok(pairs)
1797    }
1798
1799    /// As [`Model::unshare`] for several shapes at once, for an operation
1800    /// that takes shapes and returns what it makes of them, and leaves the
1801    /// shapes it was given as they are.
1802    ///
1803    /// Nothing given is edited: each node held below a shape, and each
1804    /// node on the way down to one, the shape itself included, is copied
1805    /// once for them all, so a shape with anything held below it comes
1806    /// back as a copy. What a copy shares with the shape it was copied
1807    /// from is noted as held by both ([`Model::note_held`]).
1808    ///
1809    /// # Errors
1810    ///
1811    /// As [`Model::unshare`].
1812    pub fn unshare_each(&mut self, shapes: &[Shape]) -> OgeomResult<Unshared> {
1813        if !self.any_held {
1814            return Ok(Unshared {
1815                shapes: shapes.to_vec(),
1816                copies: Vec::new(),
1817            });
1818        }
1819        let since = self.nodes.len();
1820        let mut pairs = Vec::new();
1821        let copies = self.unshare_below(shapes, false, &mut pairs)?;
1822        let mut out = Vec::with_capacity(shapes.len());
1823        for shape in shapes {
1824            match copies.get(&shape.node()) {
1825                Some(&copy) => {
1826                    let copy = Shape::new(copy, shape.location().clone(), shape.orientation());
1827                    self.note_held(&copy, since)?;
1828                    out.push(copy);
1829                }
1830                None => out.push(shape.clone()),
1831            }
1832        }
1833        Ok(Unshared {
1834            shapes: out,
1835            copies: pairs,
1836        })
1837    }
1838
1839    /// Copy every node held below `roots`; where `repoint`, point the nodes
1840    /// the roots hold alone at the copies, and otherwise copy those on the
1841    /// way down to a held node too, the roots included. The copies by
1842    /// original.
1843    fn unshare_below(
1844        &mut self,
1845        roots: &[Shape],
1846        repoint: bool,
1847        pairs: &mut Vec<(TShapeId, TShapeId)>,
1848    ) -> OgeomResult<ogeom_core::FastMap<TShapeId, TShapeId>> {
1849        // Every node reached, in the order first reached, and whether it is
1850        // held: marked, or below a held node on some path.
1851        let mut held: ogeom_core::FastMap<TShapeId, bool> = ogeom_core::FastMap::default();
1852        let mut order: Vec<TShapeId> = Vec::new();
1853        let mut stack: Vec<(TShapeId, bool)> =
1854            roots.iter().rev().map(|r| (r.node(), false)).collect();
1855        let mut any = false;
1856        while let Some((id, above)) = stack.pop() {
1857            let is_held = above || self.is_held(id);
1858            match held.get(&id) {
1859                Some(&was) if was || !is_held => continue,
1860                Some(_) => {}
1861                None => order.push(id),
1862            }
1863            held.insert(id, is_held);
1864            any |= is_held;
1865            let Some(node) = self.nodes.get(id) else {
1866                ogeom_bail!(Dangling, "shape refers to a node not in this model");
1867            };
1868            stack.extend(node.children().iter().rev().map(|c| (c.node(), is_held)));
1869        }
1870        let mut copies: ogeom_core::FastMap<TShapeId, TShapeId> = ogeom_core::FastMap::default();
1871        if !any {
1872            return Ok(copies);
1873        }
1874        // The nodes to copy: the held ones, and where nothing is pointed
1875        // anew, every node with one of them below it.
1876        let mut copied: ogeom_core::FastSet<TShapeId> = held
1877            .iter()
1878            .filter(|(_, h)| **h)
1879            .map(|(id, _)| *id)
1880            .collect();
1881        if !repoint {
1882            loop {
1883                let mut grew = false;
1884                for &id in order.iter().rev() {
1885                    if copied.contains(&id) {
1886                        continue;
1887                    }
1888                    let below = self
1889                        .nodes
1890                        .get(id)
1891                        .is_some_and(|n| n.children().iter().any(|c| copied.contains(&c.node())));
1892                    if below {
1893                        copied.insert(id);
1894                        grew = true;
1895                    }
1896                }
1897                if !grew {
1898                    break;
1899                }
1900            }
1901        }
1902        self.sync_face_boxes();
1903        for root in roots {
1904            if copied.contains(&root.node()) {
1905                self.copy_below(root.node(), &copied, &mut copies, pairs)?;
1906            }
1907        }
1908        if !repoint {
1909            return Ok(copies);
1910        }
1911        for parent in order {
1912            if copied.contains(&parent) {
1913                continue;
1914            }
1915            let Some(node) = self.nodes.get(parent) else {
1916                continue;
1917            };
1918            let children = node.children().to_vec();
1919            let mut changed = false;
1920            let mut repointed = Vec::with_capacity(children.len());
1921            for child in children {
1922                if copied.contains(&child.node()) {
1923                    let copy = self.copy_below(child.node(), &copied, &mut copies, pairs)?;
1924                    repointed.push(Shape::new(
1925                        copy,
1926                        child.location().clone(),
1927                        child.orientation(),
1928                    ));
1929                    changed = true;
1930                } else {
1931                    repointed.push(child);
1932                }
1933            }
1934            if changed && let Some(node) = self.nodes.get_mut(parent) {
1935                *node.children_mut() = repointed;
1936                self.link_children(parent, false);
1937            }
1938        }
1939        Ok(copies)
1940    }
1941
1942    /// The copy of `id`, with every node below it that `copied` names
1943    /// copied too, each once.
1944    fn copy_below(
1945        &mut self,
1946        id: TShapeId,
1947        copied: &ogeom_core::FastSet<TShapeId>,
1948        copies: &mut ogeom_core::FastMap<TShapeId, TShapeId>,
1949        pairs: &mut Vec<(TShapeId, TShapeId)>,
1950    ) -> OgeomResult<TShapeId> {
1951        // Children before parents: a node is copied once every child to
1952        // copy has its copy.
1953        let mut stack: Vec<(TShapeId, bool)> = vec![(id, false)];
1954        while let Some((at, ready)) = stack.pop() {
1955            if copies.contains_key(&at) {
1956                continue;
1957            }
1958            let Some(node) = self.nodes.get(at) else {
1959                ogeom_bail!(Dangling, "shape refers to a node not in this model");
1960            };
1961            if !ready {
1962                stack.push((at, true));
1963                stack.extend(
1964                    node.children()
1965                        .iter()
1966                        .rev()
1967                        .filter(|c| copied.contains(&c.node()) && !copies.contains_key(&c.node()))
1968                        .map(|c| (c.node(), false)),
1969                );
1970                continue;
1971            }
1972            let children = node
1973                .children()
1974                .iter()
1975                .map(|c| {
1976                    let copy = copies.get(&c.node()).copied().unwrap_or(c.node());
1977                    Shape::new(copy, c.location().clone(), c.orientation())
1978                })
1979                .collect();
1980            let copy = TShape::new(node.kind(), node.data().clone(), children);
1981            let kept = self
1982                .face_box_slot(&Shape::of(at))
1983                .and_then(|slot| slot.get().copied());
1984            let made = self.insert_node(copy);
1985            let identity = self.nodes.entry(at).and_then(|node| node.identity);
1986            if let Some(node) = self.nodes.entry_mut(made) {
1987                node.identity = identity;
1988            }
1989            if let (Some(kept), Some(slot)) = (kept, self.face_box_slot(&Shape::of(made))) {
1990                self.face_boxes.keep(slot, kept);
1991            }
1992            copies.insert(at, made);
1993            pairs.push((at, made));
1994        }
1995        copies.get(&id).copied().ok_or_else(|| {
1996            ogeom_core::ogeom_err!(Dangling, "shape refers to a node not in this model")
1997        })
1998    }
1999
2000    /// Check that every child resolves and is of the expected type.
2001    fn check_children(&self, expected: ShapeType, children: &[Shape]) -> OgeomResult<()> {
2002        for child in children {
2003            let Some(node) = self.node(child) else {
2004                ogeom_bail!(Dangling, "child refers to a node not in this model");
2005            };
2006            if node.kind() != expected {
2007                ogeom_bail!(
2008                    Construction,
2009                    "expected a {expected:?} child, got a {:?}",
2010                    node.kind()
2011                );
2012            }
2013        }
2014        Ok(())
2015    }
2016
2017    /// Verify the containment rule across a whole shape tree.
2018    ///
2019    /// `docs/DATA_MODEL.md` §5. Walks parent to child and checks that whatever
2020    /// bounds is no tighter than what it bounds.
2021    ///
2022    /// # Errors
2023    ///
2024    /// [`OgeomError::Invariant`](ogeom_core::OgeomError::Invariant) at the first
2025    /// violation, naming the two shape types involved.
2026    pub fn check_tolerances(&self, root: &Shape) -> OgeomResult<()> {
2027        // Containers carry no tolerance of their own, so each node is held
2028        // to the nearest tolerance above it, whatever lies between: a face's
2029        // edges answer to the face across the wire that holds them. Each
2030        // node is visited once per bound it is reached under.
2031        let mut stack: Vec<(Shape, Option<(ogeom_core::Tolerance, ShapeType)>)> =
2032            vec![(root.clone(), None)];
2033        let mut seen = ogeom_core::FastSet::default();
2034        while let Some((shape, bound)) = stack.pop() {
2035            let Some(node) = self.node(&shape) else {
2036                ogeom_bail!(Dangling, "shape refers to a node not in this model");
2037            };
2038            if !seen.insert((shape.node(), bound.map(|(t, _)| t.get().to_bits()))) {
2039                continue;
2040            }
2041            let own = node.data().tolerance();
2042            // A boundary is *contained by* what it bounds, so the child (the
2043            // boundary) must be the looser of the two.
2044            if let (Some((parent, parent_kind)), Some(child_tolerance)) = (bound, own)
2045                && child_tolerance < parent
2046            {
2047                ogeom_bail!(
2048                    Invariant,
2049                    "a {:?} at tolerance {} bounds a {:?} at {}, which is tighter",
2050                    node.kind(),
2051                    child_tolerance.get(),
2052                    parent_kind,
2053                    parent.get()
2054                );
2055            }
2056            let below = own.map(|t| (t, node.kind())).or(bound);
2057            for child in node.children() {
2058                stack.push((child.clone(), below));
2059            }
2060        }
2061        Ok(())
2062    }
2063
2064    /// Whether two shapes coincide in position, comparing composed transforms.
2065    ///
2066    /// # Errors
2067    ///
2068    /// As [`Location::composed`](crate::Location::composed).
2069    pub fn same_position(&self, a: &Shape, b: &Shape, tol: Tolerances) -> OgeomResult<bool> {
2070        a.is_same_position(b, &self.datums, tol)
2071    }
2072
2073    /// A shape placed by an additional transform.
2074    ///
2075    /// Interns the transform and composes it onto the shape's placement, so the
2076    /// underlying node (and all its geometry) is shared rather than copied.
2077    /// Placing ten thousand instances of a part costs ten thousand short chains
2078    /// and one copy of the geometry.
2079    ///
2080    /// Records no history: nothing is built, and a sub-shape `s` of the
2081    /// input stands in the result as `s.moved(by)`, where `by` is the
2082    /// result's location with the input's own taken off,
2083    /// `result.location().then(&shape.location().inverted())`. A caller that
2084    /// traces names across the move, or composes it with an operation's
2085    /// history, places with `ogeom_algo::transformed`, which makes the same
2086    /// placement and records every sub-shape into its occurrence.
2087    pub fn placed(&mut self, shape: &Shape, transform: Transform) -> Shape {
2088        let datum = self.add_datum(transform);
2089        shape.moved(&Location::of(datum))
2090    }
2091}
2092
2093/// A box carried by a placement: exactly, where the placement keeps the
2094/// axes; as the box of the carried box otherwise.
2095fn placed_box(kept: &Aabb, placement: &Transform) -> Aabb {
2096    match placement.kind() {
2097        TransformKind::Identity => *kept,
2098        TransformKind::Translation => match (kept.low(), kept.high()) {
2099            (Some(low), Some(high)) => {
2100                Aabb::of_corners(placement.apply(low), placement.apply(high))
2101            }
2102            _ => *kept,
2103        },
2104        _ => kept.transformed(placement),
2105    }
2106}
2107
2108/// Where an attempt began in the journal [`Model::note_widened`] opens.
2109#[derive(Debug, Clone, Copy)]
2110#[must_use]
2111pub struct WidenMark {
2112    from: usize,
2113    opened: bool,
2114}
2115
2116/// What [`Model::unshare_each`] made of the shapes it was given.
2117#[derive(Debug, Clone)]
2118pub struct Unshared {
2119    /// The shapes to work on, in the order given: each as given, or its
2120    /// copy at the same placement and orientation.
2121    pub shapes: Vec<Shape>,
2122    /// Each node copied, with its copy, in the order copied.
2123    pub copies: Vec<(TShapeId, TShapeId)>,
2124}
2125
2126/// What an absorb produced: the transplanted roots, bound to the model that
2127/// absorbed them, and where every absorbed identity ended up.
2128#[derive(Debug)]
2129pub struct Absorbed {
2130    /// The roots handed in, shifted and bound to the absorbing model.
2131    pub shapes: Vec<Shape>,
2132    /// Source-document identity → identity in the absorbing model, for every
2133    /// entity the parts carried. A caller holding references recorded against
2134    /// the source document resolves them through this.
2135    #[allow(
2136        clippy::disallowed_types,
2137        reason = "public field since 0.1; a std map keeps the API"
2138    )]
2139    pub entities: std::collections::HashMap<EntityId, EntityId>,
2140}
2141
2142/// A model's contents, laid out the way a file holds them.
2143///
2144/// Handed to [`Model::from_parts`]. Every list is in arena order, and a node's
2145/// children name other nodes by their position in `nodes`, so the order is
2146/// load-bearing rather than incidental, and a reader has to preserve it.
2147#[derive(Debug, Default)]
2148pub struct ModelParts {
2149    /// The topology nodes.
2150    pub nodes: Vec<TShape>,
2151    /// The placement datums.
2152    pub datums: Vec<crate::location::Datum>,
2153    /// The geometry, already assembled.
2154    pub geometry: GeometryStore,
2155    /// Every entity's provenance, in the order identities were issued: the
2156    /// first entry is `EntityId(1)`.
2157    pub provenance: Vec<Provenance>,
2158    /// Which identity each node carries.
2159    pub identity: Vec<(TShapeId, EntityId)>,
2160    /// The operation the document was left in.
2161    pub current_op: OpId,
2162    /// The unit scale the document was authored at.
2163    pub tolerances: Tolerances,
2164}
2165
2166/// Which sub-shapes a traversal should yield.
2167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2168pub enum Filter {
2169    /// Every shape of one type.
2170    OfType(ShapeType),
2171    /// Every shape, at every level.
2172    All,
2173}
2174
2175/// Walk a shape's tree, composing placement and orientation on descent.
2176///
2177/// Yields each matching sub-shape with its *effective* placement and
2178/// orientation: the composition of everything from the root down. That is the
2179/// only form in which a sub-shape means anything outside the tree it came from.
2180///
2181/// Sub-shapes reached by more than one route (an edge shared by two faces)
2182/// are yielded once per route, since each occurrence has its own orientation
2183/// and that is usually the point. Deduplicate with
2184/// [`SameKey`](crate::SameKey) when it is not.
2185///
2186/// # Errors
2187///
2188/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if any handle fails to
2189/// resolve in `model`.
2190pub fn explore(model: &Model, root: &Shape, filter: Filter) -> OgeomResult<Vec<Shape>> {
2191    let mut out = Vec::new();
2192    let mut stack = vec![root.clone()];
2193    while let Some(shape) = stack.pop() {
2194        // The node is fetched once and answers both questions. `kind_of` would
2195        // repeat the arena lookup that `children_of` is about to do anyway,
2196        // and this walk is the kernel's most travelled road.
2197        let Some(node) = model.node(&shape) else {
2198            ogeom_bail!(Dangling, "shape refers to a node not in this model");
2199        };
2200        let (matches, descend) = match filter {
2201            // Every child of anything but a compound is of a lower type (the
2202            // builders refuse anything else), so below a match, or below a
2203            // shape already lower than the one wanted, nothing can match:
2204            // a walk for faces never enters a wire.
2205            Filter::OfType(want) => (
2206                node.kind() == want,
2207                node.kind() == ShapeType::Compound || node.kind() > want,
2208            ),
2209            Filter::All => (true, true),
2210        };
2211        let children = if descend { node.children() } else { &[] };
2212        stack.reserve(children.len());
2213        // `children_of`'s composition, inline: the parent's placement and
2214        // sense onto each child. Done here so the walk does not allocate a
2215        // `Vec` per node only to drain it.
2216        for child in children.iter().rev() {
2217            stack.push(child.beneath(shape.location(), shape.orientation()));
2218        }
2219        if matches {
2220            // `shape` is owned and finished with; cloning it to keep it would
2221            // copy a location chain for nothing.
2222            out.push(shape);
2223        }
2224    }
2225    Ok(out)
2226}
2227
2228/// Walk a shape's tree, yielding every sub-shape of `want` exactly once.
2229///
2230/// Deduplicated by [`Shape::is_same`] (node and placement, ignoring
2231/// orientation), which is what "the distinct edges of this solid" means.
2232///
2233/// # Errors
2234///
2235/// As [`explore`].
2236pub fn explore_unique(model: &Model, root: &Shape, want: ShapeType) -> OgeomResult<Vec<Shape>> {
2237    use crate::shape::SameKey;
2238
2239    let found = explore(model, root, Filter::OfType(want))?;
2240    let mut seen = ogeom_core::FastSet::with_capacity_and_hasher(found.len(), Default::default());
2241    let mut out = Vec::with_capacity(found.len());
2242    for shape in found {
2243        if seen.insert(SameKey(shape.clone())) {
2244            out.push(shape);
2245        }
2246    }
2247    Ok(out)
2248}
2249
2250/// Every shape in `root` that has `target` among its sub-shapes.
2251///
2252/// The inverse of traversal: "which faces meet at this edge?" is how a boolean
2253/// decides what a split affects, and how a fillet finds what it is blending.
2254///
2255/// # Errors
2256///
2257/// As [`explore`].
2258pub fn ancestors_of(
2259    model: &Model,
2260    root: &Shape,
2261    target: &Shape,
2262    want: ShapeType,
2263) -> OgeomResult<Vec<Shape>> {
2264    // Each candidate is searched for the target's own type only: a walk
2265    // for an edge never enters a vertex list.
2266    let kind = model.kind_of(target)?;
2267    let mut out = Vec::new();
2268    for candidate in explore(model, root, Filter::OfType(want))? {
2269        if explore(model, &candidate, Filter::OfType(kind))?
2270            .iter()
2271            .any(|s| s.is_same(target))
2272        {
2273            out.push(candidate);
2274        }
2275    }
2276    Ok(out)
2277}
2278
2279#[cfg(test)]
2280#[allow(clippy::unwrap_used)]
2281mod tests {
2282    use super::*;
2283    use ogeom_geom::PlaneSurface;
2284    use ogeom_math::{Direction, Frame, Plane, Vector};
2285
2286    const T: Tolerances = Tolerances::millimetres();
2287
2288    /// A single square face: four vertices, four edges, one wire, one face.
2289    fn square(model: &mut Model) -> Shape {
2290        let corners = [
2291            model.add_point(Point::new(0.0, 0.0, 0.0)),
2292            model.add_point(Point::new(1.0, 0.0, 0.0)),
2293            model.add_point(Point::new(1.0, 1.0, 0.0)),
2294            model.add_point(Point::new(0.0, 1.0, 0.0)),
2295        ];
2296        let mut edges = Vec::new();
2297        for i in 0..4 {
2298            let bounds = [corners[i].clone(), corners[(i + 1) % 4].clone()];
2299            edges.push(model.add_edge(EdgeData::new(), &bounds).unwrap());
2300        }
2301        let wire = model.add_wire(&edges).unwrap();
2302        let surface = model
2303            .geometry_mut()
2304            .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
2305        model
2306            .add_face(FaceData::new(surface, Location::identity()), &[wire])
2307            .unwrap()
2308    }
2309
2310    /// Parts describing a two-vertex edge on a line, placed by one datum,
2311    /// with a primitive-and-derived provenance chain: every kind of handle a
2312    /// shift must move, in miniature, exactly as a reader would rebuild them.
2313    fn edge_parts() -> (ModelParts, Vec<Shape>) {
2314        use ogeom_core::Role;
2315
2316        use crate::entity::CurveId;
2317        use crate::location::DatumId;
2318
2319        let mut geometry = GeometryStore::new();
2320        geometry.add_curve(
2321            ogeom_geom::LineCurve::new(ogeom_math::Axis {
2322                location: Point::new(0.0, 0.0, 0.0),
2323                direction: Direction::X,
2324            })
2325            .into(),
2326        );
2327        let ends = [
2328            Shape::of(TShapeId::from_parts(0, 0)),
2329            Shape::of(TShapeId::from_parts(1, 0)).reversed(),
2330        ];
2331        let edge = TShape::new(
2332            ShapeType::Edge,
2333            NodeData::Edge(Box::new(EdgeData::on_curve(
2334                CurveId::from_parts(0, 0),
2335                Location::of(DatumId::from_parts(0, 0)),
2336                (0.0, 2.0),
2337            ))),
2338            ends.to_vec(),
2339        );
2340        let one = EntityId::from_raw(1).unwrap();
2341        let two = EntityId::from_raw(2).unwrap();
2342        let parts = ModelParts {
2343            nodes: vec![
2344                TShape::leaf(
2345                    ShapeType::Vertex,
2346                    NodeData::Vertex(VertexData::new(Point::new(0.0, 0.0, 0.0))),
2347                ),
2348                TShape::leaf(
2349                    ShapeType::Vertex,
2350                    NodeData::Vertex(VertexData::new(Point::new(2.0, 0.0, 0.0))),
2351                ),
2352                edge,
2353            ],
2354            datums: vec![Transform::translation(Vector::new(0.0, 0.0, 1.0))],
2355            geometry,
2356            provenance: vec![
2357                Provenance::Primitive {
2358                    op: OpId(1),
2359                    role: Role::SOLE,
2360                },
2361                Provenance::Derived {
2362                    op: OpId(1),
2363                    from: [one].into_iter().collect(),
2364                    role: Role::SOLE,
2365                },
2366            ],
2367            identity: vec![(TShapeId::from_parts(2, 0), two)],
2368            current_op: OpId(1),
2369            tolerances: T,
2370        };
2371        (parts, vec![Shape::of(TShapeId::from_parts(2, 0))])
2372    }
2373
2374    #[test]
2375    fn absorbing_parts_into_a_live_model_offsets_every_handle() {
2376        let mut model = Model::new();
2377        let face = square(&mut model);
2378
2379        let (parts, roots) = edge_parts();
2380        let absorbed = model.absorb(parts, &roots).unwrap();
2381        assert_eq!(absorbed.shapes.len(), 1);
2382        let edge = &absorbed.shapes[0];
2383
2384        // The absorbed root resolves here and answers as an edge.
2385        assert_eq!(model.kind_of(edge).unwrap(), ShapeType::Edge);
2386        let vertices = explore(&model, edge, Filter::OfType(ShapeType::Vertex)).unwrap();
2387        assert_eq!(vertices.len(), 2);
2388        let points: Vec<Point> = vertices
2389            .iter()
2390            .map(|v| model.node(v).unwrap().data().as_vertex().unwrap().point)
2391            .collect();
2392        assert!(points.contains(&Point::new(2.0, 0.0, 0.0)), "{points:?}");
2393
2394        // Its curve and datum handles were shifted onto this model's arenas.
2395        let node = model.node(edge).unwrap();
2396        let repr = &node.data().as_edge().unwrap().representations[0];
2397        assert!(
2398            model.geometry().holds(repr),
2399            "the absorbed edge's curve did not land"
2400        );
2401        let EdgeRepr::Curve3d { location, .. } = repr else {
2402            panic!("the representation changed kind in the shift");
2403        };
2404        assert!(
2405            location.composed(model.datums()).is_ok(),
2406            "the absorbed edge's datum did not land"
2407        );
2408
2409        // What was here before is untouched.
2410        assert_eq!(model.kind_of(&face).unwrap(), ShapeType::Face);
2411    }
2412
2413    #[test]
2414    fn absorbed_identities_keep_their_provenance_under_new_ids() {
2415        let mut model = Model::new();
2416        square(&mut model);
2417        let issued_before = model.provenance().len() as u64;
2418        assert!(
2419            issued_before > 0,
2420            "the square should have minted identities"
2421        );
2422
2423        let (parts, roots) = edge_parts();
2424        let absorbed = model.absorb(parts, &roots).unwrap();
2425
2426        // The remap table is exactly old-plus-offset.
2427        let old = EntityId::from_raw(2).unwrap();
2428        let new = absorbed.entities[&old];
2429        assert_eq!(new.get(), 2 + issued_before);
2430        assert_eq!(model.identity_of(&absorbed.shapes[0]), Some(new));
2431
2432        // The derived entry still points at its shifted source, and walking
2433        // back lands on the shifted primitive.
2434        let entry = model.provenance().get(new).unwrap();
2435        let source = EntityId::from_raw(1 + issued_before).unwrap();
2436        assert_eq!(entry.inputs(), &[source]);
2437        assert_eq!(model.provenance().roots(new), vec![source]);
2438    }
2439
2440    #[test]
2441    fn absorbing_into_an_empty_model_matches_from_parts() {
2442        let (parts, roots) = edge_parts();
2443        let restored = Model::from_parts(parts).unwrap();
2444        let bound = restored.bind(&roots[0]).unwrap();
2445
2446        let (parts, roots) = edge_parts();
2447        let mut empty = Model::new();
2448        let absorbed = empty.absorb(parts, &roots).unwrap();
2449        let shape = &absorbed.shapes[0];
2450
2451        // Zero offsets: the handles come out where the file put them, and the
2452        // identities are the file's own.
2453        assert_eq!(shape.node().index(), bound.node().index());
2454        assert_eq!(shape.node().generation(), bound.node().generation());
2455        assert_eq!(restored.identity_of(&bound), empty.identity_of(shape));
2456        assert_eq!(restored.provenance().len(), empty.provenance().len());
2457    }
2458
2459    #[test]
2460    fn parts_with_scoped_or_generation_bearing_keys_are_refused() {
2461        let mut model = Model::new();
2462
2463        let (mut parts, roots) = edge_parts();
2464        let child = parts.nodes[2].children()[0].clone();
2465        parts.nodes[2].children_mut()[0] = Shape::new(
2466            child.node().with_scope(7),
2467            Location::identity(),
2468            Orientation::Forward,
2469        );
2470        assert!(
2471            model.absorb(parts, &roots).is_err(),
2472            "a scoped child key should be refused"
2473        );
2474
2475        let (mut parts, roots) = edge_parts();
2476        parts.identity[0].0 = TShapeId::from_parts(2, 1);
2477        assert!(
2478            model.absorb(parts, &roots).is_err(),
2479            "a recycled-generation key should be refused"
2480        );
2481    }
2482
2483    #[test]
2484    fn parts_in_other_units_are_refused() {
2485        let mut model = Model::new();
2486        square(&mut model);
2487        let issued_before = model.provenance().len();
2488
2489        let (mut parts, roots) = edge_parts();
2490        parts.tolerances = Tolerances::metres();
2491        assert!(model.absorb(parts, &roots).is_err());
2492        assert_eq!(
2493            model.provenance().len(),
2494            issued_before,
2495            "a refused absorb should leave the model alone"
2496        );
2497    }
2498
2499    #[test]
2500    fn absorb_leaves_the_current_operation_alone() {
2501        let mut model = Model::new();
2502        model.begin_operation();
2503        model.begin_operation();
2504        let op = model.begin_operation();
2505
2506        let (parts, roots) = edge_parts();
2507        model.absorb(parts, &roots).unwrap();
2508        assert_eq!(model.current_operation(), op);
2509    }
2510
2511    #[test]
2512    fn an_absorbed_root_that_names_a_missing_node_dangles() {
2513        let mut model = Model::new();
2514        let (parts, _) = edge_parts();
2515        let stray = vec![Shape::of(TShapeId::from_parts(99, 0))];
2516        assert!(model.absorb(parts, &stray).is_err());
2517    }
2518
2519    #[test]
2520    fn absorbing_empty_parts_is_a_no_op() {
2521        let mut model = Model::new();
2522        square(&mut model);
2523        let issued_before = model.provenance().len();
2524
2525        let parts = ModelParts {
2526            tolerances: T,
2527            ..ModelParts::default()
2528        };
2529        let absorbed = model.absorb(parts, &[]).unwrap();
2530        assert!(absorbed.shapes.is_empty());
2531        assert!(absorbed.entities.is_empty());
2532        assert_eq!(model.provenance().len(), issued_before);
2533    }
2534
2535    #[test]
2536    fn reversing_a_wire_reverses_the_walk_as_well_as_each_edge() {
2537        // Reversing each edge without reversing the order breaks the chain:
2538        // edge 1 would end where it started while edge 2 still starts where
2539        // it did, so consecutive edges stop meeting and a face built
2540        // on the wire comes apart along its boundary.
2541        let mut model = Model::new();
2542        let face = square(&mut model);
2543        let wire = model.children_of(&face).unwrap()[0].clone();
2544
2545        let forward = model.ordered_children_of(&wire).unwrap();
2546        let backward = model.ordered_children_of(&wire.reversed()).unwrap();
2547
2548        assert_eq!(forward.len(), 4);
2549        assert_eq!(backward.len(), 4);
2550        for (i, edge) in backward.iter().enumerate() {
2551            let partner = &forward[3 - i];
2552            assert!(edge.is_same(partner), "the order did not reverse");
2553            assert_eq!(
2554                edge.orientation(),
2555                Orientation::Reversed.compose(partner.orientation()),
2556                "each edge should also flip"
2557            );
2558        }
2559
2560        // The raw accessor keeps the stored order, which is what a rebuild
2561        // wants and what a traversal must not use.
2562        let raw = model.children_of(&wire.reversed()).unwrap();
2563        assert!(raw[0].is_same(&forward[0]));
2564    }
2565
2566    #[test]
2567    fn a_built_tree_has_the_expected_shape() {
2568        let mut model = Model::new();
2569        let face = square(&mut model);
2570
2571        assert_eq!(model.kind_of(&face).unwrap(), ShapeType::Face);
2572        assert_eq!(
2573            explore_unique(&model, &face, ShapeType::Wire)
2574                .unwrap()
2575                .len(),
2576            1
2577        );
2578        assert_eq!(
2579            explore_unique(&model, &face, ShapeType::Edge)
2580                .unwrap()
2581                .len(),
2582            4
2583        );
2584        assert_eq!(
2585            explore_unique(&model, &face, ShapeType::Vertex)
2586                .unwrap()
2587                .len(),
2588            4
2589        );
2590        // Four edges of two vertices each, but only four distinct vertices:
2591        // consecutive edges share them.
2592        assert_eq!(
2593            explore(&model, &face, Filter::OfType(ShapeType::Vertex))
2594                .unwrap()
2595                .len(),
2596            8
2597        );
2598    }
2599
2600    #[test]
2601    fn children_are_returned_with_the_parents_placement_composed() {
2602        // The invariant traversal exists to guarantee. A vertex reported
2603        // without its parent's placement is a vertex at the wrong point, and
2604        // nothing about the value says so.
2605        let mut model = Model::new();
2606        let face = square(&mut model);
2607        let moved = model.placed(&face, Transform::translation(Vector::new(10.0, 0.0, 0.0)));
2608
2609        let vertices = explore_unique(&model, &moved, ShapeType::Vertex).unwrap();
2610        assert_eq!(vertices.len(), 4);
2611        for v in &vertices {
2612            let node = model.node(v).unwrap();
2613            let local = node.data().as_vertex().unwrap().point;
2614            let world = v.transform(model.datums()).unwrap().apply(local);
2615            assert!(world.x >= 10.0 - 1e-12, "vertex at {world:?} was not moved");
2616        }
2617    }
2618
2619    #[test]
2620    fn children_are_returned_with_the_parents_orientation_composed() {
2621        let mut model = Model::new();
2622        let face = square(&mut model);
2623        let reversed = face.reversed();
2624
2625        let forward_edges = model
2626            .children_of(&model.children_of(&face).unwrap()[0])
2627            .unwrap();
2628        let reversed_edges = model
2629            .children_of(&model.children_of(&reversed).unwrap()[0])
2630            .unwrap();
2631
2632        for (a, b) in forward_edges.iter().zip(&reversed_edges) {
2633            assert_eq!(
2634                b.orientation(),
2635                a.orientation().reversed(),
2636                "reversing a face must reverse what its edges present, \
2637                 without touching a single stored child"
2638            );
2639        }
2640    }
2641
2642    #[test]
2643    fn reversing_a_shape_touches_no_stored_child() {
2644        // The point of composing on descent: the reversal lives entirely in the
2645        // handle, so a shared sub-tree is not disturbed for other users of it.
2646        let mut model = Model::new();
2647        let face = square(&mut model);
2648        let before = model.node(&face).unwrap().clone();
2649        let _ = face.reversed();
2650        assert_eq!(model.node(&face).unwrap(), &before);
2651    }
2652
2653    #[test]
2654    fn placement_composes_through_nesting() {
2655        let mut model = Model::new();
2656        let vertex = model.add_point(Point::new(1.0, 0.0, 0.0));
2657        let edge = model
2658            .add_edge(EdgeData::new(), &[vertex.clone(), vertex.clone()])
2659            .unwrap();
2660        let moved_edge = model.placed(&edge, Transform::translation(Vector::new(10.0, 0.0, 0.0)));
2661        let compound = model.add_compound(&[moved_edge]).unwrap();
2662        let moved_compound = model.placed(
2663            &compound,
2664            Transform::translation(Vector::new(100.0, 0.0, 0.0)),
2665        );
2666
2667        let found = explore_unique(&model, &moved_compound, ShapeType::Vertex).unwrap();
2668        assert_eq!(found.len(), 1);
2669        let local = model
2670            .node(&found[0])
2671            .unwrap()
2672            .data()
2673            .as_vertex()
2674            .unwrap()
2675            .point;
2676        let world = found[0].transform(model.datums()).unwrap().apply(local);
2677        assert!(
2678            world.is_equal(Point::new(111.0, 0.0, 0.0), T),
2679            "expected 1 + 10 + 100, got {world:?}"
2680        );
2681    }
2682
2683    #[test]
2684    fn the_builder_refuses_children_of_the_wrong_type() {
2685        let mut model = Model::new();
2686        let vertex = model.add_point(Point::ORIGIN);
2687        let edge = model
2688            .add_edge(EdgeData::new(), std::slice::from_ref(&vertex))
2689            .unwrap();
2690
2691        assert!(
2692            model.add_wire(std::slice::from_ref(&vertex)).is_err(),
2693            "a wire holds edges"
2694        );
2695        assert!(
2696            model.add_shell(std::slice::from_ref(&edge)).is_err(),
2697            "a shell holds faces"
2698        );
2699        assert!(
2700            model.add_solid(std::slice::from_ref(&edge)).is_err(),
2701            "a solid holds shells"
2702        );
2703        assert!(model.add_wire(&[edge]).is_ok());
2704
2705        // A compound is the exception, and deliberately so.
2706        assert!(model.add_compound(&[vertex]).is_ok());
2707    }
2708
2709    #[test]
2710    fn empty_containers_are_refused_except_a_compound() {
2711        let mut model = Model::new();
2712        assert!(model.add_wire(&[]).is_err());
2713        assert!(model.add_shell(&[]).is_err());
2714        assert!(model.add_solid(&[]).is_err());
2715        assert!(model.add_compsolid(&[]).is_err());
2716        // An empty result is a legitimate answer from a boolean and needs
2717        // somewhere to live.
2718        assert!(model.add_compound(&[]).is_ok());
2719    }
2720
2721    #[test]
2722    fn an_edge_takes_at_most_two_vertices() {
2723        let mut model = Model::new();
2724        let v = model.add_point(Point::ORIGIN);
2725        assert!(model.add_edge(EdgeData::new(), &[]).is_ok(), "unbounded");
2726        assert!(
2727            model
2728                .add_edge(EdgeData::new(), std::slice::from_ref(&v))
2729                .is_ok()
2730        );
2731        assert!(
2732            model
2733                .add_edge(EdgeData::new(), &[v.clone(), v.clone()])
2734                .is_ok()
2735        );
2736        assert!(
2737            model
2738                .add_edge(EdgeData::new(), &[v.clone(), v.clone(), v])
2739                .is_err(),
2740            "three ends is not an edge"
2741        );
2742    }
2743
2744    #[test]
2745    fn building_enforces_the_containment_rule_upward() {
2746        // A coarse edge must not be capped by a finer vertex. Rather than
2747        // refusing, the builder widens the vertex; tolerances only ever grow,
2748        // so the repair goes upward.
2749        let mut model = Model::new();
2750        let vertex = model.add_point(Point::ORIGIN);
2751        assert_eq!(model.tolerance_of(&vertex).unwrap(), Some(Tolerance::MIN));
2752
2753        let mut edge_data = EdgeData::new();
2754        edge_data.widen(Tolerance::new(1e-3).unwrap());
2755        let edge = model
2756            .add_edge(edge_data, std::slice::from_ref(&vertex))
2757            .unwrap();
2758
2759        assert_eq!(
2760            model.tolerance_of(&vertex).unwrap(),
2761            Some(Tolerance::new(1e-3).unwrap()),
2762            "the vertex was widened to contain its edge"
2763        );
2764        assert!(model.check_tolerances(&edge).is_ok());
2765    }
2766
2767    #[test]
2768    fn a_kept_face_box_is_forgotten_when_what_the_face_is_made_of_changes() {
2769        let unit = Aabb::of_corners(Point::ORIGIN, Point::new(1.0, 1.0, 0.0));
2770        let keep = |model: &Model, face: &Shape| {
2771            model.kept_face_box(face, |_| Ok(unit)).unwrap();
2772        };
2773        let mut model = Model::new();
2774        let face = square(&mut model);
2775        assert_eq!(model.face_bounds(&face), None);
2776        keep(&model, &face);
2777        let kept = model.face_bounds(&face).unwrap();
2778        assert!(kept.contains_box(&unit));
2779
2780        // A placement carries the box; a tolerance widens it.
2781        let lift = model.placed(&face, Transform::translation(ogeom_math::Vector::Z));
2782        let lifted = model.face_bounds(&lift).unwrap();
2783        assert!((lifted.low().unwrap().z - 1.0).abs() < 1e-6);
2784        model.widen(&face, Tolerance::new(0.25).unwrap()).unwrap();
2785        let wide = model.face_bounds(&face).unwrap();
2786        assert!((wide.low().unwrap().x + 0.25).abs() < 1e-12);
2787
2788        // A vertex handed out for editing forgets the box of the face above.
2789        let vertex = explore_unique(&model, &face, ShapeType::Vertex).unwrap()[0].clone();
2790        let _ = model.node_mut(&vertex);
2791        assert_eq!(model.face_bounds(&face), None);
2792
2793        // So does a face handed out itself, and a surface rewritten in place,
2794        // which reaches every face.
2795        keep(&model, &face);
2796        let _ = model.node_mut(&face);
2797        assert_eq!(model.face_bounds(&face), None);
2798        keep(&model, &face);
2799        let Some(NodeData::Face(data)) = model.node(&face).map(TShape::data) else {
2800            panic!("a face holds face data");
2801        };
2802        let surface = data.surface;
2803        let _ = model.geometry_mut().surface_mut(surface);
2804        assert_eq!(model.face_bounds(&face), None);
2805        let _ = model.add_point(Point::ORIGIN);
2806        keep(&model, &face);
2807        assert!(model.face_bounds(&face).is_some());
2808
2809        // A node no face holds forgets nothing.
2810        let alone = model.add_point(Point::ORIGIN);
2811        let _ = model.node_mut(&alone);
2812        assert!(model.face_bounds(&face).is_some());
2813    }
2814
2815    /// The node indices below `shape` and the loosest tolerance among them,
2816    /// read through [`Model::kept_read`]; and whether `read` was asked.
2817    fn kept_walk(model: &Model, shape: &Shape) -> (Vec<u32>, f64, bool) {
2818        let asked = std::cell::Cell::new(false);
2819        let kept = model
2820            .kept_read(
2821                shape,
2822                |_: &(Vec<u32>, f64)| true,
2823                || {
2824                    asked.set(true);
2825                    let set = model.indices_below(shape);
2826                    let mut below: Vec<u32> = model
2827                        .nodes
2828                        .iter()
2829                        .map(|(id, _)| id.index())
2830                        .filter(|&i| set.contains(i))
2831                        .collect();
2832                    below.sort_unstable();
2833                    let loosest = below
2834                        .iter()
2835                        .filter_map(|&i| model.nodes.at(i))
2836                        .filter_map(|id| model.node_by_id(id)?.data().tolerance())
2837                        .fold(0.0, |m: f64, t| m.max(t.get()));
2838                    Ok(((below, loosest), true))
2839                },
2840            )
2841            .unwrap();
2842        (kept.0.clone(), kept.1, asked.get())
2843    }
2844
2845    #[test]
2846    fn a_kept_read_is_read_again_after_any_edit_below_its_shape() {
2847        let mut model = Model::new();
2848        let face = square(&mut model);
2849        let read = |model: &Model| kept_walk(model, &face);
2850        let first = read(&model);
2851        assert!(first.2);
2852        // Read back while nothing below changes.
2853        assert_eq!(read(&model), (first.0.clone(), first.1, false));
2854        let alone = model.add_point(Point::ORIGIN);
2855        let _ = model.node_mut(&alone);
2856        model.widen(&alone, Tolerance::new(1.0).unwrap()).unwrap();
2857        assert!(!read(&model).2);
2858
2859        // A node below handed out for editing.
2860        let vertex = explore_unique(&model, &face, ShapeType::Vertex).unwrap()[0].clone();
2861        if let Some(node) = model.node_mut(&vertex)
2862            && let NodeData::Vertex(data) = node.data_mut()
2863        {
2864            data.tolerance = Tolerance::new(0.5).unwrap();
2865        }
2866        assert_eq!(read(&model).1, 0.5);
2867        assert!(!read(&model).2);
2868
2869        // A tolerance grown, and one already as wide.
2870        model.widen(&vertex, Tolerance::new(0.75).unwrap()).unwrap();
2871        assert_eq!(read(&model).1, 0.75);
2872        model.widen(&vertex, Tolerance::new(0.25).unwrap()).unwrap();
2873        assert!(!read(&model).2);
2874
2875        // A widening put back.
2876        let mark = model.note_widened();
2877        model.widen(&face, Tolerance::new(0.9).unwrap()).unwrap();
2878        assert_eq!(read(&model).1, 0.9);
2879        model.undo_widened(mark);
2880        assert_eq!(read(&model), (first.0.clone(), 0.75, true));
2881
2882        // A surface or a pcurve rewritten in place.
2883        let Some(NodeData::Face(data)) = model.node(&face).map(TShape::data) else {
2884            panic!("a face holds face data");
2885        };
2886        let surface = data.surface;
2887        let _ = model.geometry_mut().surface_mut(surface);
2888        assert!(read(&model).2);
2889        assert!(!read(&model).2);
2890        let line = ogeom_geom::PlanarCurve::Line(
2891            ogeom_geom::Line2d::segment(
2892                ogeom_math::Point2::new(0.0, 0.0),
2893                ogeom_math::Point2::new(1.0, 0.0),
2894                T,
2895            )
2896            .unwrap(),
2897        );
2898        let pcurve = model.geometry_mut().add_pcurve(line);
2899        let _ = model.geometry_mut().pcurve_mut(pcurve);
2900        assert!(read(&model).2);
2901
2902        // A clone reads back what was kept, and forgets it on its own.
2903        let mut copy = model.clone();
2904        assert!(!kept_walk(&copy, &face).2);
2905        let _ = copy.node_mut(&vertex);
2906        assert!(kept_walk(&copy, &face).2);
2907        assert!(!read(&model).2);
2908
2909        // Dropping what nothing reaches keeps what the shape read.
2910        model.retain_reachable(std::slice::from_ref(&face)).unwrap();
2911        assert!(!read(&model).2);
2912    }
2913
2914    #[test]
2915    fn a_kept_read_is_read_again_where_a_child_is_pointed_at_a_copy() {
2916        let mut model = Model::new();
2917        let face = square(&mut model);
2918        let since = model.node_count();
2919        let shell = model.add_shell(std::slice::from_ref(&face)).unwrap();
2920        model.note_held(&shell, since).unwrap();
2921        let before = kept_walk(&model, &shell);
2922        assert!(before.2);
2923        assert!(!kept_walk(&model, &shell).2);
2924        let copies = model.unshare(&shell).unwrap();
2925        assert!(!copies.is_empty());
2926        let after = kept_walk(&model, &shell);
2927        assert!(after.2);
2928        assert_ne!(after.0, before.0);
2929    }
2930
2931    /// A read kept for a shape holding a shape already kept takes the
2932    /// nodes kept below that one without walking them, and is read again
2933    /// after an edit below either part.
2934    #[test]
2935    fn a_kept_read_beside_another_is_read_again_after_an_edit_below_any_part() {
2936        let mut built = Model::new();
2937        let face = square(&mut built);
2938        let other = square(&mut built);
2939        let shell = built.add_shell(&[face.clone(), other.clone()]).unwrap();
2940        for edited in [&face, &other] {
2941            let mut model = built.clone();
2942            assert!(kept_walk(&model, &face).2);
2943            assert!(kept_walk(&model, &shell).2);
2944            assert!(!kept_walk(&model, &shell).2);
2945            let edits = model.geometry.edits();
2946            let kept = model.nodes.reads.below(&shell, edits).unwrap();
2947            let walked = model.indices_below(&shell);
2948            let all: Vec<u32> = model.nodes.iter().map(|(id, _)| id.index()).collect();
2949            assert!(all.iter().all(|&i| !walked.contains(i) || kept.contains(i)));
2950            let vertex = explore_unique(&model, edited, ShapeType::Vertex).unwrap()[0].clone();
2951            let _ = model.node_mut(&vertex);
2952            assert!(kept_walk(&model, &shell).2);
2953        }
2954    }
2955
2956    #[test]
2957    fn a_face_widens_the_edges_it_borders() {
2958        let mut model = Model::new();
2959        let a = model.add_point(Point::ORIGIN);
2960        let b = model.add_point(Point::new(1.0, 0.0, 0.0));
2961        let edge = model.add_edge(EdgeData::new(), &[a.clone(), b]).unwrap();
2962        let wire = model.add_wire(std::slice::from_ref(&edge)).unwrap();
2963
2964        let surface = model
2965            .geometry_mut()
2966            .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
2967        let mut face_data = FaceData::new(surface, Location::identity());
2968        face_data.widen(Tolerance::new(1e-2).unwrap());
2969        let face = model.add_face(face_data, &[wire]).unwrap();
2970
2971        assert_eq!(
2972            model.tolerance_of(&edge).unwrap(),
2973            Some(Tolerance::new(1e-2).unwrap())
2974        );
2975        // And the cascade reached the vertices under those edges. Stopping one
2976        // level down would leave the rule broken where nothing looks.
2977        assert_eq!(
2978            model.tolerance_of(&a).unwrap(),
2979            Some(Tolerance::new(1e-2).unwrap()),
2980            "widening a face must reach its edges' vertices, not just its edges"
2981        );
2982        assert!(model.check_tolerances(&face).is_ok());
2983    }
2984
2985    #[test]
2986    fn check_tolerances_catches_a_violation_the_builder_would_never_make() {
2987        // The builder maintains the rule, so a violation has to be assembled
2988        // around it, which is exactly what happens when topology arrives from
2989        // a file. The check has to stand on its own, or imported geometry sails
2990        // past it.
2991        let mut model = Model::new();
2992        let vertex = Shape::of(model.insert_node(TShape::leaf(
2993            ShapeType::Vertex,
2994            NodeData::Vertex(VertexData::new(Point::ORIGIN)),
2995        )));
2996
2997        let mut edge_data = EdgeData::new();
2998        edge_data.widen(Tolerance::new(1e-1).unwrap());
2999        let edge = Shape::of(model.insert_node(TShape::new(
3000            ShapeType::Edge,
3001            NodeData::Edge(Box::new(edge_data)),
3002            vec![vertex.clone()],
3003        )));
3004
3005        let err = model.check_tolerances(&edge).unwrap_err();
3006        assert!(
3007            err.to_string().contains("tighter"),
3008            "unexpected message: {err}"
3009        );
3010
3011        // And the sanctioned repair fixes it, cascading to the vertex.
3012        model.widen(&edge, Tolerance::new(1e-1).unwrap()).unwrap();
3013        assert!(model.check_tolerances(&edge).is_ok());
3014        assert_eq!(
3015            model.tolerance_of(&vertex).unwrap(),
3016            Some(Tolerance::new(1e-1).unwrap())
3017        );
3018    }
3019
3020    /// A face looser than its edges breaks the rule across the wire between
3021    /// them, which carries no tolerance of its own to be compared with.
3022    #[test]
3023    fn check_tolerances_holds_a_face_to_its_edges_across_the_wire() {
3024        let mut model = Model::new();
3025        let vertex = Shape::of(model.insert_node(TShape::leaf(
3026            ShapeType::Vertex,
3027            NodeData::Vertex(VertexData::new(Point::ORIGIN)),
3028        )));
3029        let edge = Shape::of(model.insert_node(TShape::new(
3030            ShapeType::Edge,
3031            NodeData::Edge(Box::default()),
3032            vec![vertex],
3033        )));
3034        let wire =
3035            Shape::of(model.insert_node(TShape::container(ShapeType::Wire, vec![edge.clone()])));
3036        let surface = model
3037            .geometry_mut()
3038            .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3039        let mut face_data = FaceData::new(surface, Location::identity());
3040        face_data.widen(Tolerance::new(1e-2).unwrap());
3041        let face = Shape::of(model.insert_node(TShape::new(
3042            ShapeType::Face,
3043            NodeData::Face(Box::new(face_data)),
3044            vec![wire],
3045        )));
3046        let err = model.check_tolerances(&face).unwrap_err();
3047        assert!(err.to_string().contains("tighter"), "{err}");
3048        model.widen(&face, Tolerance::new(1e-2).unwrap()).unwrap();
3049        assert!(model.check_tolerances(&face).is_ok());
3050    }
3051
3052    #[test]
3053    fn a_face_with_no_wires_is_naturally_restricted() {
3054        let mut model = Model::new();
3055        let surface = model
3056            .geometry_mut()
3057            .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3058        let face = model
3059            .add_face(FaceData::new(surface, Location::identity()), &[])
3060            .unwrap();
3061        assert!(
3062            model
3063                .node(&face)
3064                .unwrap()
3065                .data()
3066                .as_face()
3067                .unwrap()
3068                .natural_restriction,
3069            "an untrimmed face needs no point-in-face test at all"
3070        );
3071    }
3072
3073    #[test]
3074    fn a_shared_sub_shape_is_yielded_once_per_route_and_deduplicated_on_request() {
3075        // Two faces meeting at an edge. Each occurrence carries its own
3076        // orientation, which is usually the point; asking for distinct edges is
3077        // a separate question.
3078        let mut model = Model::new();
3079        let a = model.add_point(Point::ORIGIN);
3080        let b = model.add_point(Point::new(1.0, 0.0, 0.0));
3081        let shared = model.add_edge(EdgeData::new(), &[a, b]).unwrap();
3082
3083        let wire_one = model.add_wire(std::slice::from_ref(&shared)).unwrap();
3084        let wire_two = model.add_wire(&[shared.reversed()]).unwrap();
3085        let surface = model
3086            .geometry_mut()
3087            .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3088        let face_one = model
3089            .add_face(FaceData::new(surface, Location::identity()), &[wire_one])
3090            .unwrap();
3091        let face_two = model
3092            .add_face(FaceData::new(surface, Location::identity()), &[wire_two])
3093            .unwrap();
3094        let shell = model.add_shell(&[face_one, face_two]).unwrap();
3095
3096        let all = explore(&model, &shell, Filter::OfType(ShapeType::Edge)).unwrap();
3097        assert_eq!(all.len(), 2, "one occurrence per route");
3098        assert_ne!(all[0].orientation(), all[1].orientation());
3099
3100        let distinct = explore_unique(&model, &shell, ShapeType::Edge).unwrap();
3101        assert_eq!(distinct.len(), 1, "one edge, seen from two sides");
3102    }
3103
3104    #[test]
3105    fn ancestors_answers_which_faces_meet_at_an_edge() {
3106        let mut model = Model::new();
3107        let a = model.add_point(Point::ORIGIN);
3108        let b = model.add_point(Point::new(1.0, 0.0, 0.0));
3109        let shared = model.add_edge(EdgeData::new(), &[a, b]).unwrap();
3110        let isolated = model.add_point(Point::new(5.0, 5.0, 5.0));
3111        let lone = model.add_edge(EdgeData::new(), &[isolated]).unwrap();
3112
3113        let surface = model
3114            .geometry_mut()
3115            .add_surface(PlaneSurface::new(Plane::new(Frame::WORLD)).into());
3116        let mut faces = Vec::new();
3117        for _ in 0..2 {
3118            let wire = model.add_wire(std::slice::from_ref(&shared)).unwrap();
3119            faces.push(
3120                model
3121                    .add_face(FaceData::new(surface, Location::identity()), &[wire])
3122                    .unwrap(),
3123            );
3124        }
3125        let third_wire = model.add_wire(std::slice::from_ref(&lone)).unwrap();
3126        faces.push(
3127            model
3128                .add_face(FaceData::new(surface, Location::identity()), &[third_wire])
3129                .unwrap(),
3130        );
3131        let shell = model.add_shell(&faces).unwrap();
3132
3133        let meeting = ancestors_of(&model, &shell, &shared, ShapeType::Face).unwrap();
3134        assert_eq!(meeting.len(), 2, "two faces meet at the shared edge");
3135        let alone = ancestors_of(&model, &shell, &lone, ShapeType::Face).unwrap();
3136        assert_eq!(alone.len(), 1);
3137    }
3138
3139    #[test]
3140    fn handles_from_another_model_are_reported_rather_than_resolved() {
3141        let mut model = Model::new();
3142        let mut other = Model::new();
3143        // Past the end of `model`, so it genuinely fails to resolve.
3144        let mut foreign = other.add_point(Point::ORIGIN);
3145        for _ in 0..5 {
3146            foreign = other.add_point(Point::ORIGIN);
3147        }
3148        assert!(model.kind_of(&foreign).is_err());
3149        assert!(model.children_of(&foreign).is_err());
3150        assert!(model.add_wire(std::slice::from_ref(&foreign)).is_err());
3151        assert!(model.add_compound(&[foreign]).is_err());
3152    }
3153
3154    #[test]
3155    fn placing_a_shape_shares_its_geometry_rather_than_copying_it() {
3156        // Ten thousand fasteners cost ten thousand short chains and one copy of
3157        // the geometry. That is the whole reason placement is a chain.
3158        let mut model = Model::new();
3159        let face = square(&mut model);
3160        let before = model.node_count();
3161
3162        let mut instances = Vec::new();
3163        for i in 0..100 {
3164            instances.push(model.placed(
3165                &face,
3166                Transform::translation(Vector::new(f64::from(i), 0.0, 0.0)),
3167            ));
3168        }
3169        assert_eq!(model.node_count(), before, "no topology was duplicated");
3170        assert!(instances.iter().all(|s| s.is_partner(&face)));
3171        assert!(instances.iter().all(|s| !s.is_same(&face)));
3172
3173        // And they are all in different places.
3174        let a = instances[0].transform(model.datums()).unwrap();
3175        let b = instances[99].transform(model.datums()).unwrap();
3176        assert!(!a.is_equal(&b, T));
3177    }
3178
3179    #[test]
3180    fn what_a_failed_attempt_widened_is_put_back() {
3181        let mut model = Model::new();
3182        let tolerance =
3183            |model: &Model, v: &Shape| model.node(v).unwrap().data().tolerance().unwrap().get();
3184        let v = model.add_point(Point::new(0.0, 0.0, 0.0));
3185        let w = model.add_point(Point::new(1.0, 0.0, 0.0));
3186        let was = tolerance(&model, &v);
3187        let outer = model.note_widened();
3188        model.widen(&v, Tolerance::new(0.5).unwrap()).unwrap();
3189        // An attempt within another puts back only its own.
3190        let inner = model.note_widened();
3191        model.widen(&v, Tolerance::new(2.0).unwrap()).unwrap();
3192        model.widen(&w, Tolerance::new(3.0).unwrap()).unwrap();
3193        model.undo_widened(inner);
3194        assert_eq!(tolerance(&model, &v), 0.5);
3195        assert_eq!(tolerance(&model, &w), was);
3196        model.undo_widened(outer);
3197        assert_eq!(tolerance(&model, &v), was);
3198        // An edit through the node itself is put back as well.
3199        let edit = model.note_widened();
3200        if let Some(node) = model.node_mut(&w)
3201            && let NodeData::Vertex(data) = node.data_mut()
3202        {
3203            data.tolerance = Tolerance::new(4.0).unwrap();
3204        }
3205        model.undo_widened(edit);
3206        assert_eq!(tolerance(&model, &w), was);
3207        // What an attempt that holds keeps stays, and nothing is noted after.
3208        let kept = model.note_widened();
3209        model.widen(&v, Tolerance::new(0.25).unwrap()).unwrap();
3210        model.keep_widened(kept);
3211        let after = model.note_widened();
3212        model.undo_widened(after);
3213        assert_eq!(tolerance(&model, &v), 0.25);
3214    }
3215
3216    #[test]
3217    fn an_empty_model_reports_itself_as_empty() {
3218        let model = Model::new();
3219        assert!(model.is_empty());
3220        assert_eq!(model.node_count(), 0);
3221        assert_eq!(model.geometry().counts(), (0, 0, 0));
3222        assert!(model.datums().is_empty());
3223    }
3224
3225    #[test]
3226    fn a_vertex_has_no_children_and_traversal_stops_there() {
3227        let mut model = Model::new();
3228        let v = model.add_point(Point::new(1.0, 2.0, 3.0));
3229        assert!(model.children_of(&v).unwrap().is_empty());
3230        assert_eq!(explore(&model, &v, Filter::All).unwrap().len(), 1);
3231        assert!(model.check_tolerances(&v).is_ok());
3232    }
3233
3234    #[test]
3235    fn shapes_at_different_places_are_not_the_same_position() {
3236        let mut model = Model::new();
3237        let v = model.add_point(Point::ORIGIN);
3238        let moved = model.placed(&v, Transform::translation(Vector::X));
3239        assert!(!model.same_position(&v, &moved, T).unwrap());
3240        assert!(model.same_position(&v, &v.clone(), T).unwrap());
3241
3242        // The same displacement reached twice is the same position, even
3243        // through two different datums.
3244        let again = model.placed(&v, Transform::translation(Vector::X));
3245        assert!(!moved.is_same(&again), "structurally different chains");
3246        assert!(model.same_position(&moved, &again, T).unwrap());
3247    }
3248
3249    #[test]
3250    fn a_direction_is_needed_to_build_a_non_trivial_plane() {
3251        // Guards the test helper itself: a face built on a degenerate plane
3252        // would make every other assertion here meaningless.
3253        let mut model = Model::new();
3254        let surface = model
3255            .geometry_mut()
3256            .add_surface(PlaneSurface::new(Plane::through(Point::ORIGIN, Direction::Z)).into());
3257        assert!(model.geometry().surface(surface).is_some());
3258    }
3259}