1use crate::id::GlobalId;
4use crate::property::PropertySets;
5use crate::representation::Representation;
6use glam::{DMat4, DVec3};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14pub enum IfcClass {
15 Project,
17 Site,
18 Building,
19 BuildingStorey,
20 Space,
21 Wall,
23 Slab,
24 Roof,
25 Column,
26 Beam,
27 Door,
28 Window,
29 Stair,
30 Covering,
31 Furniture,
32 OpeningElement,
33 BuildingElementProxy,
34 Other(String),
36}
37
38impl IfcClass {
39 pub fn ifc_name(&self) -> &str {
41 match self {
42 Self::Project => "IfcProject",
43 Self::Site => "IfcSite",
44 Self::Building => "IfcBuilding",
45 Self::BuildingStorey => "IfcBuildingStorey",
46 Self::Space => "IfcSpace",
47 Self::Wall => "IfcWall",
48 Self::Slab => "IfcSlab",
49 Self::Roof => "IfcRoof",
50 Self::Column => "IfcColumn",
51 Self::Beam => "IfcBeam",
52 Self::Door => "IfcDoor",
53 Self::Window => "IfcWindow",
54 Self::Stair => "IfcStair",
55 Self::Covering => "IfcCovering",
56 Self::Furniture => "IfcFurniture",
57 Self::OpeningElement => "IfcOpeningElement",
58 Self::BuildingElementProxy => "IfcBuildingElementProxy",
59 Self::Other(name) => name,
60 }
61 }
62
63 pub fn is_spatial(&self) -> bool {
72 match self {
73 Self::Project | Self::Site | Self::Building | Self::BuildingStorey | Self::Space => {
74 true
75 }
76 Self::Other(name) => is_spatial_entity(name),
77 _ => false,
78 }
79 }
80
81 pub fn is_structure_spine(&self) -> bool {
88 matches!(
89 self,
90 Self::Project | Self::Site | Self::Building | Self::BuildingStorey
91 )
92 }
93
94 pub fn can_host_openings(&self) -> bool {
96 matches!(
97 self,
98 Self::Wall | Self::Slab | Self::Roof | Self::Column | Self::Beam
99 )
100 }
101}
102
103fn is_spatial_entity(name: &str) -> bool {
108 matches!(
109 name.to_ascii_uppercase().as_str(),
110 "IFCSPATIALELEMENT"
111 | "IFCSPATIALSTRUCTUREELEMENT"
112 | "IFCSPATIALZONE"
113 | "IFCEXTERNALSPATIALELEMENT"
114 | "IFCEXTERNALSPATIALSTRUCTUREELEMENT"
115 | "IFCFACILITY"
116 | "IFCFACILITYPART"
117 | "IFCFACILITYPARTCOMMON"
118 | "IFCBRIDGE"
119 | "IFCBRIDGEPART"
120 | "IFCROAD"
121 | "IFCROADPART"
122 | "IFCRAILWAY"
123 | "IFCRAILWAYPART"
124 | "IFCMARINEFACILITY"
125 | "IFCMARINEPART"
126 | "IFCTUNNEL"
127 | "IFCTUNNELPART"
128 )
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
138pub struct Placement {
139 pub location: DVec3,
140 pub axis: DVec3,
142 pub ref_direction: DVec3,
144}
145
146impl Placement {
147 pub fn identity() -> Self {
149 Self {
150 location: DVec3::ZERO,
151 axis: DVec3::Z,
152 ref_direction: DVec3::X,
153 }
154 }
155
156 pub fn at(location: DVec3) -> Self {
157 Self {
158 location,
159 ..Self::identity()
160 }
161 }
162
163 pub fn new(location: DVec3, axis: DVec3, ref_direction: DVec3) -> Self {
168 let z = axis.try_normalize().unwrap_or(DVec3::Z);
169 let x = (ref_direction - z * z.dot(ref_direction))
170 .try_normalize()
171 .unwrap_or_else(|| {
172 let candidate = if z.x.abs() < 0.9 { DVec3::X } else { DVec3::Y };
174 (candidate - z * z.dot(candidate))
175 .try_normalize()
176 .unwrap_or(DVec3::X)
177 });
178 Self {
179 location,
180 axis: z,
181 ref_direction: x,
182 }
183 }
184
185 pub fn to_matrix(self) -> DMat4 {
187 let z = self.axis;
188 let x = self.ref_direction;
189 let y = z.cross(x);
190 DMat4::from_cols(
191 x.extend(0.0),
192 y.extend(0.0),
193 z.extend(0.0),
194 self.location.extend(1.0),
195 )
196 }
197
198 pub fn from_matrix(matrix: DMat4) -> Self {
208 Self::new(
209 matrix.w_axis.truncate(),
210 matrix.z_axis.truncate(),
211 matrix.x_axis.truncate(),
212 )
213 }
214
215 pub fn translated(self, delta: DVec3) -> Self {
217 Self {
218 location: self.location + delta,
219 ..self
220 }
221 }
222}
223
224impl Default for Placement {
225 fn default() -> Self {
226 Self::identity()
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
232pub struct BoundingBox {
233 pub min: DVec3,
234 pub max: DVec3,
235}
236
237impl BoundingBox {
238 pub fn new(min: DVec3, max: DVec3) -> Self {
239 Self {
240 min: min.min(max),
241 max: min.max(max),
242 }
243 }
244
245 pub fn empty() -> Self {
247 Self {
248 min: DVec3::splat(f64::INFINITY),
249 max: DVec3::splat(f64::NEG_INFINITY),
250 }
251 }
252
253 pub fn from_points(points: impl IntoIterator<Item = DVec3>) -> Self {
254 points.into_iter().fold(Self::empty(), |b, p| b.extended(p))
255 }
256
257 pub fn is_empty(&self) -> bool {
258 self.min.x > self.max.x || self.min.y > self.max.y || self.min.z > self.max.z
259 }
260
261 pub fn extended(self, p: DVec3) -> Self {
262 Self {
263 min: self.min.min(p),
264 max: self.max.max(p),
265 }
266 }
267
268 pub fn union(self, other: Self) -> Self {
269 if self.is_empty() {
270 return other;
271 }
272 if other.is_empty() {
273 return self;
274 }
275 Self {
276 min: self.min.min(other.min),
277 max: self.max.max(other.max),
278 }
279 }
280
281 pub fn intersects(&self, other: &Self) -> bool {
282 !self.is_empty()
283 && !other.is_empty()
284 && self.min.x <= other.max.x
285 && self.max.x >= other.min.x
286 && self.min.y <= other.max.y
287 && self.max.y >= other.min.y
288 && self.min.z <= other.max.z
289 && self.max.z >= other.min.z
290 }
291
292 pub fn center(&self) -> DVec3 {
293 (self.min + self.max) * 0.5
294 }
295
296 pub fn size(&self) -> DVec3 {
297 (self.max - self.min).max(DVec3::ZERO)
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct ElementRecord {
308 pub global_id: GlobalId,
309 pub class: IfcClass,
310 pub name: Option<String>,
311 pub object_type: Option<String>,
312 pub placement: Placement,
313 pub container: Option<GlobalId>,
315 pub type_ref: Option<GlobalId>,
317 pub properties: PropertySets,
318 pub representation: Option<Representation>,
321 pub bounds: Option<BoundingBox>,
323 pub representation_revision: u64,
325 pub semantic_revision: u64,
327}
328
329impl ElementRecord {
330 pub fn new(global_id: GlobalId, class: IfcClass) -> Self {
331 Self {
332 global_id,
333 class,
334 name: None,
335 object_type: None,
336 placement: Placement::identity(),
337 container: None,
338 type_ref: None,
339 properties: PropertySets::default(),
340 representation: None,
341 bounds: None,
342 representation_revision: 0,
343 semantic_revision: 0,
344 }
345 }
346
347 pub fn with_name(mut self, name: impl Into<String>) -> Self {
348 self.name = Some(name.into());
349 self
350 }
351
352 pub fn with_placement(mut self, placement: Placement) -> Self {
353 self.placement = placement;
354 self
355 }
356
357 pub fn with_container(mut self, container: GlobalId) -> Self {
358 self.container = Some(container);
359 self
360 }
361
362 pub fn with_representation(mut self, representation: Representation) -> Self {
363 self.representation = Some(representation);
364 self
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn placement_orthogonalises_a_skewed_basis() {
374 let p = Placement::new(DVec3::ZERO, DVec3::Z * 3.0, DVec3::new(1.0, 0.0, 0.7));
375 assert!((p.axis.length() - 1.0).abs() < 1e-12);
376 assert!((p.ref_direction.length() - 1.0).abs() < 1e-12);
377 assert!(
378 p.axis.dot(p.ref_direction).abs() < 1e-12,
379 "axes must be orthogonal"
380 );
381 }
382
383 #[test]
384 fn placement_survives_degenerate_input() {
385 let p = Placement::new(DVec3::ONE, DVec3::ZERO, DVec3::ZERO);
387 assert!(p.axis.is_finite() && p.ref_direction.is_finite());
388 assert!(p.axis.dot(p.ref_direction).abs() < 1e-12);
389 }
390
391 #[test]
392 fn placement_matrix_is_right_handed() {
393 let p = Placement::new(DVec3::new(1.0, 2.0, 3.0), DVec3::Z, DVec3::X);
394 let m = p.to_matrix();
395 assert!((m.determinant() - 1.0).abs() < 1e-12);
396 assert_eq!(m.transform_point3(DVec3::ZERO), DVec3::new(1.0, 2.0, 3.0));
397 }
398
399 #[test]
400 fn a_placement_survives_a_matrix_round_trip() {
401 let original = Placement::new(
402 DVec3::new(3.0, -7.5, 2.25),
403 DVec3::new(0.0, 0.3, 1.0),
404 DVec3::new(1.0, 0.4, 0.0),
405 );
406 let recovered = Placement::from_matrix(original.to_matrix());
407
408 assert!((recovered.location - original.location).length() < 1e-12);
409 assert!((recovered.axis - original.axis).length() < 1e-12);
410 assert!((recovered.ref_direction - original.ref_direction).length() < 1e-12);
411 }
412
413 #[test]
414 fn composing_and_decomposing_recovers_a_relative_placement() {
415 let storey = Placement::new(DVec3::new(0.0, 0.0, 3.5), DVec3::Z, DVec3::X);
418 let relative = Placement::new(DVec3::new(2.0, 1.0, 0.0), DVec3::Z, DVec3::Y);
419
420 let world = storey.to_matrix() * relative.to_matrix();
421 let recovered = Placement::from_matrix(storey.to_matrix().inverse() * world);
422
423 assert!((recovered.location - relative.location).length() < 1e-12);
424 assert!((recovered.ref_direction - relative.ref_direction).length() < 1e-12);
425 }
426
427 #[test]
428 fn scale_is_discarded_rather_than_silently_kept() {
429 let scaled = DMat4::from_scale(DVec3::new(3.0, 3.0, 3.0));
432 let recovered = Placement::from_matrix(scaled);
433 assert!((recovered.axis.length() - 1.0).abs() < 1e-12);
434 assert!((recovered.ref_direction.length() - 1.0).abs() < 1e-12);
435 }
436
437 #[test]
438 fn empty_box_is_the_union_identity() {
439 let b = BoundingBox::new(DVec3::ZERO, DVec3::ONE);
440 assert!(BoundingBox::empty().is_empty());
441 assert_eq!(BoundingBox::empty().union(b), b);
442 assert_eq!(b.union(BoundingBox::empty()), b);
443 }
444
445 #[test]
446 fn boxes_touching_at_a_face_intersect() {
447 let a = BoundingBox::new(DVec3::ZERO, DVec3::ONE);
448 let b = BoundingBox::new(DVec3::X, DVec3::new(2.0, 1.0, 1.0));
449 let far = BoundingBox::new(DVec3::splat(5.0), DVec3::splat(6.0));
450 assert!(a.intersects(&b));
451 assert!(!a.intersects(&far));
452 assert!(!a.intersects(&BoundingBox::empty()));
453 }
454
455 #[test]
456 fn new_normalises_inverted_corners() {
457 let b = BoundingBox::new(DVec3::ONE, DVec3::ZERO);
458 assert_eq!(b.min, DVec3::ZERO);
459 assert_eq!(b.size(), DVec3::ONE);
460 }
461
462 #[test]
463 fn infrastructure_spatial_parts_count_as_containers() {
464 assert!(IfcClass::Other("IfcRoadPart".into()).is_spatial());
467 assert!(IfcClass::Other("IFCBRIDGEPART".into()).is_spatial());
468 assert!(IfcClass::Other("IfcFacilityPart".into()).is_spatial());
469 assert!(IfcClass::Space.is_spatial());
470
471 assert!(!IfcClass::Other("IfcSpaceHeater".into()).is_spatial());
473 assert!(!IfcClass::Other("IfcPipeSegment".into()).is_spatial());
474 assert!(!IfcClass::Wall.is_spatial());
475 }
476
477 #[test]
478 fn the_structure_spine_is_narrower_than_spatial() {
479 for class in [
482 IfcClass::Project,
483 IfcClass::Site,
484 IfcClass::Building,
485 IfcClass::BuildingStorey,
486 ] {
487 assert!(class.is_structure_spine() && class.is_spatial());
488 }
489 assert!(IfcClass::Space.is_spatial());
490 assert!(!IfcClass::Space.is_structure_spine());
491 assert!(!IfcClass::Other("IfcRoadPart".into()).is_structure_spine());
492 }
493
494 #[test]
495 fn unknown_classes_keep_their_name() {
496 let c = IfcClass::Other("IfcDistributionElement".into());
497 assert_eq!(c.ifc_name(), "IfcDistributionElement");
498 assert!(!c.can_host_openings());
499 assert!(IfcClass::Wall.can_host_openings());
500 assert!(IfcClass::BuildingStorey.is_spatial());
501 }
502}