ifc_lite_server/services/data_model/spatial_elevation.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//! Storey-elevation extraction, split out of `spatial.rs` (#3965 follow-up):
6//! reading `IfcBuildingStorey.Elevation`, with the `ObjectPlacement` Z
7//! fallback, is a self-contained concern the tree-building code merely calls
8//! into. `spatial_tests.rs` (attached to the parent `spatial` module, not
9//! this one) exercises these functions directly via `use super::*`.
10
11use ifc_lite_core::EntityDecoder;
12
13/// Attribute index of `IfcBuildingStorey.Elevation`, the same in IFC2X3 and IFC4:
14///
15/// ```text
16/// [0] GlobalId [1] OwnerHistory [2] Name [3] Description
17/// [4] ObjectType [5] ObjectPlacement [6] Representation [7] LongName
18/// [8] CompositionType [9] Elevation
19/// ```
20///
21/// This MUST stay in step with `IFC_BUILDING_STOREY_ELEVATION_INDEX` in
22/// `packages/data/src/storey-elevation.ts` — the two paths must read the same
23/// slot (issue #1841). It previously read 8 (CompositionType, an enum) and fell
24/// back to 7 (LongName, a string): both yield no float, so every storey reported
25/// no elevation and the UI showed 0.
26const STOREY_ELEVATION_INDEX: usize = 9;
27
28/// Attribute index of `IfcBuildingStorey.ObjectPlacement`.
29const STOREY_PLACEMENT_INDEX: usize = 5;
30
31/// `IfcLocalPlacement.RelativePlacement` - the storey's own offset from its
32/// parent spatial container.
33const LOCAL_PLACEMENT_RELATIVE_INDEX: usize = 1;
34
35/// `IfcAxis2Placement3D.Location` - an `IfcCartesianPoint`.
36const AXIS_PLACEMENT_LOCATION_INDEX: usize = 0;
37
38/// Extract elevation from an IFCBUILDINGSTOREY entity, in metres.
39///
40/// Mirrors `SpatialHierarchyBuilder.extractElevation` in `@ifc-lite/parser`:
41/// read `Elevation`, and when it is null (common in Revit / ArchiCAD exports,
42/// #1289) fall back to the Z of the storey's own `ObjectPlacement`. Both results
43/// are raw IFC lengths, so the unit scale applies either way.
44pub(super) fn extract_elevation_if_storey(
45 type_name: &str,
46 entity_id: u32,
47 decoder: &mut EntityDecoder,
48 length_unit_scale: f64,
49) -> Option<f64> {
50 if !type_name.eq_ignore_ascii_case("IFCBUILDINGSTOREY") {
51 return None;
52 }
53
54 let entity = decoder.decode_by_id(entity_id).ok()?;
55
56 // Read ONLY the Elevation slot. Scanning for "some attribute that parses as
57 // a number" would pick up entity references (bare express ids) as bogus
58 // elevations, which is the trap `@ifc-lite/parser` documents at #1289.
59 let raw = match entity.get_float(STOREY_ELEVATION_INDEX) {
60 Some(elevation) => elevation,
61 None => {
62 let placement_id = entity.get_ref(STOREY_PLACEMENT_INDEX)?;
63 extract_placement_elevation(placement_id, decoder)?
64 }
65 };
66
67 Some(raw * length_unit_scale)
68}
69
70/// Resolve a storey's Z from its `ObjectPlacement`, following
71/// `IfcLocalPlacement -> RelativePlacement (IfcAxis2Placement3D) -> Location
72/// (IfcCartesianPoint).Coordinates[2]`.
73///
74/// This is the placement RELATIVE to the parent spatial container, matching the
75/// semantics of the `Elevation` attribute — deliberately not the absolute world
76/// Z, so site-level georeferencing is not folded in. Returns the raw (unscaled)
77/// value, or `None` when the chain cannot be resolved.
78fn extract_placement_elevation(placement_id: u32, decoder: &mut EntityDecoder) -> Option<f64> {
79 let placement = decoder.decode_by_id(placement_id).ok()?;
80 let axis_id = placement.get_ref(LOCAL_PLACEMENT_RELATIVE_INDEX)?;
81
82 let axis = decoder.decode_by_id(axis_id).ok()?;
83 let location_id = axis.get_ref(AXIS_PLACEMENT_LOCATION_INDEX)?;
84
85 let (_x, _y, z) = decoder.get_cartesian_point_fast(location_id)?;
86 Some(z)
87}