1use crate::style::{FullIndexedColourMap, GeometryStyleInfo};
25use ifc_lite_core::{DecodedEntity, EntityDecoder};
26use rustc_hash::FxHashMap;
27
28pub type Span = (u32, usize, usize);
30
31#[derive(Debug, Default)]
34pub struct PrepassSpans {
35 pub styled_items: Vec<Span>,
39 pub indexed_colour_maps: Vec<Span>,
42 pub material_def_reprs: Vec<Span>,
44 pub rel_associates_material: Vec<Span>,
46 pub void_rels: Vec<Span>,
48 pub fills_rels: Vec<Span>,
51 pub aggregate_rels: Vec<Span>,
54}
55
56#[derive(Debug, Clone, Copy)]
59pub struct ResolveOptions {
60 pub collect_indexed_colour_full: bool,
65 pub defer_attached_styles: bool,
71}
72
73impl Default for ResolveOptions {
74 fn default() -> Self {
75 Self {
76 collect_indexed_colour_full: true,
77 defer_attached_styles: false,
78 }
79 }
80}
81
82#[derive(Debug, Default)]
84pub struct ResolvedPrepass {
85 pub geometry_style_index: FxHashMap<u32, GeometryStyleInfo>,
89 pub indexed_colour_index: FxHashMap<u32, [f32; 4]>,
91 pub indexed_colour_full: FxHashMap<u32, FullIndexedColourMap>,
94 pub orphan_styled_items: FxHashMap<u32, [f32; 4]>,
96 pub material_def_reprs: FxHashMap<u32, Vec<u32>>,
98 pub element_to_material: FxHashMap<u32, u32>,
100 pub element_material_colors: FxHashMap<u32, Vec<[f32; 4]>>,
103 pub void_index: FxHashMap<u32, Vec<u32>>,
105 pub filling_by_opening: FxHashMap<u32, u32>,
107 pub deferred_attached_styled_spans: Vec<(usize, usize)>,
111}
112
113pub fn resolve_prepass(
116 spans: &PrepassSpans,
117 decoder: &mut EntityDecoder,
118 opts: ResolveOptions,
119) -> ResolvedPrepass {
120 let mut out = ResolvedPrepass::default();
121
122 for &(id, start, end) in &spans.styled_items {
124 let Ok(styled_item) = decoder.decode_at_with_id(id, start, end) else {
125 if opts.defer_attached_styles {
126 out.deferred_attached_styled_spans.push((start, end));
129 }
130 continue;
131 };
132 if styled_item.get_ref(0).is_none() {
133 if let Some(info) = extract_style_info_from_styled_item(&styled_item, decoder) {
137 out.orphan_styled_items.insert(id, info.color);
138 }
139 } else if opts.defer_attached_styles {
140 out.deferred_attached_styled_spans.push((start, end));
141 } else {
142 collect_geometry_style_info(&mut out.geometry_style_index, &styled_item, decoder);
143 }
144 }
145
146 for &(id, start, end) in &spans.indexed_colour_maps {
148 let Ok(icm) = decoder.decode_at_with_id(id, start, end) else {
149 continue;
150 };
151 let Some(full) = crate::style::resolve_indexed_colour_map_full(&icm, decoder) else {
152 continue;
153 };
154 let geometry_id = full.geometry_id;
155 out.indexed_colour_index
156 .entry(geometry_id)
157 .or_insert(full.dominant().to_array());
158 if opts.collect_indexed_colour_full {
159 out.indexed_colour_full.entry(geometry_id).or_insert(full);
160 }
161 }
162
163 for &(id, start, end) in &spans.material_def_reprs {
165 if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
166 if let Some(material_id) = entity.get_ref(3) {
168 if let Some(reprs) = refs_from_list(&entity, 2) {
169 out.material_def_reprs
170 .entry(material_id)
171 .or_default()
172 .extend(reprs);
173 }
174 }
175 }
176 }
177 for &(id, start, end) in &spans.rel_associates_material {
178 if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
179 if let Some(material_select_id) = entity.get_ref(5) {
181 if let Some(related) = refs_from_list(&entity, 4) {
182 for element_id in related {
183 out.element_to_material.insert(element_id, material_select_id);
184 }
185 }
186 }
187 }
188 }
189
190 for &(id, start, end) in &spans.void_rels {
192 if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
193 if let (Some(host), Some(opening)) = (entity.get_ref(4), entity.get_ref(5)) {
194 out.void_index.entry(host).or_default().push(opening);
195 }
196 }
197 }
198 for &(id, start, end) in &spans.fills_rels {
199 if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
200 if let (Some(opening_id), Some(filling_id)) = (entity.get_ref(4), entity.get_ref(5)) {
202 out.filling_by_opening.insert(opening_id, filling_id);
203 }
204 }
205 }
206 if !out.void_index.is_empty() && !spans.aggregate_rels.is_empty() {
207 let mut aggregate_children: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
208 for &(id, start, end) in &spans.aggregate_rels {
209 if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
210 let Some(parent_id) = entity.get_ref(4) else {
211 continue;
212 };
213 if let Some(children) = refs_from_list(&entity, 5) {
214 aggregate_children
215 .entry(parent_id)
216 .or_default()
217 .extend(children);
218 }
219 }
220 }
221 ifc_lite_geometry::propagate_voids_via_aggregates(
222 &mut out.void_index,
223 &aggregate_children,
224 );
225 }
226
227 out.element_material_colors = crate::style::build_element_material_colors(
229 &out.material_def_reprs,
230 &out.orphan_styled_items,
231 &out.element_to_material,
232 decoder,
233 );
234
235 out
236}
237
238pub fn resolve_styled_item_spans(
242 spans: &[(usize, usize)],
243 decoder: &mut EntityDecoder,
244) -> FxHashMap<u32, GeometryStyleInfo> {
245 let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
246 for &(start, end) in spans {
247 if let Ok(styled_item) = decoder.decode_at(start, end) {
248 if styled_item.get_ref(0).is_some() {
249 collect_geometry_style_info(&mut styles, &styled_item, decoder);
250 }
251 }
252 }
253 styles
254}
255
256pub fn merge_indexed_colours(
261 geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
262 indexed_colours: &FxHashMap<u32, [f32; 4]>,
263) {
264 for (&geometry_id, &color) in indexed_colours {
265 geometry_styles
266 .entry(geometry_id)
267 .or_insert_with(|| GeometryStyleInfo::from_color(color));
268 }
269}
270
271#[derive(Debug, Clone, Copy)]
273pub struct UnitScales {
274 pub length_unit_scale: f64,
276 pub plane_angle_to_radians: f64,
278 pub project_id: Option<u32>,
280}
281
282impl Default for UnitScales {
283 fn default() -> Self {
284 Self {
285 length_unit_scale: 1.0,
286 plane_angle_to_radians: 1.0,
287 project_id: None,
288 }
289 }
290}
291
292pub fn resolve_unit_scales(
308 content: &[u8],
309 project_id_hint: Option<u32>,
310 decoder: &mut EntityDecoder,
311) -> UnitScales {
312 let project_id = project_id_hint.or_else(|| find_ifcproject_id(content));
313 let Some(pid) = project_id else {
314 return UnitScales::default();
315 };
316
317 let length = ifc_lite_core::try_extract_length_unit_scale(decoder, pid);
319 let angle = ifc_lite_core::extract_plane_angle_to_radians(decoder, pid).ok();
320
321 if let (Some(length_unit_scale), Some(plane_angle_to_radians)) = (length, angle) {
322 return UnitScales {
323 length_unit_scale,
324 plane_angle_to_radians,
325 project_id,
326 };
327 }
328
329 let full_index = ifc_lite_core::build_entity_index(content);
331 let mut full_decoder = EntityDecoder::with_index(content, full_index);
332 UnitScales {
333 length_unit_scale: length.or_else(|| {
334 ifc_lite_core::extract_length_unit_scale(&mut full_decoder, pid).ok()
335 })
336 .unwrap_or(1.0),
337 plane_angle_to_radians: angle
338 .or_else(|| {
339 ifc_lite_core::extract_plane_angle_to_radians(&mut full_decoder, pid).ok()
340 })
341 .unwrap_or(1.0),
342 project_id,
343 }
344}
345
346pub fn find_ifcproject_id(content: &[u8]) -> Option<u32> {
349 let mut from = 0usize;
350 while let Some(rel) = memchr::memmem::find(&content[from..], b"=IFCPROJECT(") {
351 let eq = from + rel;
352 let mut i = eq;
354 while i > 0 && content[i - 1].is_ascii_digit() {
355 i -= 1;
356 }
357 if i > 0 && content[i - 1] == b'#' && i < eq {
358 let mut id: u32 = 0;
359 for &b in &content[i..eq] {
360 id = id.wrapping_mul(10).wrapping_add((b - b'0') as u32);
361 }
362 return Some(id);
363 }
364 from = eq + 1;
367 }
368 None
369}
370
371pub fn flat_styles_rgba8(resolved: &ResolvedPrepass, decoder: &mut EntityDecoder) -> (Vec<u32>, Vec<u8>) {
377 let mut merged: FxHashMap<u32, [f32; 4]> = resolved
378 .geometry_style_index
379 .iter()
380 .map(|(&id, info)| (id, info.color))
381 .collect();
382 for (&geometry_id, &color) in &resolved.indexed_colour_index {
383 merged.entry(geometry_id).or_insert(color);
384 }
385 let material_styles = crate::style::build_material_style_index(
388 &resolved.material_def_reprs,
389 &resolved.orphan_styled_items,
390 decoder,
391 );
392 for (&mat_id, &color) in crate::style::flatten_material_color_index(&material_styles).iter() {
393 merged.entry(mat_id).or_insert(color);
394 }
395 for (&element_id, colors) in &resolved.element_material_colors {
396 if let Some(&color) = colors.first() {
397 merged.entry(element_id).or_insert(color);
398 }
399 }
400
401 let mut ids: Vec<u32> = Vec::with_capacity(merged.len());
402 let mut rgba: Vec<u8> = Vec::with_capacity(merged.len() * 4);
403 for (&id, &color) in &merged {
404 ids.push(id);
405 rgba.extend_from_slice(&crate::style::Rgba::from_array(color).to_rgba8());
406 }
407 (ids, rgba)
408}
409
410pub fn flat_voids(void_index: &FxHashMap<u32, Vec<u32>>) -> (Vec<u32>, Vec<u32>, Vec<u32>) {
413 let mut keys: Vec<u32> = Vec::with_capacity(void_index.len());
414 let mut counts: Vec<u32> = Vec::with_capacity(void_index.len());
415 let mut values: Vec<u32> = Vec::new();
416 for (&host_id, openings) in void_index {
417 keys.push(host_id);
418 counts.push(openings.len() as u32);
419 values.extend(openings.iter().copied());
420 }
421 (keys, counts, values)
422}
423
424pub fn flat_material_colors(
428 element_material_colors: &FxHashMap<u32, Vec<[f32; 4]>>,
429) -> (Vec<u32>, Vec<u32>, Vec<u8>) {
430 let mut ids: Vec<u32> = Vec::with_capacity(element_material_colors.len());
431 let mut counts: Vec<u32> = Vec::with_capacity(element_material_colors.len());
432 let mut rgba: Vec<u8> = Vec::new();
433 for (&element_id, colors) in element_material_colors {
434 if colors.is_empty() {
435 continue;
436 }
437 ids.push(element_id);
438 counts.push(colors.len() as u32);
439 for &c in colors {
440 rgba.extend_from_slice(&crate::style::Rgba::from_array(c).to_rgba8());
441 }
442 }
443 (ids, counts, rgba)
444}
445
446pub fn material_colors_from_flat(
449 element_ids: &[u32],
450 counts: &[u32],
451 rgba: &[u8],
452) -> FxHashMap<u32, Vec<[f32; 4]>> {
453 let mut out: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
454 let mut offset = 0usize;
455 for (i, &element_id) in element_ids.iter().enumerate() {
456 let Some(&count) = counts.get(i) else { break };
457 let count = count as usize;
458 let mut colors: Vec<[f32; 4]> = Vec::with_capacity(count);
459 for c in 0..count {
460 let base = (offset + c) * 4;
461 if base + 3 >= rgba.len() {
462 break;
463 }
464 colors.push(
465 crate::style::Rgba::from_rgba8([
466 rgba[base],
467 rgba[base + 1],
468 rgba[base + 2],
469 rgba[base + 3],
470 ])
471 .to_array(),
472 );
473 }
474 offset += count;
475 if !colors.is_empty() {
476 out.insert(element_id, colors);
477 }
478 }
479 out
480}
481
482pub(crate) fn collect_geometry_style_info(
487 geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
488 styled_item: &DecodedEntity,
489 decoder: &mut EntityDecoder,
490) {
491 let Some(geometry_id) = styled_item.get_ref(0) else {
492 return;
493 };
494 if geometry_styles.contains_key(&geometry_id) {
495 return;
496 }
497 if let Some(style_info) = extract_style_info_from_styled_item(styled_item, decoder) {
498 geometry_styles.insert(geometry_id, style_info);
499 }
500}
501
502pub(crate) fn extract_style_info_from_styled_item(
505 styled_item: &DecodedEntity,
506 decoder: &mut EntityDecoder,
507) -> Option<GeometryStyleInfo> {
508 let style_refs = refs_from_list(styled_item, 1)?;
509
510 for style_id in style_refs {
511 if let Ok(style) = decoder.decode_by_id(style_id) {
512 if let Some(inner_refs) = refs_from_list(&style, 0) {
514 for inner_id in inner_refs {
515 if let Some(info) = extract_surface_style_info(inner_id, decoder) {
516 return Some(info);
517 }
518 }
519 }
520
521 if let Some(info) = extract_surface_style_info(style_id, decoder) {
523 return Some(info);
524 }
525 }
526 }
527
528 None
529}
530
531fn extract_surface_style_info(
536 style_id: u32,
537 decoder: &mut EntityDecoder,
538) -> Option<GeometryStyleInfo> {
539 let style = decoder.decode_by_id(style_id).ok()?;
540 let material_name = normalize_style_name(style.get_string(0));
541 let (color, shading_color) = crate::style::extract_surface_style_colors(style_id, decoder)?;
542 Some(GeometryStyleInfo {
543 color,
544 shading_color,
545 material_name,
546 })
547}
548
549fn normalize_style_name(raw: Option<&str>) -> Option<String> {
550 let name = raw?.trim();
551 if name.is_empty() || name == "$" {
552 return None;
553 }
554 if name.eq_ignore_ascii_case("<unnamed>") || name.eq_ignore_ascii_case("unnamed") {
555 return None;
556 }
557 Some(name.to_string())
558}
559
560fn refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
562 let list = entity.get_list(index)?;
563 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
564 if refs.is_empty() {
565 None
566 } else {
567 Some(refs)
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn find_ifcproject_id_late_in_file() {
577 let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\n#999123=IFCPROJECT('g',$,'P',$,$,$,$,$,$);\nENDSEC;\n";
578 assert_eq!(find_ifcproject_id(ifc), Some(999123));
579 }
580
581 #[test]
582 fn find_ifcproject_id_absent() {
583 let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\nENDSEC;\n";
584 assert_eq!(find_ifcproject_id(ifc), None);
585 }
586
587 #[test]
588 fn find_ifcproject_id_skips_string_decoys() {
589 let ifc = b"DATA;\n#5=IFCWALL('decoy =IFCPROJECT( in a name',$);\n#7=IFCPROJECT('g',$);\n";
590 assert_eq!(find_ifcproject_id(ifc), Some(7));
591 }
592
593 #[test]
594 fn material_colors_flat_round_trip() {
595 let mut map: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
596 map.insert(10, vec![[0.5, 0.5, 0.5, 1.0], [0.7, 0.9, 0.5, 0.2]]);
597 map.insert(42, vec![[1.0, 0.0, 0.0, 1.0]]);
598
599 let (ids, counts, rgba) = flat_material_colors(&map);
600 let back = material_colors_from_flat(&ids, &counts, &rgba);
601
602 assert_eq!(back.len(), 2);
603 assert_eq!(back[&42].len(), 1);
604 assert_eq!(back[&10].len(), 2);
605 for (orig, round) in map[&10].iter().zip(back[&10].iter()) {
607 for (a, b) in orig.iter().zip(round.iter()) {
608 assert!((a - b).abs() <= 1.0 / 255.0 + 1e-6);
609 }
610 }
611 }
612
613 #[test]
614 fn resolve_unit_scales_resolves_degrees_and_millimetres() {
615 const IFC: &[u8] = br#"ISO-10303-21;
616HEADER;
617FILE_DESCRIPTION((''),'2;1');
618FILE_NAME('u.ifc','2026-06-12T00:00:00',(''),(''),'','','');
619FILE_SCHEMA(('IFC4'));
620ENDSEC;
621DATA;
622#1=IFCWALL('w',$,$,$,$,$,$,$,$);
623#10=IFCPROJECT('g',$,'P',$,$,$,$,$,#11);
624#11=IFCUNITASSIGNMENT((#12,#13));
625#12=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
626#13=IFCCONVERSIONBASEDUNIT(#14,.PLANEANGLEUNIT.,'DEGREE',#15);
627#14=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
628#15=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
629#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
630ENDSEC;
631END-ISO-10303-21;
632"#;
633 let mut decoder = EntityDecoder::new(IFC);
635 let scales = resolve_unit_scales(IFC, None, &mut decoder);
636 assert_eq!(scales.project_id, Some(10));
637 assert!((scales.length_unit_scale - 0.001).abs() < 1e-12);
638 assert!((scales.plane_angle_to_radians - 0.017_453_292_519_943_295).abs() < 1e-12);
639 }
640}