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)]
23pub enum SpatialKind {
24 /// `IfcProject` -- the root. A conformant file has exactly one.
25 Project,
26 /// `IfcSite`.
27 Site,
28 /// `IfcBuilding`.
29 Building,
30 /// `IfcBuildingStorey`.
31 Storey,
32 /// `IfcSpace`.
33 Space,
34 /// Any other spatial element of the release: `IfcSpatialZone` and
35 /// `IfcExternalSpatialElement` (IFC4, IFC4X3), and the IFC4X3
36 /// facilities (`IfcFacility`, `IfcBridge`, `IfcRoad`, `IfcRailway`,
37 /// `IfcMarineFacility`) and facility parts (`IfcBridgePart`,
38 /// `IfcRoadPart`, `IfcRailwayPart`, `IfcMarinePart`,
39 /// `IfcFacilityPartCommon`).
40 OtherContainer,
41 /// Not a spatial container: a wall, a door, a slab, or any entity the
42 /// release does not declare as a spatial element.
43 Element,
44}
45
46impl SpatialKind {
47 /// Classify a STEP type name without a release: a container when any
48 /// bundled release (IFC2X3, IFC4, IFC4X3) declares it a spatial
49 /// element.
50 ///
51 /// No bundled release declares as a spatial element a name another
52 /// declares as something else (`tests/classification.rs` asserts it),
53 /// so this never contradicts [`classify_in`](Self::classify_in) for a
54 /// name the release declares. Prefer `classify_in` when the release is
55 /// known; [`SpatialTree`](crate::SpatialTree) does.
56 #[must_use]
57 pub fn classify(type_name: &str) -> Self {
58 Classifier::any_release().classify(type_name)
59 }
60
61 /// Classify a STEP type name against one release's schema table.
62 #[must_use]
63 pub fn classify_in(type_name: &str, release: SchemaVersion) -> Self {
64 Classifier::for_release(release).classify(type_name)
65 }
66
67 /// Whether entities of this kind can contain others.
68 #[must_use]
69 pub const fn is_container(self) -> bool {
70 !matches!(self, Self::Element)
71 }
72}
73
74/// The table(s) a classification is answered from.
75pub(crate) struct Classifier {
76 /// The bound release, if the file declares exactly one bundled release.
77 release: Option<SchemaVersion>,
78 tables: Vec<&'static Schema>,
79}
80
81impl Classifier {
82 /// The release `model` declares, when it names exactly one bundled
83 /// release; otherwise every bundled release.
84 pub(crate) fn for_model(model: &Model) -> Self {
85 match model.header().schema.as_slice() {
86 [token] => match SchemaVersion::from_header_token(token) {
87 Some(release) => Self::for_release(release),
88 None => Self::any_release(),
89 },
90 _ => Self::any_release(),
91 }
92 }
93
94 fn for_release(release: SchemaVersion) -> Self {
95 Self {
96 release: Some(release),
97 tables: for_version(release).into_iter().collect(),
98 }
99 }
100
101 fn any_release() -> Self {
102 let tables = [
103 SchemaVersion::Ifc2x3,
104 SchemaVersion::Ifc4,
105 SchemaVersion::Ifc4x3,
106 ]
107 .into_iter()
108 .filter_map(for_version)
109 .collect();
110 Self {
111 release: None,
112 tables,
113 }
114 }
115
116 /// The release bound, or `None` when every bundled release is asked.
117 pub(crate) fn bound_release(&self) -> Option<SchemaVersion> {
118 self.release
119 }
120
121 pub(crate) fn classify(&self, type_name: &str) -> SpatialKind {
122 let upper = type_name.to_ascii_uppercase();
123 if !self.tables.iter().any(|table| is_spatial(table, &upper)) {
124 return SpatialKind::Element;
125 }
126 match upper.as_str() {
127 "IFCPROJECT" => SpatialKind::Project,
128 "IFCSITE" => SpatialKind::Site,
129 "IFCBUILDING" => SpatialKind::Building,
130 "IFCBUILDINGSTOREY" => SpatialKind::Storey,
131 "IFCSPACE" => SpatialKind::Space,
132 _ => SpatialKind::OtherContainer,
133 }
134 }
135}
136
137/// Whether `table` declares `upper` as the project or a spatial element.
138fn is_spatial(table: &Schema, upper: &str) -> bool {
139 // IfcProject is an IfcContext (IFC4, IFC4X3) or an IfcObject (IFC2X3),
140 // not a spatial element, but it is the root every tree hangs from.
141 if upper == "IFCPROJECT" {
142 return table.is_a(upper, "IFCPROJECT");
143 }
144 // IFC2X3 has no IfcSpatialElement; its spatial root is
145 // IfcSpatialStructureElement. Asking for an undeclared ancestor
146 // answers false, so the IFC4 root is tried first and the IFC2X3 one
147 // only matters where it is the root.
148 table.is_a(upper, "IFCSPATIALELEMENT") || table.is_a(upper, "IFCSPATIALSTRUCTUREELEMENT")
149}