ifc_lite_server/services/data_model/
material_units.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//! Per-owner length units for material layers in federated, multi-project IFC.
6
7use super::types::{EntityMetadata, Relationship};
8use ifc_lite_core::{EntityDecoder, ProjectUnits};
9use std::collections::{HashMap, HashSet};
10
11pub(super) struct MaterialUnitContext {
12    default_scale: f64,
13    project_scales: HashMap<u32, f64>,
14    containment: HashMap<u32, u32>,
15    aggregation: HashMap<u32, u32>,
16    type_objects: HashMap<u32, Vec<u32>>,
17    mixed_type_scales: HashSet<u32>,
18}
19
20impl MaterialUnitContext {
21    pub(super) fn new(
22        entities: &[EntityMetadata],
23        relationships: &[Relationship],
24        decoder: &mut EntityDecoder,
25        default_scale: f64,
26    ) -> Self {
27        let project_ids: Vec<u32> = entities
28            .iter()
29            .filter(|e| e.type_name.eq_ignore_ascii_case("IFCPROJECT"))
30            .map(|e| e.entity_id)
31            .collect();
32        let mut context = Self {
33            default_scale,
34            project_scales: HashMap::new(),
35            containment: HashMap::new(),
36            aggregation: HashMap::new(),
37            type_objects: HashMap::new(),
38            mixed_type_scales: HashSet::new(),
39        };
40        // The ordinary single-project file uses the caller's scale directly.
41        if project_ids.len() <= 1 {
42            return context;
43        }
44
45        for &project_id in &project_ids {
46            let scale = if project_id == project_ids[0] {
47                default_scale
48            } else {
49                ProjectUnits::resolve(decoder, project_id)
50                    .resolved_for_unit_type("LENGTHUNIT")
51                    .map_or(default_scale, |unit| unit.si_scale)
52            };
53            context.project_scales.insert(project_id, scale);
54        }
55        for rel in relationships {
56            let kind = rel.rel_type.as_str();
57            if kind.eq_ignore_ascii_case("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
58                context
59                    .containment
60                    .entry(rel.related_id)
61                    .or_insert(rel.relating_id);
62            } else if kind.eq_ignore_ascii_case("IFCRELAGGREGATES") {
63                context
64                    .aggregation
65                    .entry(rel.related_id)
66                    .or_insert(rel.relating_id);
67            } else if kind.eq_ignore_ascii_case("IFCRELDEFINESBYTYPE") {
68                context
69                    .type_objects
70                    .entry(rel.relating_id)
71                    .or_default()
72                    .push(rel.related_id);
73            }
74        }
75        // A type used by occurrences in different unit contexts has no
76        // single valid layer thickness. Keep its association unresolved until
77        // the wire can carry per-occurrence resolved material definitions.
78        context.mixed_type_scales = context
79            .type_objects
80            .iter()
81            .filter_map(|(&type_id, objects)| {
82                let mut scales = objects.iter().map(|&id| context.scale_for(id).to_bits());
83                let first = scales.next()?;
84                scales.any(|scale| scale != first).then_some(type_id)
85            })
86            .collect();
87        context
88    }
89
90    pub(super) fn has_mixed_type_scales(&self, entity_id: u32) -> bool {
91        self.mixed_type_scales.contains(&entity_id)
92    }
93
94    pub(super) fn scale_for(&self, entity_id: u32) -> f64 {
95        if self.project_scales.is_empty() {
96            return self.default_scale;
97        }
98        let mut visited = HashSet::new();
99        let mut current = entity_id;
100        loop {
101            if let Some(&scale) = self.project_scales.get(&current) {
102                return scale;
103            }
104            if !visited.insert(current) {
105                return self.default_scale;
106            }
107            // Match the raw parser's ownership walk: containment first, then
108            // aggregation, then a type's first defined occurrence (#3554).
109            let next = self
110                .containment
111                .get(&current)
112                .or_else(|| self.aggregation.get(&current))
113                .or_else(|| {
114                    self.type_objects
115                        .get(&current)
116                        .and_then(|objects| objects.first())
117                });
118            let Some(&next_id) = next else {
119                return self.default_scale;
120            };
121            current = next_id;
122        }
123    }
124}