Skip to main content

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                    if let Some(ids) = self.by_type.get_mut(&previous_key) {
168                        ids.retain(|existing| *existing != id);
169                    }
170                } else {
171                    // Same type: the entry is still correct, so re-adding it
172                    // below would duplicate it.
173                    self.max_id = self.max_id.max(id.0);
174                    self.revision += 1;
175                    return;
176                }
177            }
178        }
179        match self.by_type.get_mut(key) {
180            Some(ids) => ids.push(id),
181            None => {
182                self.by_type.insert(key.to_owned(), vec![id]);
183            }
184        }
185        self.max_id = self.max_id.max(id.0);
186        self.revision += 1;
187    }
188
189    /// Append an entity, allocating the next free id.
190    pub fn push(&mut self, entity: Entity) -> EntityId {
191        let id = EntityId(self.max_id + 1);
192        self.insert(id, entity);
193        id
194    }
195
196    /// How many structural changes this model has seen.
197    ///
198    /// Starts at zero and increases; the absolute value carries no meaning
199    /// beyond comparison. See [`Transaction`](crate::Transaction).
200    pub fn revision(&self) -> u64 {
201        self.revision
202    }
203
204    /// Record a structural change made through a sibling mutation module.
205    pub(crate) fn bump_revision(&mut self) {
206        self.revision += 1;
207    }
208
209    /// The id [`Model::push`] would allocate next.
210    ///
211    /// A transaction reserves ids from here so several creates in one batch
212    /// cannot collide with each other or with existing entities.
213    pub fn next_id(&self) -> EntityId {
214        EntityId(self.max_id + 1)
215    }
216
217    /// Look up one entity, decoding it first if it was loaded lazily.
218    pub fn get(&self, id: EntityId) -> Option<&Entity> {
219        self.entities
220            .get(&id)
221            .map(|slot| slot.get(self.source.as_deref()))
222    }
223
224    /// Whether `id` names an entity, without decoding it.
225    pub fn contains(&self, id: EntityId) -> bool {
226        self.entities.contains_key(&id)
227    }
228
229    /// How many entities have been decoded. Equals [`Model::len`] for a
230    /// model built from decoded entities; for a lazily loaded one it counts
231    /// the entities accessed so far.
232    pub fn decoded_len(&self) -> usize {
233        self.entities
234            .values()
235            .filter(|slot| slot.is_decoded())
236            .count()
237    }
238
239    /// Decode every entity now, on up to `threads` threads.
240    ///
241    /// A lazily loaded model decodes on first access, one entity at a time.
242    /// A consumer about to touch most of the model -- a writer, a full
243    /// validation, a geometry pass -- can decode everything up front in
244    /// parallel instead. Idempotent, and a no-op on a decoded model.
245    pub fn decode_all(&self, threads: usize) {
246        let pending: Vec<&Slot> = self
247            .entities
248            .values()
249            .filter(|slot| !slot.is_decoded())
250            .collect();
251        if pending.is_empty() {
252            return;
253        }
254        let source = self.source.as_deref();
255        let threads = threads.clamp(1, pending.len());
256        if threads == 1 {
257            for slot in pending {
258                slot.get(source);
259            }
260            return;
261        }
262        let chunk = pending.len().div_ceil(threads);
263        std::thread::scope(|scope| {
264            for part in pending.chunks(chunk) {
265                scope.spawn(move || {
266                    for slot in part {
267                        slot.get(source);
268                    }
269                });
270            }
271        });
272    }
273
274    /// Number of entities.
275    pub fn len(&self) -> usize {
276        self.entities.len()
277    }
278
279    /// Whether the model holds no entities.
280    pub fn is_empty(&self) -> bool {
281        self.entities.is_empty()
282    }
283
284    /// Entity ids in original file order.
285    pub fn ids(&self) -> impl Iterator<Item = EntityId> + '_ {
286        self.order.iter().copied()
287    }
288
289    /// Entities in original file order.
290    pub fn iter(&self) -> impl Iterator<Item = (EntityId, &Entity)> + '_ {
291        self.order
292            .iter()
293            .filter_map(move |id| self.get(*id).map(|e| (*id, e)))
294    }
295
296    /// Ids of every entity with this exact type name, case-insensitive.
297    ///
298    /// This is an exact-type query and does **not** include subtypes: asking
299    /// for `IfcElement` will not return walls. Subtype queries need the schema,
300    /// which this crate does not depend on. The facade's
301    /// `ifc::ids_of_type_including_subtypes` (feature `schema`) joins the two.
302    pub fn ids_of_type(&self, type_name: &str) -> &[EntityId] {
303        self.by_type
304            .get(&type_name.to_ascii_uppercase())
305            .map(|v| v.as_slice())
306            .unwrap_or(&[])
307    }
308
309    /// Entities with this exact type name.
310    pub fn of_type<'a>(&'a self, type_name: &str) -> impl Iterator<Item = (EntityId, &'a Entity)> {
311        self.ids_of_type(type_name)
312            .iter()
313            .filter_map(move |id| self.get(*id).map(|e| (*id, e)))
314            .collect::<Vec<_>>()
315            .into_iter()
316    }
317
318    /// Every distinct type name present, with its instance count.
319    ///
320    /// Useful as a cheap file summary and as the basis for a coverage report
321    /// of what a given build can and cannot interpret.
322    pub fn type_histogram(&self) -> Vec<(&str, usize)> {
323        let mut v: Vec<_> = self
324            .by_type
325            .iter()
326            .map(|(k, ids)| (k.as_str(), ids.len()))
327            .collect();
328        v.sort_unstable_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
329        v
330    }
331
332    /// Ids that are referenced by some entity but do not exist.
333    ///
334    /// A dangling reference is the most common corruption in real files, and
335    /// it is a structural question, so it belongs here rather than in a
336    /// validation crate.
337    pub fn dangling_references(&self) -> Vec<(EntityId, EntityId)> {
338        let mut out = Vec::new();
339        for (id, entity) in self.iter() {
340            for target in entity.references() {
341                if !self.entities.contains_key(&target) {
342                    out.push((id, target));
343                }
344            }
345        }
346        out
347    }
348
349    // --- crate-internal seams for `mutation::edit` -----------------------
350    //
351    // Kept private-to-crate rather than `pub`: an edit that changes
352    // `type_name` must also fix up `by_type`, or `ids_of_type` silently goes
353    // stale. `mutation::edit` is the only module trusted to touch these
354    // fields directly, and it exists precisely to keep that invariant in one
355    // place instead of copied into every editor.
356
357    /// The entity under `id` for editing, decoded first when needed.
358    pub(crate) fn entity_mut(&mut self, id: EntityId) -> Option<&mut Entity> {
359        let source = self.source.clone();
360        Some(self.entities.get_mut(&id)?.get_mut(source.as_deref()))
361    }
362
363    /// Takes the entity under `id` out of storage, decoded; the caller
364    /// fixes up `by_type` and `order`.
365    pub(crate) fn take_entity(&mut self, id: EntityId) -> Option<Entity> {
366        let slot = self.entities.remove(&id)?;
367        Some(slot.into_entity(self.source.as_deref()))
368    }
369
370    pub(crate) fn by_type_mut(&mut self) -> &mut AHashMap<String, Vec<EntityId>> {
371        &mut self.by_type
372    }
373
374    pub(crate) fn order_mut(&mut self) -> &mut Vec<EntityId> {
375        &mut self.order
376    }
377}