Skip to main content

ifc_lite_core/decoder/
schema_layout.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Source-schema attribute layout and the IfcGridAxis -> IfcGrid inverse.
6//!
7//! The canonical [`crate::IfcType`] is the IFC4X3 universe, but an entity's
8//! attribute POSITIONS are defined by the schema the file declares. Most
9//! geometry entities share one layout across IFC2X3/IFC4/IFC4X3, so readers
10//! index them directly; the few that moved (IfcGridPlacement gained the
11//! inherited `PlacementRelTo` in IFC4X1) must ask here instead of hard-coding
12//! an offset, or an IFC2X3/IFC4 file is read one slot out (#6232 F2).
13
14use super::{EntityDecoder, EntityScanner};
15use crate::generated::schema_registry::SchemaVersion;
16use rustc_hash::FxHashMap;
17use std::sync::{Arc, OnceLock};
18
19/// Source-scoped, lazily built `IfcGridAxis` id -> owning `IfcGrid` id map.
20///
21/// Opaque and shareable: every decoder over one source may hold the same
22/// `Arc<GridAxisIndex>` ([`EntityDecoder::set_grid_axis_index`]), so the one
23/// scan that builds it is paid once per source rather than once per
24/// per-element decoder. The columnar and dense entity indexes carry one and
25/// install it with themselves.
26#[derive(Debug, Default)]
27pub struct GridAxisIndex(OnceLock<FxHashMap<u32, u32>>);
28
29impl EntityDecoder<'_> {
30    /// The EXPRESS schema this source declares in its `FILE_SCHEMA` header,
31    /// read from a bounded header window on first use and cached. `None` when
32    /// no declared identifier names a bundled schema family.
33    pub(crate) fn schema_version(&mut self) -> Option<SchemaVersion> {
34        if let Some(cached) = self.schema_version_cache {
35            return cached;
36        }
37        let version = crate::parser::header::declared_schemas_bounded(self.content)
38            .iter()
39            .find_map(|label| SchemaVersion::from_file_schema(label));
40        self.schema_version_cache = Some(version);
41        version
42    }
43
44    /// Position of `attribute` in `entity_name`'s record under this source's
45    /// schema. A source whose schema is undeclared or unknown is read with the
46    /// canonical IFC4X3 layout (the same universe [`crate::IfcType`] uses).
47    /// `None` when the entity or attribute does not exist in that schema, e.g.
48    /// `IfcGridPlacement.PlacementRelTo` on IFC2X3/IFC4.
49    pub fn attribute_index(&mut self, entity_name: &str, attribute: &str) -> Option<usize> {
50        let schema = self.schema_version().unwrap_or(SchemaVersion::Ifc4x3);
51        schema
52            .attribute_names(entity_name)?
53            .iter()
54            .position(|name| name.eq_ignore_ascii_case(attribute))
55    }
56
57    /// The `IfcGrid` that lists `axis_id` among its `UAxes`/`VAxes`/`WAxes`
58    /// (the inverse `PartOfU`/`PartOfV`/`PartOfW`). An axis claimed by more
59    /// than one grid resolves to the first in file order.
60    ///
61    /// Built by one scan over the source the first time a caller asks (nothing
62    /// asks unless it met an IFC2X3/IFC4 grid placement), into this decoder's
63    /// [`GridAxisIndex`], which may be shared across decoders of one source.
64    pub fn grid_of_axis(&mut self, axis_id: u32) -> Option<u32> {
65        let slots: Vec<usize> = ["UAxes", "VAxes", "WAxes"]
66            .iter()
67            .filter_map(|name| self.attribute_index("IfcGrid", name))
68            .collect();
69        self.grid_axis_index
70            .0
71            .get_or_init(|| self.build_grid_axis_owners(&slots))
72            .get(&axis_id)
73            .copied()
74    }
75
76    /// Share one source's [`GridAxisIndex`] with this decoder. The index must
77    /// belong to this decoder's source bytes; installing a columnar or dense
78    /// entity index installs that index's own.
79    pub fn set_grid_axis_index(&mut self, index: Arc<GridAxisIndex>) {
80        self.grid_axis_index = index;
81    }
82
83    fn build_grid_axis_owners(&self, axis_slots: &[usize]) -> FxHashMap<u32, u32> {
84        let mut owners = FxHashMap::default();
85        let mut scanner = EntityScanner::new(self.content);
86        while let Some((id, type_name, start, end)) = scanner.next_entity() {
87            // Raw keyword slice; STEP keywords are case-insensitive (#4497).
88            if !type_name.eq_ignore_ascii_case("IFCGRID") {
89                continue;
90            }
91            let Ok(grid) = self.decode_at_uncached(start, end) else {
92                continue;
93            };
94            for &slot in axis_slots {
95                for axis in grid.get_refs(slot).unwrap_or_default() {
96                    owners.entry(axis).or_insert(id);
97                }
98            }
99        }
100        owners
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::GridAxisIndex;
107    use crate::{ColumnarEntityIndex, EntityDecoder};
108    use std::sync::Arc;
109
110    fn source(schema: &str) -> String {
111        format!(
112            "ISO-10303-21;\nHEADER;\nFILE_SCHEMA(('{schema}'));\nENDSEC;\nDATA;\n\
113#1=IFCGRIDAXIS('A',$,.T.);\n#2=IFCGRIDAXIS('1',$,.T.);\n#3=IFCGRIDAXIS('X',$,.T.);\n\
114#9=IfcGrid('g',$,$,$,$,$,$,(#1),(#2),$);\nENDSEC;\nEND-ISO-10303-21;\n"
115        )
116    }
117
118    #[test]
119    fn grid_placement_layout_follows_the_declared_schema() {
120        for (schema, rel_to, location, ref_dir) in [
121            ("IFC2X3", None, Some(0), Some(1)),
122            ("IFC4", None, Some(0), Some(1)),
123            ("IFC4X3_ADD2", Some(0), Some(1), Some(2)),
124            ("NOT_A_SCHEMA", Some(0), Some(1), Some(2)),
125        ] {
126            let content = source(schema);
127            let mut decoder = EntityDecoder::new(&content);
128            assert_eq!(
129                decoder.attribute_index("IfcGridPlacement", "PlacementRelTo"),
130                rel_to,
131                "{schema}"
132            );
133            assert_eq!(
134                decoder.attribute_index("IfcGridPlacement", "PlacementLocation"),
135                location,
136                "{schema}"
137            );
138            assert_eq!(
139                decoder.attribute_index("IfcGridPlacement", "PlacementRefDirection"),
140                ref_dir,
141                "{schema}"
142            );
143        }
144    }
145
146    #[test]
147    fn grid_of_axis_is_the_inverse_of_the_grid_axis_lists() {
148        let content = source("IFC4");
149        let mut decoder = EntityDecoder::new(&content);
150        assert_eq!(decoder.grid_of_axis(1), Some(9), "U axis");
151        assert_eq!(decoder.grid_of_axis(2), Some(9), "V axis");
152        assert_eq!(decoder.grid_of_axis(3), None, "an axis no grid lists");
153    }
154
155    #[test]
156    fn grid_axis_owners_are_shared_through_the_columnar_index() {
157        let content = source("IFC2X3");
158        let index = Arc::new(ColumnarEntityIndex::from_scan(content.as_bytes()));
159        let mut first = EntityDecoder::with_arc_columnar_index(&content, index.clone());
160        assert_eq!(first.grid_of_axis(1), Some(9));
161        drop(first);
162        assert!(
163            index.grid_axis_index.0.get().is_some(),
164            "built once per source, not per decoder"
165        );
166        let mut second = EntityDecoder::with_arc_columnar_index(&content, index);
167        assert_eq!(second.grid_of_axis(2), Some(9));
168    }
169
170    #[test]
171    fn a_shared_grid_axis_index_is_built_once_across_decoders() {
172        let content = source("IFC4");
173        let shared = Arc::new(GridAxisIndex::default());
174        let mut first = EntityDecoder::new(&content);
175        first.set_grid_axis_index(shared.clone());
176        assert_eq!(first.grid_of_axis(1), Some(9));
177        assert!(shared.0.get().is_some());
178        // A second decoder over the same source reads the shared map; prove it
179        // by handing it a source with no grid at all.
180        let empty = "ISO-10303-21;\nHEADER;\nFILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\nENDSEC;\n";
181        let mut second = EntityDecoder::new(empty);
182        second.set_grid_axis_index(shared);
183        assert_eq!(second.grid_of_axis(2), Some(9));
184    }
185}