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 std::ops::Range;
23use std::sync::Arc;
24
25use crate::diagnostic::Diagnostic;
26use crate::entity::Entity;
27use crate::header::Header;
28use crate::lazy::{EntitySource, Slot};
29use crate::value::EntityId;
30use ahash::AHashMap;
31
32/// A parsed IFC file: header, entities, and indices over them.
33#[derive(Debug, Clone, Default)]
34pub struct Model {
35 header: Header,
36 /// Entities keyed by their in-file id, so `#42` survives a round-trip.
37 /// A slot holds its entity decoded, or the span `source` decodes it from
38 /// on first access (see [`EntitySource`]).
39 entities: AHashMap<EntityId, Slot>,
40 /// Insertion order, so a re-export preserves the original file order
41 /// instead of hash order. Diffing two exports is otherwise unreadable.
42 order: Vec<EntityId>,
43 /// Type name to entity ids. Built during insertion because "every
44 /// IfcWall" is the most common query in any consumer.
45 by_type: AHashMap<String, Vec<EntityId>>,
46 max_id: u64,
47 /// Bumped by every structural change. A transaction opened against one
48 /// revision refuses to commit against another, so an editor working from
49 /// a stale view is told rather than silently overwriting.
50 ///
51 /// Not a content hash: two different edit sequences can reach the same
52 /// bytes and still get different revisions. It answers "did anything
53 /// change", which is the question optimistic concurrency asks.
54 revision: u64,
55 /// Non-fatal findings from the read that produced this model. Empty
56 /// unless a codec recovered from damaged input.
57 diagnostics: Vec<Diagnostic>,
58 /// Decodes lazily registered entities; `None` for a model built only
59 /// from decoded entities. Shared by clones.
60 source: Option<Arc<dyn EntitySource>>,
61}
62
63impl Model {
64 /// An empty model.
65 pub fn new() -> Self {
66 Self::default()
67 }
68
69 /// An empty model whose entities a codec registers with
70 /// [`Model::insert_lazy`] and `source` decodes on first access.
71 pub fn with_source(source: Arc<dyn EntitySource>) -> Self {
72 Self {
73 source: Some(source),
74 ..Self::default()
75 }
76 }
77
78 /// The file header (schema declaration, description, author).
79 pub fn header(&self) -> &Header {
80 &self.header
81 }
82
83 /// Mutable access to the header, for writers and editors.
84 pub fn header_mut(&mut self) -> &mut Header {
85 &mut self.header
86 }
87
88 /// Non-fatal problems reported by the codec that read this model.
89 ///
90 /// Empty for a clean file. A non-empty slice means the model is
91 /// incomplete relative to its source: the codec recovered from damage and
92 /// each entry says exactly what was dropped, so a consumer can surface
93 /// "loaded, 1 record skipped" instead of pretending the read was lossless.
94 pub fn diagnostics(&self) -> &[Diagnostic] {
95 &self.diagnostics
96 }
97
98 /// Whether the read that produced this model dropped anything.
99 pub fn is_complete(&self) -> bool {
100 self.diagnostics.is_empty()
101 }
102
103 /// Attaches a codec diagnostic. Called by codecs during a recovered read.
104 pub fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
105 self.diagnostics.push(diagnostic);
106 }
107
108 /// Insert an entity under a specific id, replacing any previous occupant.
109 ///
110 /// Codecs use this to preserve file ids exactly.
111 pub fn insert(&mut self, id: EntityId, entity: Entity) {
112 #[cfg(feature = "authored-dump")]
113 crate::authored_dump::record(&entity.type_name, "insert");
114 let type_name = std::sync::Arc::clone(&entity.type_name);
115 self.place(id, &type_name, Slot::decoded(entity));
116 }
117
118 /// Register the entity stored at `span` of this model's source under
119 /// `id`, without decoding it; it is decoded on first access. Replaces
120 /// any previous occupant exactly as [`Model::insert`] does.
121 ///
122 /// `type_name` must be the type the span decodes to, which lets
123 /// [`Model::ids_of_type`] answer without decoding. Only a codec that
124 /// validated the span may register it (see [`EntitySource`]).
125 ///
126 /// # Panics
127 ///
128 /// When the model has no source ([`Model::with_source`]).
129 pub fn insert_lazy(&mut self, id: EntityId, type_name: &str, span: Range<usize>) {
130 assert!(
131 self.source.is_some(),
132 "insert_lazy needs a model built with Model::with_source"
133 );
134 #[cfg(feature = "authored-dump")]
135 crate::authored_dump::record(type_name, "insert");
136 self.place(id, type_name, Slot::lazy(span));
137 }
138
139 /// Reserve room for `additional` more entities; a codec that knows the
140 /// record count up front avoids regrowing the storage while it loads.
141 pub fn reserve(&mut self, additional: usize) {
142 self.entities.reserve(additional);
143 self.order.reserve(additional);
144 }
145
146 /// The shared tail of [`Model::insert`] and [`Model::insert_lazy`].
147 fn place(&mut self, id: EntityId, type_name: &str, slot: Slot) {
148 // Type names are ASCII upper case in practice, so the key is
149 // usually `type_name` itself and nothing is allocated per entity.
150 let upper;
151 let key = if type_name.bytes().any(|byte| byte.is_ascii_lowercase()) {
152 upper = type_name.to_ascii_uppercase();
153 upper.as_str()
154 } else {
155 type_name
156 };
157 match self.entities.insert(id, slot) {
158 None => self.order.push(id),
159 // Replacing an occupant: drop its old type-index entry, otherwise
160 // `ids_of_type` reports the id twice for the same type, or keeps
161 // reporting it under a type the entity no longer has. Both make an
162 // edit layer silently wrong.
163 Some(previous) => {
164 let previous = previous.into_entity(self.source.as_deref());
165 let previous_key = previous.type_name.to_ascii_uppercase();
166 if previous_key != key {
167 self.unindex(&previous_key, id);
168 } else {
169 // Same type: the entry is still correct, so re-adding it
170 // below would duplicate it.
171 self.max_id = self.max_id.max(id.0);
172 self.revision += 1;
173 return;
174 }
175 }
176 }
177 match self.by_type.get_mut(key) {
178 Some(ids) => ids.push(id),
179 None => {
180 self.by_type.insert(key.to_owned(), vec![id]);
181 }
182 }
183 self.max_id = self.max_id.max(id.0);
184 self.revision += 1;
185 }
186
187 /// Append an entity, allocating the next free id.
188 pub fn push(&mut self, entity: Entity) -> EntityId {
189 let id = EntityId(self.max_id + 1);
190 self.insert(id, entity);
191 id
192 }
193
194 /// How many structural changes this model has seen.
195 ///
196 /// Starts at zero and increases; the absolute value carries no meaning
197 /// beyond comparison. See [`Transaction`](crate::Transaction).
198 pub fn revision(&self) -> u64 {
199 self.revision
200 }
201
202 /// Record a structural change made through a sibling mutation module.
203 pub(crate) fn bump_revision(&mut self) {
204 self.revision += 1;
205 }
206
207 /// The id [`Model::push`] would allocate next.
208 ///
209 /// A transaction reserves ids from here so several creates in one batch
210 /// cannot collide with each other or with existing entities.
211 pub fn next_id(&self) -> EntityId {
212 EntityId(self.max_id + 1)
213 }
214
215 /// Look up one entity, decoding it first if it was loaded lazily.
216 pub fn get(&self, id: EntityId) -> Option<&Entity> {
217 self.entities
218 .get(&id)
219 .map(|slot| slot.get(self.source.as_deref()))
220 }
221
222 /// Whether `id` names an entity, without decoding it.
223 pub fn contains(&self, id: EntityId) -> bool {
224 self.entities.contains_key(&id)
225 }
226
227 /// How many entities have been decoded. Equals [`Model::len`] for a
228 /// model built from decoded entities; for a lazily loaded one it counts
229 /// the entities accessed so far.
230 pub fn decoded_len(&self) -> usize {
231 self.entities
232 .values()
233 .filter(|slot| slot.is_decoded())
234 .count()
235 }
236
237 /// Decode every entity now, on up to `threads` threads.
238 ///
239 /// A lazily loaded model decodes on first access, one entity at a time.
240 /// A consumer about to touch most of the model -- a writer, a full
241 /// validation, a geometry pass -- can decode everything up front in
242 /// parallel instead. Idempotent, and a no-op on a decoded model.
243 pub fn decode_all(&self, threads: usize) {
244 let pending: Vec<&Slot> = self
245 .entities
246 .values()
247 .filter(|slot| !slot.is_decoded())
248 .collect();
249 if pending.is_empty() {
250 return;
251 }
252 let source = self.source.as_deref();
253 let threads = threads.clamp(1, pending.len());
254 if threads == 1 {
255 for slot in pending {
256 slot.get(source);
257 }
258 return;
259 }
260 let chunk = pending.len().div_ceil(threads);
261 std::thread::scope(|scope| {
262 for part in pending.chunks(chunk) {
263 scope.spawn(move || {
264 for slot in part {
265 slot.get(source);
266 }
267 });
268 }
269 });
270 }
271
272 /// Number of entities.
273 pub fn len(&self) -> usize {
274 self.entities.len()
275 }
276
277 /// Whether the model holds no entities.
278 pub fn is_empty(&self) -> bool {
279 self.entities.is_empty()
280 }
281
282 /// Entity ids in original file order.
283 pub fn ids(&self) -> impl Iterator<Item = EntityId> + '_ {
284 self.order.iter().copied()
285 }
286
287 /// Entities in original file order.
288 pub fn iter(&self) -> impl Iterator<Item = (EntityId, &Entity)> + '_ {
289 self.order
290 .iter()
291 .filter_map(move |id| self.get(*id).map(|e| (*id, e)))
292 }
293
294 /// Ids of every entity with this exact type name, case-insensitive.
295 ///
296 /// This is an exact-type query and does **not** include subtypes: asking
297 /// for `IfcElement` will not return walls. Subtype queries need the schema,
298 /// which this crate does not depend on. The facade's
299 /// `ifc::ids_of_type_including_subtypes` (feature `schema`) joins the two.
300 pub fn ids_of_type(&self, type_name: &str) -> &[EntityId] {
301 self.by_type
302 .get(&type_name.to_ascii_uppercase())
303 .map(|v| v.as_slice())
304 .unwrap_or(&[])
305 }
306
307 /// Entities with this exact type name.
308 pub fn of_type<'a>(&'a self, type_name: &str) -> impl Iterator<Item = (EntityId, &'a Entity)> {
309 self.ids_of_type(type_name)
310 .iter()
311 .filter_map(move |id| self.get(*id).map(|e| (*id, e)))
312 .collect::<Vec<_>>()
313 .into_iter()
314 }
315
316 /// Every distinct type name present, with its instance count.
317 ///
318 /// Useful as a cheap file summary and as the basis for a coverage report
319 /// of what a given build can and cannot interpret.
320 pub fn type_histogram(&self) -> Vec<(&str, usize)> {
321 let mut v: Vec<_> = self
322 .by_type
323 .iter()
324 .map(|(k, ids)| (k.as_str(), ids.len()))
325 .collect();
326 v.sort_unstable_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
327 v
328 }
329
330 /// Ids that are referenced by some entity but do not exist.
331 ///
332 /// A dangling reference is the most common corruption in real files, and
333 /// it is a structural question, so it belongs here rather than in a
334 /// validation crate.
335 pub fn dangling_references(&self) -> Vec<(EntityId, EntityId)> {
336 let mut out = Vec::new();
337 for (id, entity) in self.iter() {
338 for target in entity.references() {
339 if !self.entities.contains_key(&target) {
340 out.push((id, target));
341 }
342 }
343 }
344 out
345 }
346
347 // --- crate-internal seams for `mutation::edit` -----------------------
348 //
349 // Kept private-to-crate rather than `pub`: an edit that changes
350 // `type_name` must also fix up `by_type`, or `ids_of_type` silently goes
351 // stale. `mutation::edit` is the only module trusted to touch these
352 // fields directly, and it exists precisely to keep that invariant in one
353 // place instead of copied into every editor.
354
355 /// The entity under `id` for editing, decoded first when needed.
356 pub(crate) fn entity_mut(&mut self, id: EntityId) -> Option<&mut Entity> {
357 let source = self.source.clone();
358 Some(self.entities.get_mut(&id)?.get_mut(source.as_deref()))
359 }
360
361 /// Takes the entity under `id` out of storage, decoded; the caller
362 /// fixes up `by_type` and `order`.
363 pub(crate) fn take_entity(&mut self, id: EntityId) -> Option<Entity> {
364 let slot = self.entities.remove(&id)?;
365 Some(slot.into_entity(self.source.as_deref()))
366 }
367
368 pub(crate) fn by_type_mut(&mut self) -> &mut AHashMap<String, Vec<EntityId>> {
369 &mut self.by_type
370 }
371
372 /// Drops `id` from the type-index bucket `key` (already upper-cased),
373 /// and the bucket itself once it is empty: a type no entity has any
374 /// more must not linger in [`Model::type_histogram`] with a count of
375 /// zero, which a model rebuilt from the same entities would not list
376 /// (#106).
377 pub(crate) fn unindex(&mut self, key: &str, id: EntityId) {
378 if let Some(ids) = self.by_type.get_mut(key) {
379 ids.retain(|existing| *existing != id);
380 if ids.is_empty() {
381 self.by_type.remove(key);
382 }
383 }
384 }
385
386 pub(crate) fn order_mut(&mut self) -> &mut Vec<EntityId> {
387 &mut self.order
388 }
389}