ifc_spatial/tree/kind.rs
1//! Where an entity sits in the spatial hierarchy.
2//!
3//! # From the declared release, not from the name
4//!
5//! A spatial container is an `IfcSpatialElement` (IFC4 ADD2 TC1, IFC4X3
6//! ADD2) or an `IfcSpatialStructureElement` (IFC2X3 TC1, which has no
7//! `IfcSpatialElement`), plus the `IfcProject` at the root. The set differs
8//! by release: IFC4 adds `IfcSpatialZone` and `IfcExternalSpatialElement`,
9//! IFC4X3 adds `IfcFacility` with `IfcBridge`, `IfcRoad`, `IfcRailway` and
10//! `IfcMarineFacility`, and the `IfcFacilityPart` subtypes. None of those
11//! names shares a pattern, so membership is the release's own subtype test
12//! (`Schema::is_a`) against the bundled table of the release the file
13//! declares.
14
15use ifc_model::Model;
16use ifc_schema::{for_version, Schema, SchemaVersion};
17
18/// The spatial role of an entity, as far as containment is concerned.
19///
20/// The five named kinds are those exact entities. Every other spatial
21/// element of the release is [`OtherContainer`](Self::OtherContainer).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23#[non_exhaustive]
24pub enum SpatialKind {
25 /// `IfcProject` -- the root. A conformant file has exactly one.
26 Project,
27 /// `IfcSite`.
28 Site,
29 /// `IfcBuilding`.
30 Building,
31 /// `IfcBuildingStorey`.
32 Storey,
33 /// `IfcSpace`.
34 Space,
35 /// Any other spatial element of the release: `IfcSpatialZone` and
36 /// `IfcExternalSpatialElement` (IFC4, IFC4X3), and the IFC4X3
37 /// facilities (`IfcFacility`, `IfcBridge`, `IfcRoad`, `IfcRailway`,
38 /// `IfcMarineFacility`) and facility parts (`IfcBridgePart`,
39 /// `IfcRoadPart`, `IfcRailwayPart`, `IfcMarinePart`,
40 /// `IfcFacilityPartCommon`).
41 OtherContainer,
42 /// Not a spatial container: a wall, a door, a slab, or any entity the
43 /// release does not declare as a spatial element.
44 Element,
45}
46
47impl SpatialKind {
48 /// Classify a STEP type name without a release: a container when any
49 /// bundled release (IFC2X3, IFC4, IFC4X3) declares it a spatial
50 /// element.
51 ///
52 /// No bundled release declares as a spatial element a name another
53 /// declares as something else (`tests/classification.rs` asserts it),
54 /// so this never contradicts [`classify_in`](Self::classify_in) for a
55 /// name the release declares. Prefer `classify_in` when the release is
56 /// known; [`SpatialTree`](crate::SpatialTree) does.
57 #[must_use]
58 pub fn classify(type_name: &str) -> Self {
59 Classifier::any_release().classify(type_name)
60 }
61
62 /// Classify a STEP type name against one release's schema table.
63 #[must_use]
64 pub fn classify_in(type_name: &str, release: SchemaVersion) -> Self {
65 Classifier::for_release(release).classify(type_name)
66 }
67
68 /// Whether entities of this kind can contain others.
69 #[must_use]
70 pub const fn is_container(self) -> bool {
71 !matches!(self, Self::Element)
72 }
73}
74
75/// Releases the spatial classification is verified against.
76const VERIFIED: [SchemaVersion; 3] = [
77 SchemaVersion::Ifc2x3,
78 SchemaVersion::Ifc4,
79 SchemaVersion::Ifc4x3,
80];
81
82/// The table(s) a classification is answered from.
83pub(crate) struct Classifier {
84 /// The bound release, if the file declares exactly one bundled release.
85 release: Option<SchemaVersion>,
86 tables: Vec<&'static Schema>,
87}
88
89impl Classifier {
90 /// The release `model` declares, when it names exactly one release this
91 /// classifier is verified for; otherwise every verified release, with
92 /// none bound.
93 ///
94 /// IFC4X1 and IFC4X2 are bundled by `ifc-schema` but not verified here,
95 /// so they bind nothing ([`Self::bound_release`] is `None`) rather than
96 /// being read as IFC4 or IFC4X3.
97 pub(crate) fn for_model(model: &Model) -> Self {
98 match model.header().schema.as_slice() {
99 [token] => match SchemaVersion::from_header_token(token) {
100 Some(release) if VERIFIED.contains(&release) => Self::for_release(release),
101 _ => Self::any_release(),
102 },
103 _ => Self::any_release(),
104 }
105 }
106
107 fn for_release(release: SchemaVersion) -> Self {
108 Self {
109 release: Some(release),
110 tables: for_version(release).into_iter().collect(),
111 }
112 }
113
114 fn any_release() -> Self {
115 let tables = VERIFIED
116 .into_iter()
117 .filter_map(|release| for_version(release).ok())
118 .collect();
119 Self {
120 release: None,
121 tables,
122 }
123 }
124
125 /// The release bound, or `None` when every bundled release is asked.
126 pub(crate) fn bound_release(&self) -> Option<SchemaVersion> {
127 self.release
128 }
129
130 pub(crate) fn classify(&self, type_name: &str) -> SpatialKind {
131 let upper = type_name.to_ascii_uppercase();
132 if !self.tables.iter().any(|table| is_spatial(table, &upper)) {
133 return SpatialKind::Element;
134 }
135 match upper.as_str() {
136 "IFCPROJECT" => SpatialKind::Project,
137 "IFCSITE" => SpatialKind::Site,
138 "IFCBUILDING" => SpatialKind::Building,
139 "IFCBUILDINGSTOREY" => SpatialKind::Storey,
140 "IFCSPACE" => SpatialKind::Space,
141 _ => SpatialKind::OtherContainer,
142 }
143 }
144}
145
146/// Whether `table` declares `upper` as the project or a spatial element.
147fn is_spatial(table: &Schema, upper: &str) -> bool {
148 // IfcProject is an IfcContext (IFC4, IFC4X3) or an IfcObject (IFC2X3),
149 // not a spatial element, but it is the root every tree hangs from.
150 if upper == "IFCPROJECT" {
151 return table.is_a(upper, "IFCPROJECT");
152 }
153 // IFC2X3 has no IfcSpatialElement; its spatial root is
154 // IfcSpatialStructureElement. Asking for an undeclared ancestor
155 // answers false, so the IFC4 root is tried first and the IFC2X3 one
156 // only matters where it is the root.
157 table.is_a(upper, "IFCSPATIALELEMENT") || table.is_a(upper, "IFCSPATIALSTRUCTUREELEMENT")
158}
159
160#[cfg(test)]
161mod intermediate_release_tests {
162 use super::*;
163
164 /// An IFC4X1 or IFC4X2 header binds no release: the classifier answers
165 /// from the verified tables with nothing bound, instead of claiming the
166 /// file was read as IFC4 or IFC4X3.
167 #[test]
168 fn ifc4x1_and_ifc4x2_bind_no_release() {
169 for token in ["IFC4X1", "IFC4X2"] {
170 let mut model = Model::new();
171 model.header_mut().schema = vec![token.to_owned()];
172 assert_eq!(
173 Classifier::for_model(&model).bound_release(),
174 None,
175 "{token}"
176 );
177 }
178 let mut model = Model::new();
179 model.header_mut().schema = vec!["IFC4X3".to_owned()];
180 assert_eq!(
181 Classifier::for_model(&model).bound_release(),
182 Some(SchemaVersion::Ifc4x3)
183 );
184 }
185}