Skip to main content

cadforge_core/
model.rs

1//! The model store.
2//!
3//! Holds elements and their relationships, applies commands, and maintains the revision and
4//! undo history. Validation happens before mutation, so a rejected command leaves the model
5//! untouched.
6
7use crate::command::{CommandError, CommandOutcome, ModelCommand};
8use crate::element::{BoundingBox, ElementRecord, IfcClass};
9use crate::id::GlobalId;
10use crate::spatial::{IndexedElement, SpatialIndex};
11use std::collections::{BTreeMap, BTreeSet};
12
13const REL_VOIDS: &str = "IfcRelVoidsElement";
14const REL_FILLS: &str = "IfcRelFillsElement";
15
16/// One entry in the audit trail.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Revision {
19    pub number: u64,
20    pub command: ModelCommand,
21    pub changed: Vec<GlobalId>,
22}
23
24/// The semantic model.
25///
26/// `BTreeMap`/`BTreeSet` throughout, not hash containers: iteration order must be stable so
27/// that export from a given revision is reproducible (`docs/ifc-semantics.md` §7.2).
28#[derive(Debug, Clone, Default)]
29pub struct Model {
30    elements: BTreeMap<GlobalId, ElementRecord>,
31    /// `(host, opening)`.
32    voids: BTreeSet<(GlobalId, GlobalId)>,
33    /// `(opening, filler)`.
34    fills: BTreeSet<(GlobalId, GlobalId)>,
35    revision: u64,
36    history: Vec<Revision>,
37    undo_stack: Vec<ModelCommand>,
38    redo_stack: Vec<ModelCommand>,
39}
40
41impl Model {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    // ---- queries -------------------------------------------------------------------
47
48    pub fn get(&self, id: &GlobalId) -> Option<&ElementRecord> {
49        self.elements.get(id)
50    }
51
52    pub fn contains(&self, id: &GlobalId) -> bool {
53        self.elements.contains_key(id)
54    }
55
56    pub fn len(&self) -> usize {
57        self.elements.len()
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.elements.is_empty()
62    }
63
64    pub fn iter(&self) -> impl Iterator<Item = &ElementRecord> {
65        self.elements.values()
66    }
67
68    pub fn revision(&self) -> u64 {
69        self.revision
70    }
71
72    pub fn history(&self) -> &[Revision] {
73        &self.history
74    }
75
76    pub fn can_undo(&self) -> bool {
77        !self.undo_stack.is_empty()
78    }
79
80    pub fn can_redo(&self) -> bool {
81        !self.redo_stack.is_empty()
82    }
83
84    /// Elements of a class, in stable order.
85    pub fn by_class<'a>(&'a self, class: &'a IfcClass) -> impl Iterator<Item = &'a ElementRecord> {
86        self.elements.values().filter(move |e| &e.class == class)
87    }
88
89    /// Elements contained in a spatial structure element.
90    pub fn contained_in<'a>(
91        &'a self,
92        container: &'a GlobalId,
93    ) -> impl Iterator<Item = &'a ElementRecord> {
94        self.elements
95            .values()
96            .filter(move |e| e.container.as_ref() == Some(container))
97    }
98
99    /// Every `(host, opening)` pair, in stable order.
100    ///
101    /// Prefer this over calling [`Model::openings_of`] in a loop: that is a scan per call, so
102    /// walking every element with it is quadratic. Exporting a 20k-wall model took 5.9 s that
103    /// way and 0.9 s this way.
104    pub fn voids(&self) -> impl Iterator<Item = (&GlobalId, &GlobalId)> {
105        self.voids.iter().map(|(h, o)| (h, o))
106    }
107
108    /// Every `(opening, filler)` pair, in stable order. See [`Model::voids`].
109    pub fn fills(&self) -> impl Iterator<Item = (&GlobalId, &GlobalId)> {
110        self.fills.iter().map(|(o, f)| (o, f))
111    }
112
113    /// Openings cutting a host (`IfcRelVoidsElement`).
114    ///
115    /// **O(number of voids in the model)** — it scans. Fine for answering a question about
116    /// one element, wrong inside a loop over all of them; use [`Model::voids`] there. If this
117    /// ever shows up in a profile for single-element queries, the fix is a secondary index
118    /// keyed by host, not a change at the call site.
119    pub fn openings_of<'a>(&'a self, host: &'a GlobalId) -> impl Iterator<Item = &'a GlobalId> {
120        self.voids
121            .iter()
122            .filter(move |(h, _)| h == host)
123            .map(|(_, o)| o)
124    }
125
126    /// Elements filling an opening (`IfcRelFillsElement`).
127    pub fn fills_of<'a>(&'a self, opening: &'a GlobalId) -> impl Iterator<Item = &'a GlobalId> {
128        self.fills
129            .iter()
130            .filter(move |(o, _)| o == opening)
131            .map(|(_, f)| f)
132    }
133
134    /// The host an opening cuts, if any.
135    pub fn host_of(&self, opening: &GlobalId) -> Option<&GlobalId> {
136        self.voids
137            .iter()
138            .find(|(_, o)| o == opening)
139            .map(|(h, _)| h)
140    }
141
142    /// World bounds of everything with evaluated geometry.
143    pub fn bounds(&self) -> BoundingBox {
144        self.elements
145            .values()
146            .filter_map(|e| e.bounds)
147            .fold(BoundingBox::empty(), BoundingBox::union)
148    }
149
150    /// Build a spatial index over elements that have bounds.
151    ///
152    /// Rebuilt rather than maintained incrementally: bulk loading an R-tree is fast, and a
153    /// stale index is a far worse failure than a rebuild (`docs/ifc-semantics.md` §4.2).
154    pub fn spatial_index(&self) -> SpatialIndex {
155        SpatialIndex::build(self.elements.values().filter_map(|e| {
156            e.bounds.map(|b| IndexedElement {
157                global_id: e.global_id.clone(),
158                bounds: b,
159            })
160        }))
161    }
162
163    /// Attach evaluated geometry bounds. Not a command — bounds are a derived cache, and
164    /// caches do not belong in the audit trail (`docs/ifc-semantics.md` ADR-001).
165    pub fn set_bounds(&mut self, id: &GlobalId, bounds: Option<BoundingBox>) -> bool {
166        match self.elements.get_mut(id) {
167            Some(e) => {
168                e.bounds = bounds;
169                true
170            }
171            None => false,
172        }
173    }
174
175    // ---- mutation ------------------------------------------------------------------
176
177    /// Apply a command, recording it for undo.
178    pub fn apply(&mut self, command: ModelCommand) -> Result<CommandOutcome, CommandError> {
179        let outcome = self.execute(command.clone())?;
180        self.undo_stack.push(outcome.inverse.clone());
181        self.redo_stack.clear();
182        self.history.push(Revision {
183            number: outcome.revision,
184            command,
185            changed: outcome.changed.clone(),
186        });
187        Ok(outcome)
188    }
189
190    /// Apply a batch, stopping at the first failure.
191    ///
192    /// Note this is *not* transactional — earlier commands stay applied. Callers wanting
193    /// all-or-nothing should undo back to the starting revision.
194    pub fn apply_all(
195        &mut self,
196        commands: impl IntoIterator<Item = ModelCommand>,
197    ) -> Result<Vec<CommandOutcome>, CommandError> {
198        commands.into_iter().map(|c| self.apply(c)).collect()
199    }
200
201    pub fn undo(&mut self) -> Result<CommandOutcome, CommandError> {
202        let inverse = self.undo_stack.pop().ok_or(CommandError::NothingToUndo)?;
203        match self.execute(inverse.clone()) {
204            Ok(outcome) => {
205                self.redo_stack.push(outcome.inverse.clone());
206                self.history.push(Revision {
207                    number: outcome.revision,
208                    command: inverse,
209                    changed: outcome.changed.clone(),
210                });
211                Ok(outcome)
212            }
213            Err(e) => {
214                // Undo must never lose a step. If the inverse was rejected the model has a
215                // consistency bug, but the stack stays intact so it can be inspected.
216                self.undo_stack.push(inverse);
217                Err(e)
218            }
219        }
220    }
221
222    pub fn redo(&mut self) -> Result<CommandOutcome, CommandError> {
223        let command = self.redo_stack.pop().ok_or(CommandError::NothingToRedo)?;
224        match self.execute(command.clone()) {
225            Ok(outcome) => {
226                self.undo_stack.push(outcome.inverse.clone());
227                self.history.push(Revision {
228                    number: outcome.revision,
229                    command,
230                    changed: outcome.changed.clone(),
231                });
232                Ok(outcome)
233            }
234            Err(e) => {
235                self.redo_stack.push(command);
236                Err(e)
237            }
238        }
239    }
240
241    // ---- execution -----------------------------------------------------------------
242
243    /// Validate, mutate, and produce the inverse. Does not touch the undo stacks — that is
244    /// what lets `undo` and `redo` reuse it without recursion.
245    fn execute(&mut self, command: ModelCommand) -> Result<CommandOutcome, CommandError> {
246        let invalidates = command.invalidates_geometry();
247        let (inverse, changed) = match command {
248            ModelCommand::CreateElement { element } => {
249                let id = element.global_id.clone();
250                if self.elements.contains_key(&id) {
251                    return Err(CommandError::DuplicateElement(id));
252                }
253                if let Some(container) = &element.container {
254                    self.require_spatial(container)?;
255                }
256                self.elements.insert(id.clone(), *element);
257                (
258                    ModelCommand::DeleteElement {
259                        global_id: id.clone(),
260                    },
261                    vec![id],
262                )
263            }
264
265            ModelCommand::DeleteElement { global_id } => {
266                self.require_element(&global_id)?;
267                self.require_unreferenced(&global_id)?;
268                let element = self
269                    .elements
270                    .remove(&global_id)
271                    .expect("existence checked above");
272                (
273                    ModelCommand::CreateElement {
274                        element: Box::new(element),
275                    },
276                    vec![global_id],
277                )
278            }
279
280            ModelCommand::SetName { global_id, name } => {
281                let element = self.element_mut(&global_id)?;
282                let previous = std::mem::replace(&mut element.name, name);
283                element.semantic_revision += 1;
284                (
285                    ModelCommand::SetName {
286                        global_id: global_id.clone(),
287                        name: previous,
288                    },
289                    vec![global_id],
290                )
291            }
292
293            ModelCommand::SetProperty {
294                global_id,
295                set,
296                name,
297                value,
298            } => {
299                let element = self.element_mut(&global_id)?;
300                let previous = element.properties.set(&set, &name, value);
301                element.semantic_revision += 1;
302                (
303                    ModelCommand::SetProperty {
304                        global_id: global_id.clone(),
305                        set,
306                        name,
307                        value: previous,
308                    },
309                    vec![global_id],
310                )
311            }
312
313            ModelCommand::MoveElement { global_id, delta } => {
314                let element = self.element_mut(&global_id)?;
315                element.placement = element.placement.translated(delta);
316                element.representation_revision += 1;
317                (
318                    ModelCommand::MoveElement {
319                        global_id: global_id.clone(),
320                        delta: -delta,
321                    },
322                    vec![global_id],
323                )
324            }
325
326            ModelCommand::SetPlacement {
327                global_id,
328                placement,
329            } => {
330                let element = self.element_mut(&global_id)?;
331                let previous = std::mem::replace(&mut element.placement, placement);
332                element.representation_revision += 1;
333                (
334                    ModelCommand::SetPlacement {
335                        global_id: global_id.clone(),
336                        placement: previous,
337                    },
338                    vec![global_id],
339                )
340            }
341
342            ModelCommand::AssignContainer {
343                global_id,
344                container,
345            } => {
346                self.require_element(&global_id)?;
347                if let Some(c) = &container {
348                    if c == &global_id {
349                        return Err(CommandError::SelfReference(global_id));
350                    }
351                    self.require_spatial(c)?;
352                }
353                let element = self.element_mut(&global_id)?;
354                let previous = std::mem::replace(&mut element.container, container);
355                element.semantic_revision += 1;
356                (
357                    ModelCommand::AssignContainer {
358                        global_id: global_id.clone(),
359                        container: previous,
360                    },
361                    vec![global_id],
362                )
363            }
364
365            ModelCommand::SetRepresentation {
366                global_id,
367                representation,
368            } => {
369                if let Some(r) = &representation {
370                    if !r.is_valid() {
371                        return Err(CommandError::InvalidRepresentation(global_id));
372                    }
373                }
374                let element = self.element_mut(&global_id)?;
375                let previous =
376                    std::mem::replace(&mut element.representation, representation.map(|r| *r));
377                element.representation_revision += 1;
378                (
379                    ModelCommand::SetRepresentation {
380                        global_id: global_id.clone(),
381                        representation: previous.map(Box::new),
382                    },
383                    vec![global_id],
384                )
385            }
386
387            ModelCommand::AssignType {
388                global_id,
389                type_ref,
390            } => {
391                if type_ref.as_ref() == Some(&global_id) {
392                    return Err(CommandError::SelfReference(global_id));
393                }
394                // The type may be a family type held outside the element store, so its
395                // existence is deliberately not checked here (ADR-0005).
396                let element = self.element_mut(&global_id)?;
397                let previous = std::mem::replace(&mut element.type_ref, type_ref);
398                element.semantic_revision += 1;
399                (
400                    ModelCommand::AssignType {
401                        global_id: global_id.clone(),
402                        type_ref: previous,
403                    },
404                    vec![global_id],
405                )
406            }
407
408            ModelCommand::AddVoid { host, opening } => {
409                self.validate_void(&host, &opening)?;
410                if !self.voids.insert((host.clone(), opening.clone())) {
411                    return Err(CommandError::RelationshipExists {
412                        relationship: REL_VOIDS,
413                        a: host,
414                        b: opening,
415                    });
416                }
417                self.bump_representation(&host);
418                self.bump_representation(&opening);
419                (
420                    ModelCommand::RemoveVoid {
421                        host: host.clone(),
422                        opening: opening.clone(),
423                    },
424                    vec![host, opening],
425                )
426            }
427
428            ModelCommand::RemoveVoid { host, opening } => {
429                if !self.voids.remove(&(host.clone(), opening.clone())) {
430                    return Err(CommandError::NoSuchRelationship {
431                        relationship: REL_VOIDS,
432                        a: host,
433                        b: opening,
434                    });
435                }
436                self.bump_representation(&host);
437                self.bump_representation(&opening);
438                (
439                    ModelCommand::AddVoid {
440                        host: host.clone(),
441                        opening: opening.clone(),
442                    },
443                    vec![host, opening],
444                )
445            }
446
447            ModelCommand::AddFill { opening, filler } => {
448                self.validate_fill(&opening, &filler)?;
449                if !self.fills.insert((opening.clone(), filler.clone())) {
450                    return Err(CommandError::RelationshipExists {
451                        relationship: REL_FILLS,
452                        a: opening,
453                        b: filler,
454                    });
455                }
456                self.bump_representation(&filler);
457                (
458                    ModelCommand::RemoveFill {
459                        opening: opening.clone(),
460                        filler: filler.clone(),
461                    },
462                    vec![opening, filler],
463                )
464            }
465
466            ModelCommand::RemoveFill { opening, filler } => {
467                if !self.fills.remove(&(opening.clone(), filler.clone())) {
468                    return Err(CommandError::NoSuchRelationship {
469                        relationship: REL_FILLS,
470                        a: opening,
471                        b: filler,
472                    });
473                }
474                self.bump_representation(&filler);
475                (
476                    ModelCommand::AddFill {
477                        opening: opening.clone(),
478                        filler: filler.clone(),
479                    },
480                    vec![opening, filler],
481                )
482            }
483        };
484
485        self.revision += 1;
486        let geometry_invalidated = if invalidates {
487            changed.clone()
488        } else {
489            Vec::new()
490        };
491        Ok(CommandOutcome {
492            revision: self.revision,
493            inverse,
494            changed,
495            geometry_invalidated,
496        })
497    }
498
499    // ---- validation helpers --------------------------------------------------------
500
501    fn require_element(&self, id: &GlobalId) -> Result<&ElementRecord, CommandError> {
502        self.elements
503            .get(id)
504            .ok_or_else(|| CommandError::UnknownElement(id.clone()))
505    }
506
507    fn element_mut(&mut self, id: &GlobalId) -> Result<&mut ElementRecord, CommandError> {
508        self.elements
509            .get_mut(id)
510            .ok_or_else(|| CommandError::UnknownElement(id.clone()))
511    }
512
513    fn require_spatial(&self, id: &GlobalId) -> Result<(), CommandError> {
514        let element = self.require_element(id)?;
515        if element.class.is_spatial() {
516            Ok(())
517        } else {
518            Err(CommandError::NotSpatial(id.clone()))
519        }
520    }
521
522    /// Refuse to delete anything still referenced.
523    ///
524    /// Cascading deletes would make exact inversion impossible — undo would have to restore
525    /// an unbounded set of relationships. Forcing the caller to unwind explicitly keeps every
526    /// command invertible (`docs/ifc-semantics.md` §11 Phase 3: deletions must not orphan
527    /// relationships).
528    fn require_unreferenced(&self, id: &GlobalId) -> Result<(), CommandError> {
529        if self.voids.iter().any(|(h, o)| h == id || o == id) {
530            return Err(CommandError::StillReferenced {
531                global_id: id.clone(),
532                relationship: REL_VOIDS,
533            });
534        }
535        if self.fills.iter().any(|(o, f)| o == id || f == id) {
536            return Err(CommandError::StillReferenced {
537                global_id: id.clone(),
538                relationship: REL_FILLS,
539            });
540        }
541        if self
542            .elements
543            .values()
544            .any(|e| e.container.as_ref() == Some(id))
545        {
546            return Err(CommandError::StillReferenced {
547                global_id: id.clone(),
548                relationship: "IfcRelContainedInSpatialStructure",
549            });
550        }
551        if self
552            .elements
553            .values()
554            .any(|e| e.type_ref.as_ref() == Some(id))
555        {
556            return Err(CommandError::StillReferenced {
557                global_id: id.clone(),
558                relationship: "IfcRelDefinesByType",
559            });
560        }
561        Ok(())
562    }
563
564    fn validate_void(&self, host: &GlobalId, opening: &GlobalId) -> Result<(), CommandError> {
565        if host == opening {
566            return Err(CommandError::SelfReference(host.clone()));
567        }
568        let host_element = self.require_element(host)?;
569        if !host_element.class.can_host_openings() {
570            return Err(CommandError::NotAHost {
571                host: host.clone(),
572                class: host_element.class.ifc_name().to_owned(),
573            });
574        }
575        let opening_element = self.require_element(opening)?;
576        if opening_element.class != IfcClass::OpeningElement {
577            return Err(CommandError::NotAnOpening(opening.clone()));
578        }
579        Ok(())
580    }
581
582    fn validate_fill(&self, opening: &GlobalId, filler: &GlobalId) -> Result<(), CommandError> {
583        if opening == filler {
584            return Err(CommandError::SelfReference(opening.clone()));
585        }
586        let opening_element = self.require_element(opening)?;
587        if opening_element.class != IfcClass::OpeningElement {
588            return Err(CommandError::NotAnOpening(opening.clone()));
589        }
590        self.require_element(filler)?;
591        Ok(())
592    }
593
594    fn bump_representation(&mut self, id: &GlobalId) {
595        if let Some(e) = self.elements.get_mut(id) {
596            e.representation_revision += 1;
597        }
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use crate::element::Placement;
605    use crate::property::PropertyValue;
606    use glam::DVec3;
607
608    fn element(class: IfcClass) -> ElementRecord {
609        ElementRecord::new(GlobalId::new(), class)
610    }
611
612    fn create(element: ElementRecord) -> ModelCommand {
613        ModelCommand::CreateElement {
614            element: Box::new(element),
615        }
616    }
617
618    /// Storey, wall, opening, door — the smallest model with every relationship in it.
619    fn wall_with_door() -> (Model, GlobalId, GlobalId, GlobalId, GlobalId) {
620        let mut m = Model::new();
621        let storey = element(IfcClass::BuildingStorey);
622        let wall = element(IfcClass::Wall);
623        let opening = element(IfcClass::OpeningElement);
624        let door = element(IfcClass::Door);
625        let (s, w, o, d) = (
626            storey.global_id.clone(),
627            wall.global_id.clone(),
628            opening.global_id.clone(),
629            door.global_id.clone(),
630        );
631
632        m.apply(create(storey)).unwrap();
633        m.apply(create(wall.with_container(s.clone()))).unwrap();
634        m.apply(create(opening)).unwrap();
635        m.apply(create(door)).unwrap();
636        m.apply(ModelCommand::AddVoid {
637            host: w.clone(),
638            opening: o.clone(),
639        })
640        .unwrap();
641        m.apply(ModelCommand::AddFill {
642            opening: o.clone(),
643            filler: d.clone(),
644        })
645        .unwrap();
646        (m, s, w, o, d)
647    }
648
649    #[test]
650    fn create_assigns_a_revision_and_is_queryable() {
651        let mut m = Model::new();
652        let e = element(IfcClass::Wall);
653        let id = e.global_id.clone();
654
655        let outcome = m.apply(create(e)).unwrap();
656        assert_eq!(outcome.revision, 1);
657        assert_eq!(outcome.changed, vec![id.clone()]);
658        assert_eq!(outcome.geometry_invalidated, vec![id.clone()]);
659        assert!(m.contains(&id));
660        assert_eq!(m.len(), 1);
661    }
662
663    #[test]
664    fn duplicate_create_is_rejected_without_mutating() {
665        let mut m = Model::new();
666        let e = element(IfcClass::Wall);
667        m.apply(create(e.clone())).unwrap();
668        let revision = m.revision();
669
670        assert!(matches!(
671            m.apply(create(e)),
672            Err(CommandError::DuplicateElement(_))
673        ));
674        assert_eq!(
675            m.revision(),
676            revision,
677            "a rejected command must not advance the revision"
678        );
679        assert_eq!(m.len(), 1);
680    }
681
682    #[test]
683    fn every_command_inverts_exactly() {
684        let (mut m, _s, w, o, d) = wall_with_door();
685        let baseline = m.clone();
686
687        let commands = vec![
688            ModelCommand::SetName {
689                global_id: w.clone(),
690                name: Some("Exterior wall".into()),
691            },
692            ModelCommand::SetProperty {
693                global_id: w.clone(),
694                set: "Pset_WallCommon".into(),
695                name: "IsExternal".into(),
696                value: Some(PropertyValue::Boolean(true)),
697            },
698            ModelCommand::MoveElement {
699                global_id: w.clone(),
700                delta: DVec3::new(1.5, -2.0, 0.25),
701            },
702            ModelCommand::SetPlacement {
703                global_id: d.clone(),
704                placement: Placement::at(DVec3::new(3.0, 0.0, 0.0)),
705            },
706            ModelCommand::RemoveFill {
707                opening: o.clone(),
708                filler: d.clone(),
709            },
710            ModelCommand::AssignType {
711                global_id: d.clone(),
712                type_ref: Some(GlobalId::new()),
713            },
714            ModelCommand::SetRepresentation {
715                global_id: w.clone(),
716                representation: Some(Box::new(crate::Representation::extrusion(
717                    vec![[0.0, 0.0], [4.0, 0.0], [4.0, 0.2], [0.0, 0.2]],
718                    [0.0, 0.0, 1.0],
719                    3.0,
720                ))),
721            },
722        ];
723        let count = commands.len();
724        m.apply_all(commands).unwrap();
725
726        assert_ne!(
727            m.elements, baseline.elements,
728            "the batch must actually change something"
729        );
730
731        for _ in 0..count {
732            m.undo().unwrap();
733        }
734
735        // Revision counters advance — history is append-only — but the state must match.
736        assert_eq!(m.elements.len(), baseline.elements.len());
737        for (id, element) in &baseline.elements {
738            let restored = m.get(id).expect("element restored");
739            assert_eq!(restored.name, element.name);
740            assert_eq!(restored.placement, element.placement);
741            assert_eq!(restored.properties, element.properties);
742            assert_eq!(restored.representation, element.representation);
743            assert_eq!(restored.container, element.container);
744            assert_eq!(restored.type_ref, element.type_ref);
745        }
746        assert_eq!(m.voids, baseline.voids);
747        assert_eq!(m.fills, baseline.fills);
748    }
749
750    #[test]
751    fn undo_then_redo_returns_to_the_edited_state() {
752        let mut m = Model::new();
753        let e = element(IfcClass::Wall);
754        let id = e.global_id.clone();
755        m.apply(create(e)).unwrap();
756        m.apply(ModelCommand::SetName {
757            global_id: id.clone(),
758            name: Some("W-01".into()),
759        })
760        .unwrap();
761
762        m.undo().unwrap();
763        assert_eq!(m.get(&id).unwrap().name, None);
764        assert!(m.can_redo());
765
766        m.redo().unwrap();
767        assert_eq!(m.get(&id).unwrap().name.as_deref(), Some("W-01"));
768    }
769
770    #[test]
771    fn a_new_command_clears_the_redo_stack() {
772        let mut m = Model::new();
773        let e = element(IfcClass::Wall);
774        let id = e.global_id.clone();
775        m.apply(create(e)).unwrap();
776        m.apply(ModelCommand::SetName {
777            global_id: id.clone(),
778            name: Some("W-01".into()),
779        })
780        .unwrap();
781        m.undo().unwrap();
782        assert!(m.can_redo());
783
784        m.apply(ModelCommand::SetName {
785            global_id: id,
786            name: Some("W-02".into()),
787        })
788        .unwrap();
789        assert!(
790            !m.can_redo(),
791            "branching history must discard the abandoned future"
792        );
793    }
794
795    #[test]
796    fn undo_on_an_empty_stack_is_an_error_not_a_panic() {
797        let mut m = Model::new();
798        assert_eq!(m.undo(), Err(CommandError::NothingToUndo));
799        assert_eq!(m.redo(), Err(CommandError::NothingToRedo));
800    }
801
802    #[test]
803    fn renaming_does_not_invalidate_geometry() {
804        let mut m = Model::new();
805        let e = element(IfcClass::Wall);
806        let id = e.global_id.clone();
807        m.apply(create(e)).unwrap();
808        let representation = m.get(&id).unwrap().representation_revision;
809
810        let outcome = m
811            .apply(ModelCommand::SetName {
812                global_id: id.clone(),
813                name: Some("W-01".into()),
814            })
815            .unwrap();
816
817        assert!(outcome.geometry_invalidated.is_empty());
818        assert_eq!(m.get(&id).unwrap().representation_revision, representation);
819        assert_eq!(m.get(&id).unwrap().semantic_revision, 1);
820    }
821
822    #[test]
823    fn moving_invalidates_geometry() {
824        let mut m = Model::new();
825        let e = element(IfcClass::Wall);
826        let id = e.global_id.clone();
827        m.apply(create(e)).unwrap();
828
829        let outcome = m
830            .apply(ModelCommand::MoveElement {
831                global_id: id.clone(),
832                delta: DVec3::X,
833            })
834            .unwrap();
835
836        assert_eq!(outcome.geometry_invalidated, vec![id.clone()]);
837        assert_eq!(m.get(&id).unwrap().semantic_revision, 0);
838        assert!(m.get(&id).unwrap().representation_revision > 0);
839    }
840
841    #[test]
842    fn only_hosts_take_openings_and_only_openings_are_taken() {
843        let mut m = Model::new();
844        let furniture = element(IfcClass::Furniture);
845        let wall = element(IfcClass::Wall);
846        let opening = element(IfcClass::OpeningElement);
847        let (f, w, o) = (
848            furniture.global_id.clone(),
849            wall.global_id.clone(),
850            opening.global_id.clone(),
851        );
852        m.apply_all([create(furniture), create(wall), create(opening)])
853            .unwrap();
854
855        assert!(matches!(
856            m.apply(ModelCommand::AddVoid {
857                host: f,
858                opening: o.clone()
859            }),
860            Err(CommandError::NotAHost { .. })
861        ));
862        assert!(matches!(
863            m.apply(ModelCommand::AddVoid {
864                host: w.clone(),
865                opening: w.clone()
866            }),
867            Err(CommandError::SelfReference(_))
868        ));
869        m.apply(ModelCommand::AddVoid {
870            host: w.clone(),
871            opening: o.clone(),
872        })
873        .unwrap();
874        assert!(matches!(
875            m.apply(ModelCommand::AddVoid {
876                host: w,
877                opening: o
878            }),
879            Err(CommandError::RelationshipExists { .. })
880        ));
881    }
882
883    #[test]
884    fn deleting_a_referenced_element_is_refused() {
885        let (mut m, storey, wall, opening, _door) = wall_with_door();
886
887        // The wall hosts an opening.
888        assert!(matches!(
889            m.apply(ModelCommand::DeleteElement {
890                global_id: wall.clone()
891            }),
892            Err(CommandError::StillReferenced { .. })
893        ));
894        // The storey contains the wall.
895        assert!(matches!(
896            m.apply(ModelCommand::DeleteElement { global_id: storey }),
897            Err(CommandError::StillReferenced { .. })
898        ));
899
900        // Unwind explicitly, and it becomes possible.
901        let door = m.fills_of(&opening).next().cloned().unwrap();
902        m.apply(ModelCommand::RemoveFill {
903            opening: opening.clone(),
904            filler: door,
905        })
906        .unwrap();
907        m.apply(ModelCommand::RemoveVoid {
908            host: wall.clone(),
909            opening,
910        })
911        .unwrap();
912        m.apply(ModelCommand::AssignContainer {
913            global_id: wall.clone(),
914            container: None,
915        })
916        .unwrap();
917        m.apply(ModelCommand::DeleteElement {
918            global_id: wall.clone(),
919        })
920        .unwrap();
921        assert!(!m.contains(&wall));
922    }
923
924    #[test]
925    fn containers_must_be_spatial() {
926        let mut m = Model::new();
927        let wall = element(IfcClass::Wall);
928        let door = element(IfcClass::Door);
929        let (w, d) = (wall.global_id.clone(), door.global_id.clone());
930        m.apply_all([create(wall), create(door)]).unwrap();
931
932        assert!(matches!(
933            m.apply(ModelCommand::AssignContainer {
934                global_id: d,
935                container: Some(w.clone()),
936            }),
937            Err(CommandError::NotSpatial(_))
938        ));
939        assert!(matches!(
940            m.apply(ModelCommand::AssignContainer {
941                global_id: w.clone(),
942                container: Some(w),
943            }),
944            Err(CommandError::SelfReference(_))
945        ));
946    }
947
948    #[test]
949    fn relationships_are_navigable_in_both_directions() {
950        let (m, _s, wall, opening, door) = wall_with_door();
951        assert_eq!(m.openings_of(&wall).collect::<Vec<_>>(), vec![&opening]);
952        assert_eq!(m.host_of(&opening), Some(&wall));
953        assert_eq!(m.fills_of(&opening).collect::<Vec<_>>(), vec![&door]);
954        assert_eq!(m.contained_in(&_s).count(), 1);
955        assert_eq!(m.by_class(&IfcClass::Door).count(), 1);
956    }
957
958    #[test]
959    fn a_malformed_representation_is_refused_before_it_reaches_the_model() {
960        // Two collinear points enclose nothing, and an IfcExtrudedAreaSolid built from them
961        // produces a file other applications cannot open. Reject it at the command, not at
962        // export time.
963        let mut m = Model::new();
964        let e = element(IfcClass::Wall);
965        let id = e.global_id.clone();
966        m.apply(create(e)).unwrap();
967        let revision = m.revision();
968
969        let result = m.apply(ModelCommand::SetRepresentation {
970            global_id: id.clone(),
971            representation: Some(Box::new(crate::Representation::extrusion(
972                vec![[0.0, 0.0], [1.0, 0.0]],
973                [0.0, 0.0, 1.0],
974                3.0,
975            ))),
976        });
977
978        assert!(matches!(
979            result,
980            Err(CommandError::InvalidRepresentation(_))
981        ));
982        assert_eq!(m.revision(), revision);
983        assert!(m.get(&id).unwrap().representation.is_none());
984    }
985
986    #[test]
987    fn setting_a_representation_invalidates_geometry_not_semantics() {
988        let mut m = Model::new();
989        let e = element(IfcClass::Wall);
990        let id = e.global_id.clone();
991        m.apply(create(e)).unwrap();
992
993        let outcome = m
994            .apply(ModelCommand::SetRepresentation {
995                global_id: id.clone(),
996                representation: Some(Box::new(crate::Representation::extrusion(
997                    vec![[0.0, 0.0], [4.0, 0.0], [4.0, 0.2], [0.0, 0.2]],
998                    [0.0, 0.0, 1.0],
999                    3.0,
1000                ))),
1001            })
1002            .unwrap();
1003
1004        assert_eq!(outcome.geometry_invalidated, vec![id.clone()]);
1005        assert_eq!(m.get(&id).unwrap().semantic_revision, 0);
1006        assert!(m
1007            .get(&id)
1008            .unwrap()
1009            .representation
1010            .as_ref()
1011            .unwrap()
1012            .is_native_parametric());
1013    }
1014
1015    #[test]
1016    fn history_records_every_mutation() {
1017        let (m, ..) = wall_with_door();
1018        assert_eq!(m.history().len(), 6);
1019        assert_eq!(m.revision(), 6);
1020        assert_eq!(m.history()[0].number, 1);
1021    }
1022}