ifc_model/model.rs
1//! The entity graph — storage, lookup, and nothing else.
2//!
3//! # What this type deliberately does NOT do
4//!
5//! `Model` has no idea what a cost item, a task, a wall, or a material is. It
6//! stores entities and answers structural questions about them. Every domain
7//! meaning lives in a separate crate that borrows a `&Model` and interprets it.
8//!
9//! That is not a stylistic preference, it is what makes two things possible:
10//!
11//! 1. **Thin builds.** An app that only reads geometry compiles no cost,
12//! schedule, or structural code, because those crates are optional features
13//! rather than parts of the model.
14//! 2. **Lossless round-trips of data we do not understand.** Since the model
15//! stores entities structurally, a cost entity survives parse and re-export
16//! byte-for-byte in content even when `ifc-cost` is not compiled in. If the
17//! model instead held a `CostItem` struct, dropping the feature would drop
18//! the data.
19//!
20//! The rule to preserve: **no `if type_name == "IFCWALL"` in this crate.**
21
22use crate::diagnostic::Diagnostic;
23use crate::entity::Entity;
24use crate::header::Header;
25use crate::value::EntityId;
26use ahash::AHashMap;
27
28/// A parsed IFC file: header, entities, and indices over them.
29#[derive(Debug, Clone, Default)]
30pub struct Model {
31 header: Header,
32 /// Entities keyed by their in-file id, so `#42` survives a round-trip.
33 entities: AHashMap<EntityId, Entity>,
34 /// Insertion order, so a re-export preserves the original file order
35 /// instead of hash order. Diffing two exports is otherwise unreadable.
36 order: Vec<EntityId>,
37 /// Type name to entity ids. Built during insertion because "every
38 /// IfcWall" is the most common query in any consumer.
39 by_type: AHashMap<String, Vec<EntityId>>,
40 max_id: u64,
41 /// Bumped by every structural change. A transaction opened against one
42 /// revision refuses to commit against another, so an editor working from
43 /// a stale view is told rather than silently overwriting.
44 ///
45 /// Not a content hash: two different edit sequences can reach the same
46 /// bytes and still get different revisions. It answers "did anything
47 /// change", which is the question optimistic concurrency asks.
48 revision: u64,
49 /// Non-fatal findings from the read that produced this model. Empty
50 /// unless a codec recovered from damaged input.
51 diagnostics: Vec<Diagnostic>,
52}
53
54impl Model {
55 /// An empty model.
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 /// The file header (schema declaration, description, author).
61 pub fn header(&self) -> &Header {
62 &self.header
63 }
64
65 /// Mutable access to the header, for writers and editors.
66 pub fn header_mut(&mut self) -> &mut Header {
67 &mut self.header
68 }
69
70 /// Non-fatal problems reported by the codec that read this model.
71 ///
72 /// Empty for a clean file. A non-empty slice means the model is
73 /// incomplete relative to its source: the codec recovered from damage and
74 /// each entry says exactly what was dropped, so a consumer can surface
75 /// "loaded, 1 record skipped" instead of pretending the read was lossless.
76 pub fn diagnostics(&self) -> &[Diagnostic] {
77 &self.diagnostics
78 }
79
80 /// Whether the read that produced this model dropped anything.
81 pub fn is_complete(&self) -> bool {
82 self.diagnostics.is_empty()
83 }
84
85 /// Attaches a codec diagnostic. Called by codecs during a recovered read.
86 pub fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
87 self.diagnostics.push(diagnostic);
88 }
89
90 /// Insert an entity under a specific id, replacing any previous occupant.
91 ///
92 /// Codecs use this to preserve file ids exactly.
93 pub fn insert(&mut self, id: EntityId, entity: Entity) {
94 #[cfg(feature = "authored-dump")]
95 crate::authored_dump::record(&entity.type_name, "insert");
96 let key = entity.type_name.to_ascii_uppercase();
97 match self.entities.insert(id, entity) {
98 None => self.order.push(id),
99 // Replacing an occupant: drop its old type-index entry, otherwise
100 // `ids_of_type` reports the id twice for the same type, or keeps
101 // reporting it under a type the entity no longer has. Both make an
102 // edit layer silently wrong.
103 Some(previous) => {
104 let previous_key = previous.type_name.to_ascii_uppercase();
105 if previous_key != key {
106 if let Some(ids) = self.by_type.get_mut(&previous_key) {
107 ids.retain(|existing| *existing != id);
108 }
109 } else {
110 // Same type: the entry is still correct, so re-adding it
111 // below would duplicate it.
112 self.max_id = self.max_id.max(id.0);
113 self.revision += 1;
114 return;
115 }
116 }
117 }
118 self.by_type.entry(key).or_default().push(id);
119 self.max_id = self.max_id.max(id.0);
120 self.revision += 1;
121 }
122
123 /// Append an entity, allocating the next free id.
124 pub fn push(&mut self, entity: Entity) -> EntityId {
125 let id = EntityId(self.max_id + 1);
126 self.insert(id, entity);
127 id
128 }
129
130 /// How many structural changes this model has seen.
131 ///
132 /// Starts at zero and increases; the absolute value carries no meaning
133 /// beyond comparison. See [`Transaction`](crate::Transaction).
134 pub fn revision(&self) -> u64 {
135 self.revision
136 }
137
138 /// Record a structural change made through a sibling mutation module.
139 pub(crate) fn bump_revision(&mut self) {
140 self.revision += 1;
141 }
142
143 /// The id [`Model::push`] would allocate next.
144 ///
145 /// A transaction reserves ids from here so several creates in one batch
146 /// cannot collide with each other or with existing entities.
147 pub fn next_id(&self) -> EntityId {
148 EntityId(self.max_id + 1)
149 }
150
151 /// Look up one entity.
152 pub fn get(&self, id: EntityId) -> Option<&Entity> {
153 self.entities.get(&id)
154 }
155
156 /// Number of entities.
157 pub fn len(&self) -> usize {
158 self.entities.len()
159 }
160
161 /// Whether the model holds no entities.
162 pub fn is_empty(&self) -> bool {
163 self.entities.is_empty()
164 }
165
166 /// Entity ids in original file order.
167 pub fn ids(&self) -> impl Iterator<Item = EntityId> + '_ {
168 self.order.iter().copied()
169 }
170
171 /// Entities in original file order.
172 pub fn iter(&self) -> impl Iterator<Item = (EntityId, &Entity)> + '_ {
173 self.order
174 .iter()
175 .filter_map(move |id| self.entities.get(id).map(|e| (*id, e)))
176 }
177
178 /// Ids of every entity with this exact type name, case-insensitive.
179 ///
180 /// This is an exact-type query and does **not** include subtypes: asking
181 /// for `IfcElement` will not return walls. Subtype queries need the schema,
182 /// which this crate does not depend on. The facade's
183 /// `ifc::ids_of_type_including_subtypes` (feature `schema`) joins the two.
184 pub fn ids_of_type(&self, type_name: &str) -> &[EntityId] {
185 self.by_type
186 .get(&type_name.to_ascii_uppercase())
187 .map(|v| v.as_slice())
188 .unwrap_or(&[])
189 }
190
191 /// Entities with this exact type name.
192 pub fn of_type<'a>(&'a self, type_name: &str) -> impl Iterator<Item = (EntityId, &'a Entity)> {
193 self.ids_of_type(type_name)
194 .iter()
195 .filter_map(move |id| self.entities.get(id).map(|e| (*id, e)))
196 .collect::<Vec<_>>()
197 .into_iter()
198 }
199
200 /// Every distinct type name present, with its instance count.
201 ///
202 /// Useful as a cheap file summary and as the basis for a coverage report
203 /// of what a given build can and cannot interpret.
204 pub fn type_histogram(&self) -> Vec<(&str, usize)> {
205 let mut v: Vec<_> = self
206 .by_type
207 .iter()
208 .map(|(k, ids)| (k.as_str(), ids.len()))
209 .collect();
210 v.sort_unstable_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
211 v
212 }
213
214 /// Ids that are referenced by some entity but do not exist.
215 ///
216 /// A dangling reference is the most common corruption in real files, and
217 /// it is a structural question, so it belongs here rather than in a
218 /// validation crate.
219 pub fn dangling_references(&self) -> Vec<(EntityId, EntityId)> {
220 let mut out = Vec::new();
221 for (id, entity) in self.iter() {
222 for target in entity.references() {
223 if !self.entities.contains_key(&target) {
224 out.push((id, target));
225 }
226 }
227 }
228 out
229 }
230
231 // --- crate-internal seams for `mutation::edit` -----------------------
232 //
233 // Kept private-to-crate rather than `pub`: an edit that changes
234 // `type_name` must also fix up `by_type`, or `ids_of_type` silently goes
235 // stale. `mutation::edit` is the only module trusted to touch these
236 // fields directly, and it exists precisely to keep that invariant in one
237 // place instead of copied into every editor.
238
239 pub(crate) fn entities_mut(&mut self) -> &mut AHashMap<EntityId, Entity> {
240 &mut self.entities
241 }
242
243 pub(crate) fn by_type_mut(&mut self) -> &mut AHashMap<String, Vec<EntityId>> {
244 &mut self.by_type
245 }
246
247 pub(crate) fn order_mut(&mut self) -> &mut Vec<EntityId> {
248 &mut self.order
249 }
250}