1use 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#[derive(Debug, Clone, PartialEq)]
18pub struct Revision {
19 pub number: u64,
20 pub command: ModelCommand,
21 pub changed: Vec<GlobalId>,
22}
23
24#[derive(Debug, Clone, Default)]
29pub struct Model {
30 elements: BTreeMap<GlobalId, ElementRecord>,
31 voids: BTreeSet<(GlobalId, GlobalId)>,
33 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 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 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 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 pub fn voids(&self) -> impl Iterator<Item = (&GlobalId, &GlobalId)> {
105 self.voids.iter().map(|(h, o)| (h, o))
106 }
107
108 pub fn fills(&self) -> impl Iterator<Item = (&GlobalId, &GlobalId)> {
110 self.fills.iter().map(|(o, f)| (o, f))
111 }
112
113 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(matches!(
889 m.apply(ModelCommand::DeleteElement {
890 global_id: wall.clone()
891 }),
892 Err(CommandError::StillReferenced { .. })
893 ));
894 assert!(matches!(
896 m.apply(ModelCommand::DeleteElement { global_id: storey }),
897 Err(CommandError::StillReferenced { .. })
898 ));
899
900 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 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}