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, Clone)]
33pub struct PrepassSpans {
34 pub styled_items: Vec<Span>,
38 pub indexed_colour_maps: Vec<Span>,
41 pub material_def_reprs: Vec<Span>,
43 pub rel_associates_material: Vec<Span>,
45 pub void_rels: Vec<Span>,
47 pub fills_rels: Vec<Span>,
50 pub aggregate_rels: Vec<Span>,
53}
54
55#[derive(Debug, Clone, Copy)]
57pub struct ResolveOptions {
58 pub collect_indexed_colour_full: bool,
63 pub defer_attached_styles: bool,
69}
70
71impl Default for ResolveOptions {
72 fn default() -> Self {
73 Self {
74 collect_indexed_colour_full: true,
75 defer_attached_styles: false,
76 }
77 }
78}
79
80#[derive(Debug, Default)]
82pub struct ResolvedPrepass {
83 pub geometry_style_index: FxHashMap<u32, GeometryStyleInfo>,
87 pub indexed_colour_index: FxHashMap<u32, [f32; 4]>,
89 pub indexed_colour_full: FxHashMap<u32, FullIndexedColourMap>,
92 pub orphan_styled_items: FxHashMap<u32, [f32; 4]>,
94 pub material_def_reprs: FxHashMap<u32, Vec<u32>>,
96 pub element_to_material: FxHashMap<u32, u32>,
98 pub element_material_colors: FxHashMap<u32, Vec<[f32; 4]>>,
101 pub void_index: FxHashMap<u32, Vec<u32>>,
103 pub filling_by_opening: FxHashMap<u32, u32>,
105 pub deferred_attached_styled_spans: Vec<(usize, usize)>,
108}
109
110pub use crate::prepass_styled::{resolve_styled_items_into, StyleSeeds};
111
112pub fn resolve_prepass(
114 spans: &PrepassSpans,
115 decoder: &mut EntityDecoder,
116 opts: ResolveOptions,
117) -> ResolvedPrepass {
118 resolve_prepass_with_style_seeds(spans, decoder, opts, None)
119}
120
121pub fn resolve_prepass_with_style_seeds(
125 spans: &PrepassSpans,
126 decoder: &mut EntityDecoder,
127 opts: ResolveOptions,
128 style_seeds: Option<StyleSeeds>,
129) -> ResolvedPrepass {
130 let mut out = ResolvedPrepass::default();
131
132 if let Some((orphan, geom)) = style_seeds {
133 out.orphan_styled_items = orphan;
134 out.geometry_style_index = geom;
135 }
136 resolve_styled_items_into(
138 &spans.styled_items,
139 decoder,
140 opts.defer_attached_styles,
141 &mut out.orphan_styled_items,
142 &mut out.geometry_style_index,
143 &mut out.deferred_attached_styled_spans,
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 for openings in out.void_index.values_mut() {
233 openings.sort_unstable();
234 }
235
236 out.element_material_colors = crate::style::build_element_material_colors(
238 &out.material_def_reprs,
239 &out.orphan_styled_items,
240 &out.element_to_material,
241 decoder,
242 );
243
244 out
245}
246
247pub fn resolve_styled_item_spans(
251 spans: &[(usize, usize)],
252 decoder: &mut EntityDecoder,
253) -> FxHashMap<u32, GeometryStyleInfo> {
254 let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
255 for &(start, end) in spans {
256 if let Ok(styled_item) = decoder.decode_at(start, end) {
257 if styled_item.get_ref(0).is_some() {
258 collect_geometry_style_info(&mut styles, &styled_item, decoder);
259 }
260 }
261 }
262 styles
263}
264
265pub fn merge_indexed_colours(
270 geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
271 indexed_colours: &FxHashMap<u32, [f32; 4]>,
272) {
273 for (&geometry_id, &color) in indexed_colours {
274 geometry_styles
275 .entry(geometry_id)
276 .or_insert_with(|| GeometryStyleInfo::from_color(color));
277 }
278}
279
280#[derive(Debug, Clone, Copy, PartialEq)]
282pub struct UnitScales {
283 pub length_unit_scale: f64,
285 pub plane_angle_to_radians: f64,
287 pub project_id: Option<u32>,
289}
290
291impl Default for UnitScales {
292 fn default() -> Self {
293 Self {
294 length_unit_scale: 1.0,
295 plane_angle_to_radians: 1.0,
296 project_id: None,
297 }
298 }
299}
300
301pub fn resolve_unit_scales(
317 content: &[u8],
318 project_id_hint: Option<u32>,
319 decoder: &mut EntityDecoder,
320) -> UnitScales {
321 let project_id = project_id_hint.or_else(|| find_ifcproject_id(content));
322 let Some(pid) = project_id else {
323 return UnitScales::default();
324 };
325
326 let length = ifc_lite_core::try_extract_length_unit_scale(decoder, pid);
331 let angle = ifc_lite_core::try_extract_plane_angle_to_radians(decoder, pid);
332
333 if let (Some(length_unit_scale), Some(plane_angle_to_radians)) = (length, angle) {
334 return UnitScales {
335 length_unit_scale,
336 plane_angle_to_radians,
337 project_id,
338 };
339 }
340
341 let full_index = ifc_lite_core::build_entity_index(content);
343 let mut full_decoder = EntityDecoder::with_index(content, full_index);
344 UnitScales {
345 length_unit_scale: length.or_else(|| {
346 ifc_lite_core::extract_length_unit_scale(&mut full_decoder, pid).ok()
347 })
348 .unwrap_or(1.0),
349 plane_angle_to_radians: angle
350 .or_else(|| {
351 ifc_lite_core::extract_plane_angle_to_radians(&mut full_decoder, pid).ok()
352 })
353 .unwrap_or(1.0),
354 project_id,
355 }
356}
357
358pub fn find_ifcproject_id(content: &[u8]) -> Option<u32> {
361 let mut from = 0usize;
362 while let Some(rel) = memchr::memmem::find(&content[from..], b"IFCPROJECT(") {
370 let kw = from + rel;
371 let mut i = kw;
373 while i > 0 && content[i - 1].is_ascii_whitespace() {
374 i -= 1;
375 }
376 if i > 0 && content[i - 1] == b'=' {
377 i -= 1; while i > 0 && content[i - 1].is_ascii_whitespace() {
380 i -= 1;
381 }
382 let digits_end = i;
384 while i > 0 && content[i - 1].is_ascii_digit() {
385 i -= 1;
386 }
387 if i > 0 && content[i - 1] == b'#' && i < digits_end {
388 let mut id: u32 = 0;
389 for &b in &content[i..digits_end] {
390 id = id.wrapping_mul(10).wrapping_add((b - b'0') as u32);
391 }
392 return Some(id);
393 }
394 }
395 from = kw + 1;
398 }
399 None
400}
401
402pub fn flat_styles_rgba8(resolved: &ResolvedPrepass, decoder: &mut EntityDecoder) -> (Vec<u32>, Vec<u8>) {
408 let mut merged: FxHashMap<u32, [f32; 4]> = resolved
409 .geometry_style_index
410 .iter()
411 .map(|(&id, info)| (id, info.color))
412 .collect();
413 for (&geometry_id, &color) in &resolved.indexed_colour_index {
414 merged.entry(geometry_id).or_insert(color);
415 }
416 let material_styles = crate::style::build_material_style_index(
419 &resolved.material_def_reprs,
420 &resolved.orphan_styled_items,
421 decoder,
422 );
423 for (&mat_id, &color) in crate::style::flatten_material_color_index(&material_styles).iter() {
424 merged.entry(mat_id).or_insert(color);
425 }
426 for (&element_id, colors) in &resolved.element_material_colors {
427 if let Some(&color) = colors.first() {
428 merged.entry(element_id).or_insert(color);
429 }
430 }
431
432 let mut entries: Vec<(u32, [f32; 4])> = merged.into_iter().collect();
436 entries.sort_unstable_by_key(|&(id, _)| id);
437 let mut ids: Vec<u32> = Vec::with_capacity(entries.len());
438 let mut rgba: Vec<u8> = Vec::with_capacity(entries.len() * 4);
439 for (id, color) in entries {
440 ids.push(id);
441 rgba.extend_from_slice(&crate::style::Rgba::from_array(color).to_rgba8());
442 }
443 (ids, rgba)
444}
445
446pub fn flat_voids(void_index: &FxHashMap<u32, Vec<u32>>) -> (Vec<u32>, Vec<u32>, Vec<u32>) {
456 let mut hosts: Vec<(&u32, &Vec<u32>)> = void_index.iter().collect();
457 hosts.sort_unstable_by_key(|&(&host_id, _)| host_id);
458 let mut keys: Vec<u32> = Vec::with_capacity(hosts.len());
459 let mut counts: Vec<u32> = Vec::with_capacity(hosts.len());
460 let mut values: Vec<u32> = Vec::new();
461 for (&host_id, openings) in hosts {
462 keys.push(host_id);
463 counts.push(openings.len() as u32);
464 values.extend(openings.iter().copied());
465 }
466 (keys, counts, values)
467}
468
469pub fn flat_material_colors(
478 element_material_colors: &FxHashMap<u32, Vec<[f32; 4]>>,
479) -> (Vec<u32>, Vec<u32>, Vec<u8>) {
480 let mut elements: Vec<(&u32, &Vec<[f32; 4]>)> = element_material_colors.iter().collect();
481 elements.sort_unstable_by_key(|&(&element_id, _)| element_id);
482 let mut ids: Vec<u32> = Vec::with_capacity(elements.len());
483 let mut counts: Vec<u32> = Vec::with_capacity(elements.len());
484 let mut rgba: Vec<u8> = Vec::new();
485 for (&element_id, colors) in elements {
486 if colors.is_empty() {
487 continue;
488 }
489 ids.push(element_id);
490 counts.push(colors.len() as u32);
491 for &c in colors {
492 rgba.extend_from_slice(&crate::style::Rgba::from_array(c).to_rgba8());
493 }
494 }
495 (ids, counts, rgba)
496}
497
498pub fn material_colors_from_flat(
501 element_ids: &[u32],
502 counts: &[u32],
503 rgba: &[u8],
504) -> FxHashMap<u32, Vec<[f32; 4]>> {
505 let mut out: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
506 let mut offset = 0usize;
507 for (i, &element_id) in element_ids.iter().enumerate() {
508 let Some(&count) = counts.get(i) else { break };
509 let count = count as usize;
510 let mut colors: Vec<[f32; 4]> = Vec::with_capacity(count);
511 for c in 0..count {
512 let base = (offset + c) * 4;
513 if base + 3 >= rgba.len() {
514 break;
515 }
516 colors.push(
517 crate::style::Rgba::from_rgba8([
518 rgba[base],
519 rgba[base + 1],
520 rgba[base + 2],
521 rgba[base + 3],
522 ])
523 .to_array(),
524 );
525 }
526 offset += count;
527 if !colors.is_empty() {
528 out.insert(element_id, colors);
529 }
530 }
531 out
532}
533
534pub(crate) fn collect_geometry_style_info(
539 geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
540 styled_item: &DecodedEntity,
541 decoder: &mut EntityDecoder,
542) {
543 let Some(geometry_id) = styled_item.get_ref(0) else {
544 return;
545 };
546 if geometry_styles.contains_key(&geometry_id) {
547 return;
548 }
549 if let Some(style_info) = extract_style_info_from_styled_item(styled_item, decoder) {
550 geometry_styles.insert(geometry_id, style_info);
551 }
552}
553
554pub(crate) fn extract_style_info_from_styled_item(
557 styled_item: &DecodedEntity,
558 decoder: &mut EntityDecoder,
559) -> Option<GeometryStyleInfo> {
560 let style_refs = refs_from_list(styled_item, 1)?;
561
562 for style_id in style_refs {
563 if let Ok(style) = decoder.decode_by_id(style_id) {
564 if let Some(inner_refs) = refs_from_list(&style, 0) {
566 for inner_id in inner_refs {
567 if let Some(info) = extract_surface_style_info(inner_id, decoder) {
568 return Some(info);
569 }
570 }
571 }
572
573 if let Some(info) = extract_surface_style_info(style_id, decoder) {
575 return Some(info);
576 }
577 }
578 }
579
580 None
581}
582
583fn extract_surface_style_info(
588 style_id: u32,
589 decoder: &mut EntityDecoder,
590) -> Option<GeometryStyleInfo> {
591 let style = decoder.decode_by_id(style_id).ok()?;
592 let material_name = normalize_style_name(style.get_string(0));
593 let (color, shading_color) = crate::style::extract_surface_style_colors(style_id, decoder)?;
594 Some(GeometryStyleInfo {
595 color,
596 shading_color,
597 material_name,
598 })
599}
600
601fn normalize_style_name(raw: Option<&str>) -> Option<String> {
602 let name = raw?.trim();
603 if name.is_empty() || name == "$" {
604 return None;
605 }
606 if name.eq_ignore_ascii_case("<unnamed>") || name.eq_ignore_ascii_case("unnamed") {
607 return None;
608 }
609 Some(name.to_string())
610}
611
612fn refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
614 let list = entity.get_list(index)?;
615 let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
616 if refs.is_empty() {
617 None
618 } else {
619 Some(refs)
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use ifc_lite_core::{EntityIndex, EntityScanner};
627
628 #[test]
629 fn find_ifcproject_id_late_in_file() {
630 let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\n#999123=IFCPROJECT('g',$,'P',$,$,$,$,$,$);\nENDSEC;\n";
631 assert_eq!(find_ifcproject_id(ifc), Some(999123));
632 }
633
634 #[test]
635 fn find_ifcproject_id_absent() {
636 let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\nENDSEC;\n";
637 assert_eq!(find_ifcproject_id(ifc), None);
638 }
639
640 #[test]
641 fn find_ifcproject_id_skips_string_decoys() {
642 let ifc = b"DATA;\n#5=IFCWALL('decoy =IFCPROJECT( in a name',$);\n#7=IFCPROJECT('g',$);\n";
643 assert_eq!(find_ifcproject_id(ifc), Some(7));
644 }
645
646 #[test]
647 fn find_ifcproject_id_handles_whitespace_around_equals() {
648 let space_after = b"DATA;\n#1=IFCWALL('x',$);\n#1593796= IFCPROJECT('g',$,'P',$,$,$,$,$,$);\n";
652 assert_eq!(find_ifcproject_id(space_after), Some(1593796));
653
654 let space_both = b"DATA;\n#42 = IFCPROJECT('g',$);\n";
655 assert_eq!(find_ifcproject_id(space_both), Some(42));
656
657 let crs_only = b"DATA;\n#9= IFCPROJECTEDCRS('EPSG:32632',$,'WGS84',$,'UTM','32N',$);\n";
659 assert_eq!(find_ifcproject_id(crs_only), None);
660 }
661
662 #[test]
669 fn resolve_unit_scales_recovers_degrees_when_measure_past_partial_index() {
670 const IFC: &[u8] = br#"ISO-10303-21;
671HEADER;
672FILE_DESCRIPTION((''),'2;1');
673FILE_NAME('u.ifc','2026-06-26T00:00:00',(''),(''),'','','');
674FILE_SCHEMA(('IFC2X3'));
675ENDSEC;
676DATA;
677#10= IFCPROJECT('g',$,'P',$,$,$,$,$,#11);
678#11= IFCUNITASSIGNMENT((#12,#13));
679#12= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
680#13= IFCCONVERSIONBASEDUNIT(#14,.PLANEANGLEUNIT.,'DEGREE',#15);
681#14= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
682#16= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
683#15= IFCMEASUREWITHUNIT(IFCRATIOMEASURE(0.0174532925199433),#16);
684ENDSEC;
685END-ISO-10303-21;
686"#;
687 let mut partial = EntityIndex::default();
690 let mut scanner = EntityScanner::new(&IFC);
691 while let Some((id, _t, start, end)) = scanner.next_entity() {
692 if id == 15 || id == 14 {
693 continue; }
695 partial.insert(id, (start, end));
696 }
697 let mut decoder = EntityDecoder::with_index(IFC, partial);
698 let scales = resolve_unit_scales(IFC, Some(10), &mut decoder);
699 assert_eq!(scales.project_id, Some(10));
700 assert!((scales.length_unit_scale - 0.001).abs() < 1e-12);
701 assert!(
702 (scales.plane_angle_to_radians - 0.0174532925199433).abs() < 1e-12,
703 "expected degrees via full-index retry, got {}",
704 scales.plane_angle_to_radians
705 );
706 }
707
708 #[test]
709 fn material_colors_flat_round_trip() {
710 let mut map: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
711 map.insert(10, vec![[0.5, 0.5, 0.5, 1.0], [0.7, 0.9, 0.5, 0.2]]);
712 map.insert(42, vec![[1.0, 0.0, 0.0, 1.0]]);
713
714 let (ids, counts, rgba) = flat_material_colors(&map);
715 let back = material_colors_from_flat(&ids, &counts, &rgba);
716
717 assert_eq!(back.len(), 2);
718 assert_eq!(back[&42].len(), 1);
719 assert_eq!(back[&10].len(), 2);
720 for (orig, round) in map[&10].iter().zip(back[&10].iter()) {
722 for (a, b) in orig.iter().zip(round.iter()) {
723 assert!((a - b).abs() <= 1.0 / 255.0 + 1e-6);
724 }
725 }
726 }
727
728 #[test]
732 fn flat_wire_arrays_are_sorted_by_id() {
733 let mut voids: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
734 voids.insert(300, vec![301, 302]);
735 voids.insert(7, vec![8]);
736 voids.insert(90, vec![91]);
737 let (keys, counts, values) = flat_voids(&voids);
738 assert_eq!(keys, vec![7, 90, 300]);
739 assert_eq!(counts, vec![1, 1, 2]);
740 assert_eq!(values, vec![8, 91, 301, 302]);
742
743 let mut colors: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
744 colors.insert(42, vec![[1.0, 0.0, 0.0, 1.0]]);
745 colors.insert(10, vec![[0.0, 1.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.5]]);
746 let (ids, counts, rgba) = flat_material_colors(&colors);
747 assert_eq!(ids, vec![10, 42]);
748 assert_eq!(counts, vec![2, 1]);
749 assert_eq!(rgba.len(), 12);
750 assert_eq!(&rgba[0..4], &[0, 255, 0, 255]);
752 }
753
754 #[test]
755 fn resolve_unit_scales_resolves_degrees_and_millimetres() {
756 const IFC: &[u8] = br#"ISO-10303-21;
757HEADER;
758FILE_DESCRIPTION((''),'2;1');
759FILE_NAME('u.ifc','2026-06-12T00:00:00',(''),(''),'','','');
760FILE_SCHEMA(('IFC4'));
761ENDSEC;
762DATA;
763#1=IFCWALL('w',$,$,$,$,$,$,$,$);
764#10=IFCPROJECT('g',$,'P',$,$,$,$,$,#11);
765#11=IFCUNITASSIGNMENT((#12,#13));
766#12=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
767#13=IFCCONVERSIONBASEDUNIT(#14,.PLANEANGLEUNIT.,'DEGREE',#15);
768#14=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
769#15=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
770#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
771ENDSEC;
772END-ISO-10303-21;
773"#;
774 let mut decoder = EntityDecoder::new(IFC);
776 let scales = resolve_unit_scales(IFC, None, &mut decoder);
777 assert_eq!(scales.project_id, Some(10));
778 assert!((scales.length_unit_scale - 0.001).abs() < 1e-12);
779 assert!((scales.plane_angle_to_radians - 0.017_453_292_519_943_295).abs() < 1e-12);
780 }
781}