ifc_lite_server/services/data_model/
material_units.rs1use 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 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 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(¤t) {
102 return scale;
103 }
104 if !visited.insert(current) {
105 return self.default_scale;
106 }
107 let next = self
110 .containment
111 .get(¤t)
112 .or_else(|| self.aggregation.get(¤t))
113 .or_else(|| {
114 self.type_objects
115 .get(¤t)
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}