ifc_lite_processing/style/
material.rs1use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
25use rustc_hash::FxHashMap;
26
27use super::TRANSPARENCY_ALPHA_THRESHOLD;
28
29const MAX_MATERIAL_RESOLVE_DEPTH: u8 = 4;
31
32pub fn build_element_material_colors(
43 material_def_reprs: &FxHashMap<u32, Vec<u32>>,
44 orphan_styled_items: &FxHashMap<u32, [f32; 4]>,
45 element_to_material: &FxHashMap<u32, u32>,
46 decoder: &mut EntityDecoder,
47) -> FxHashMap<u32, Vec<[f32; 4]>> {
48 if element_to_material.is_empty() || orphan_styled_items.is_empty() {
49 return FxHashMap::default();
50 }
51
52 let material_styles = build_material_style_index(material_def_reprs, orphan_styled_items, decoder);
53 if material_styles.is_empty() {
54 return FxHashMap::default();
55 }
56
57 let mut out: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
58 for (&element_id, &material_select_id) in element_to_material {
59 let mut colors: Vec<[f32; 4]> = Vec::new();
60 for material_id in resolve_material_ids(material_select_id, decoder) {
61 if let Some(mat_colors) = material_styles.get(&material_id) {
62 colors.extend(mat_colors);
63 }
64 }
65 if !colors.is_empty() {
66 out.insert(element_id, colors);
67 }
68 }
69 out
70}
71
72pub fn flatten_material_color_index(
77 material_styles: &FxHashMap<u32, Vec<[f32; 4]>>,
78) -> FxHashMap<u32, [f32; 4]> {
79 material_styles
80 .iter()
81 .filter_map(|(&mat_id, colors)| pick_opaque_first(colors).map(|c| (mat_id, c)))
82 .collect()
83}
84
85pub fn pick_opaque_first(colors: &[[f32; 4]]) -> Option<[f32; 4]> {
87 if colors.is_empty() {
88 return None;
89 }
90 Some(
91 colors
92 .iter()
93 .find(|c| c[3] >= TRANSPARENCY_ALPHA_THRESHOLD)
94 .copied()
95 .unwrap_or(colors[0]),
96 )
97}
98
99pub fn pick_material_style_for_submesh(
104 colors: &[[f32; 4]],
105 prefer_transparent: bool,
106) -> Option<[f32; 4]> {
107 if colors.is_empty() {
108 return None;
109 }
110 let matched = if prefer_transparent {
111 colors.iter().find(|c| c[3] < TRANSPARENCY_ALPHA_THRESHOLD)
112 } else {
113 colors.iter().find(|c| c[3] >= TRANSPARENCY_ALPHA_THRESHOLD)
114 };
115 Some(matched.copied().unwrap_or(colors[0]))
116}
117
118pub fn resolve_submesh_color(
133 direct_color: Option<[f32; 4]>,
134 material_colors: Option<&[[f32; 4]]>,
135 mat_color_idx: &mut usize,
136 element_color: [f32; 4],
137) -> [f32; 4] {
138 if let Some(color) = direct_color {
139 return color;
140 }
141 if let Some(colors) = material_colors {
142 let prefer_transparent = *mat_color_idx % 2 == 0;
143 *mat_color_idx += 1;
144 if let Some(color) = pick_material_style_for_submesh(colors, prefer_transparent) {
145 return color;
146 }
147 }
148 element_color
149}
150
151pub fn build_material_style_index(
154 material_def_reprs: &FxHashMap<u32, Vec<u32>>,
155 orphan_styled_items: &FxHashMap<u32, [f32; 4]>,
156 decoder: &mut EntityDecoder,
157) -> FxHashMap<u32, Vec<[f32; 4]>> {
158 let mut material_styles: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
159 for (&material_id, styled_repr_ids) in material_def_reprs {
160 for &styled_repr_id in styled_repr_ids {
161 let Ok(styled_repr) = decoder.decode_by_id(styled_repr_id) else {
162 continue;
163 };
164 for styled_item_id in extract_refs_from_list(&styled_repr, 3) {
166 if let Some(&color) = orphan_styled_items.get(&styled_item_id) {
167 material_styles.entry(material_id).or_default().push(color);
168 }
169 }
170 }
171 }
172 material_styles
173}
174
175pub fn resolve_material_ids(material_select_id: u32, decoder: &mut EntityDecoder) -> Vec<u32> {
177 resolve_material_ids_inner(material_select_id, decoder, 0)
178}
179
180fn resolve_material_ids_inner(
181 material_select_id: u32,
182 decoder: &mut EntityDecoder,
183 depth: u8,
184) -> Vec<u32> {
185 if depth >= MAX_MATERIAL_RESOLVE_DEPTH {
186 return vec![];
187 }
188 let Ok(entity) = decoder.decode_by_id(material_select_id) else {
189 return vec![];
190 };
191 match entity.ifc_type {
192 IfcType::IfcMaterial => vec![material_select_id],
193 IfcType::IfcMaterialList => extract_refs_from_list(&entity, 0),
195 IfcType::IfcMaterialLayerSetUsage => entity
197 .get_ref(0)
198 .map(|id| resolve_material_ids_inner(id, decoder, depth + 1))
199 .unwrap_or_default(),
200 IfcType::IfcMaterialLayerSet => extract_nested_material_ids(&entity, 0, 0, decoder),
202 IfcType::IfcMaterialConstituentSet => extract_nested_material_ids(&entity, 2, 2, decoder),
204 IfcType::IfcMaterialProfileSet => extract_nested_material_ids(&entity, 2, 2, decoder),
206 IfcType::IfcMaterialProfileSetUsage | IfcType::IfcMaterialProfileSetUsageTapering => entity
207 .get_ref(0)
208 .map(|id| resolve_material_ids_inner(id, decoder, depth + 1))
209 .unwrap_or_default(),
210 _ => vec![],
211 }
212}
213
214fn extract_nested_material_ids(
217 entity: &DecodedEntity,
218 container_list_attr_idx: usize,
219 material_attr_idx: usize,
220 decoder: &mut EntityDecoder,
221) -> Vec<u32> {
222 let mut materials = Vec::new();
223 for container_id in extract_refs_from_list(entity, container_list_attr_idx) {
224 if let Ok(container) = decoder.decode_by_id(container_id) {
225 if let Some(mat_id) = container.get_ref(material_attr_idx) {
226 materials.push(mat_id);
227 }
228 }
229 }
230 materials
231}
232
233fn extract_refs_from_list(entity: &DecodedEntity, index: usize) -> Vec<u32> {
234 entity
235 .get(index)
236 .and_then(|attr| attr.as_list())
237 .map(|list| list.iter().filter_map(|v| v.as_entity_ref()).collect())
238 .unwrap_or_default()
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 const OPAQUE: [f32; 4] = [0.6, 0.6, 0.6, 1.0];
246 const GLASS: [f32; 4] = [0.5, 0.7, 0.9, 0.4];
247 const ELEMENT: [f32; 4] = [0.1, 0.1, 0.1, 1.0];
248
249 #[test]
250 fn submesh_direct_style_wins_without_touching_counter() {
251 let mut idx = 0usize;
252 let direct = [0.9, 0.2, 0.2, 1.0];
253 let colors = [OPAQUE, GLASS];
254 assert_eq!(
255 resolve_submesh_color(Some(direct), Some(&colors), &mut idx, ELEMENT),
256 direct
257 );
258 assert_eq!(idx, 0, "the alternation counter must not advance when a direct style wins");
259 }
260
261 #[test]
262 fn submesh_material_alternates_transparent_opaque() {
263 let colors = [OPAQUE, GLASS];
264 let mut idx = 0usize;
265 assert_eq!(resolve_submesh_color(None, Some(&colors), &mut idx, ELEMENT), GLASS);
267 assert_eq!(resolve_submesh_color(None, Some(&colors), &mut idx, ELEMENT), OPAQUE);
269 assert_eq!(idx, 2, "counter advances once per material-resolved sub-mesh");
270 }
271
272 #[test]
273 fn submesh_falls_back_to_element_color() {
274 let mut idx = 0usize;
275 assert_eq!(resolve_submesh_color(None, None, &mut idx, ELEMENT), ELEMENT);
276 assert_eq!(idx, 0, "no material list → counter untouched");
277 let empty: [[f32; 4]; 0] = [];
279 assert_eq!(resolve_submesh_color(None, Some(&empty), &mut idx, ELEMENT), ELEMENT);
280 }
281}