use crate::style::{FullIndexedColourMap, GeometryStyleInfo};
use ifc_lite_core::{DecodedEntity, EntityDecoder};
use rustc_hash::FxHashMap;
pub type Span = (u32, usize, usize);
#[derive(Debug, Default, Clone)]
pub struct PrepassSpans {
pub styled_items: Vec<Span>,
pub indexed_colour_maps: Vec<Span>,
pub material_def_reprs: Vec<Span>,
pub rel_associates_material: Vec<Span>,
pub void_rels: Vec<Span>,
pub fills_rels: Vec<Span>,
pub aggregate_rels: Vec<Span>,
}
#[derive(Debug, Clone, Copy)]
pub struct ResolveOptions {
pub collect_indexed_colour_full: bool,
pub defer_attached_styles: bool,
}
impl Default for ResolveOptions {
fn default() -> Self {
Self {
collect_indexed_colour_full: true,
defer_attached_styles: false,
}
}
}
#[derive(Debug, Default)]
pub struct ResolvedPrepass {
pub geometry_style_index: FxHashMap<u32, GeometryStyleInfo>,
pub indexed_colour_index: FxHashMap<u32, [f32; 4]>,
pub indexed_colour_full: FxHashMap<u32, FullIndexedColourMap>,
pub orphan_styled_items: FxHashMap<u32, [f32; 4]>,
pub material_def_reprs: FxHashMap<u32, Vec<u32>>,
pub element_to_material: FxHashMap<u32, u32>,
pub element_material_colors: FxHashMap<u32, Vec<[f32; 4]>>,
pub void_index: FxHashMap<u32, Vec<u32>>,
pub filling_by_opening: FxHashMap<u32, u32>,
pub deferred_attached_styled_spans: Vec<(usize, usize)>,
}
pub use crate::prepass_styled::{resolve_styled_items_into, StyleSeeds};
pub fn resolve_prepass(
spans: &PrepassSpans,
decoder: &mut EntityDecoder,
opts: ResolveOptions,
) -> ResolvedPrepass {
resolve_prepass_with_style_seeds(spans, decoder, opts, None)
}
pub fn resolve_prepass_with_style_seeds(
spans: &PrepassSpans,
decoder: &mut EntityDecoder,
opts: ResolveOptions,
style_seeds: Option<StyleSeeds>,
) -> ResolvedPrepass {
let mut out = ResolvedPrepass::default();
if let Some((orphan, geom)) = style_seeds {
out.orphan_styled_items = orphan;
out.geometry_style_index = geom;
}
resolve_styled_items_into(
&spans.styled_items,
decoder,
opts.defer_attached_styles,
&mut out.orphan_styled_items,
&mut out.geometry_style_index,
&mut out.deferred_attached_styled_spans,
);
for &(id, start, end) in &spans.indexed_colour_maps {
let Ok(icm) = decoder.decode_at_with_id(id, start, end) else {
continue;
};
let Some(full) = crate::style::resolve_indexed_colour_map_full(&icm, decoder) else {
continue;
};
let geometry_id = full.geometry_id;
out.indexed_colour_index
.entry(geometry_id)
.or_insert(full.dominant().to_array());
if opts.collect_indexed_colour_full {
out.indexed_colour_full.entry(geometry_id).or_insert(full);
}
}
for &(id, start, end) in &spans.material_def_reprs {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if let Some(material_id) = entity.get_ref(3) {
if let Some(reprs) = refs_from_list(&entity, 2) {
out.material_def_reprs
.entry(material_id)
.or_default()
.extend(reprs);
}
}
}
}
for &(id, start, end) in &spans.rel_associates_material {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if let Some(material_select_id) = entity.get_ref(5) {
if let Some(related) = refs_from_list(&entity, 4) {
for element_id in related {
out.element_to_material.insert(element_id, material_select_id);
}
}
}
}
}
for &(id, start, end) in &spans.void_rels {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if let (Some(host), Some(opening)) = (entity.get_ref(4), entity.get_ref(5)) {
out.void_index.entry(host).or_default().push(opening);
}
}
}
for &(id, start, end) in &spans.fills_rels {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
if let (Some(opening_id), Some(filling_id)) = (entity.get_ref(4), entity.get_ref(5)) {
out.filling_by_opening.insert(opening_id, filling_id);
}
}
}
if !out.void_index.is_empty() && !spans.aggregate_rels.is_empty() {
let mut aggregate_children: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
for &(id, start, end) in &spans.aggregate_rels {
if let Ok(entity) = decoder.decode_at_with_id(id, start, end) {
let Some(parent_id) = entity.get_ref(4) else {
continue;
};
if let Some(children) = refs_from_list(&entity, 5) {
aggregate_children
.entry(parent_id)
.or_default()
.extend(children);
}
}
}
ifc_lite_geometry::propagate_voids_via_aggregates(
&mut out.void_index,
&aggregate_children,
);
}
for openings in out.void_index.values_mut() {
openings.sort_unstable();
}
out.element_material_colors = crate::style::build_element_material_colors(
&out.material_def_reprs,
&out.orphan_styled_items,
&out.element_to_material,
decoder,
);
out
}
pub fn resolve_styled_item_spans(
spans: &[(usize, usize)],
decoder: &mut EntityDecoder,
) -> FxHashMap<u32, GeometryStyleInfo> {
let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
for &(start, end) in spans {
if let Ok(styled_item) = decoder.decode_at(start, end) {
if styled_item.get_ref(0).is_some() {
collect_geometry_style_info(&mut styles, &styled_item, decoder);
}
}
}
styles
}
pub fn merge_indexed_colours(
geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
indexed_colours: &FxHashMap<u32, [f32; 4]>,
) {
for (&geometry_id, &color) in indexed_colours {
geometry_styles
.entry(geometry_id)
.or_insert_with(|| GeometryStyleInfo::from_color(color));
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UnitScales {
pub length_unit_scale: f64,
pub plane_angle_to_radians: f64,
pub project_id: Option<u32>,
}
impl Default for UnitScales {
fn default() -> Self {
Self {
length_unit_scale: 1.0,
plane_angle_to_radians: 1.0,
project_id: None,
}
}
}
pub fn resolve_unit_scales(
content: &[u8],
project_id_hint: Option<u32>,
decoder: &mut EntityDecoder,
) -> UnitScales {
let project_id = project_id_hint.or_else(|| find_ifcproject_id(content));
let Some(pid) = project_id else {
return UnitScales::default();
};
let length = ifc_lite_core::try_extract_length_unit_scale(decoder, pid);
let angle = ifc_lite_core::try_extract_plane_angle_to_radians(decoder, pid);
if let (Some(length_unit_scale), Some(plane_angle_to_radians)) = (length, angle) {
return UnitScales {
length_unit_scale,
plane_angle_to_radians,
project_id,
};
}
let full_index = ifc_lite_core::build_entity_index(content);
let mut full_decoder = EntityDecoder::with_index(content, full_index);
UnitScales {
length_unit_scale: length.or_else(|| {
ifc_lite_core::extract_length_unit_scale(&mut full_decoder, pid).ok()
})
.unwrap_or(1.0),
plane_angle_to_radians: angle
.or_else(|| {
ifc_lite_core::extract_plane_angle_to_radians(&mut full_decoder, pid).ok()
})
.unwrap_or(1.0),
project_id,
}
}
pub fn find_ifcproject_id(content: &[u8]) -> Option<u32> {
let mut from = 0usize;
while let Some(rel) = memchr::memmem::find(&content[from..], b"IFCPROJECT(") {
let kw = from + rel;
let mut i = kw;
while i > 0 && content[i - 1].is_ascii_whitespace() {
i -= 1;
}
if i > 0 && content[i - 1] == b'=' {
i -= 1; while i > 0 && content[i - 1].is_ascii_whitespace() {
i -= 1;
}
let digits_end = i;
while i > 0 && content[i - 1].is_ascii_digit() {
i -= 1;
}
if i > 0 && content[i - 1] == b'#' && i < digits_end {
let mut id: u32 = 0;
for &b in &content[i..digits_end] {
id = id.wrapping_mul(10).wrapping_add((b - b'0') as u32);
}
return Some(id);
}
}
from = kw + 1;
}
None
}
pub fn flat_styles_rgba8(resolved: &ResolvedPrepass, decoder: &mut EntityDecoder) -> (Vec<u32>, Vec<u8>) {
let mut merged: FxHashMap<u32, [f32; 4]> = resolved
.geometry_style_index
.iter()
.map(|(&id, info)| (id, info.color))
.collect();
for (&geometry_id, &color) in &resolved.indexed_colour_index {
merged.entry(geometry_id).or_insert(color);
}
let material_styles = crate::style::build_material_style_index(
&resolved.material_def_reprs,
&resolved.orphan_styled_items,
decoder,
);
for (&mat_id, &color) in crate::style::flatten_material_color_index(&material_styles).iter() {
merged.entry(mat_id).or_insert(color);
}
for (&element_id, colors) in &resolved.element_material_colors {
if let Some(&color) = colors.first() {
merged.entry(element_id).or_insert(color);
}
}
let mut entries: Vec<(u32, [f32; 4])> = merged.into_iter().collect();
entries.sort_unstable_by_key(|&(id, _)| id);
let mut ids: Vec<u32> = Vec::with_capacity(entries.len());
let mut rgba: Vec<u8> = Vec::with_capacity(entries.len() * 4);
for (id, color) in entries {
ids.push(id);
rgba.extend_from_slice(&crate::style::Rgba::from_array(color).to_rgba8());
}
(ids, rgba)
}
pub fn flat_voids(void_index: &FxHashMap<u32, Vec<u32>>) -> (Vec<u32>, Vec<u32>, Vec<u32>) {
let mut hosts: Vec<(&u32, &Vec<u32>)> = void_index.iter().collect();
hosts.sort_unstable_by_key(|&(&host_id, _)| host_id);
let mut keys: Vec<u32> = Vec::with_capacity(hosts.len());
let mut counts: Vec<u32> = Vec::with_capacity(hosts.len());
let mut values: Vec<u32> = Vec::new();
for (&host_id, openings) in hosts {
keys.push(host_id);
counts.push(openings.len() as u32);
values.extend(openings.iter().copied());
}
(keys, counts, values)
}
pub fn flat_material_colors(
element_material_colors: &FxHashMap<u32, Vec<[f32; 4]>>,
) -> (Vec<u32>, Vec<u32>, Vec<u8>) {
let mut elements: Vec<(&u32, &Vec<[f32; 4]>)> = element_material_colors.iter().collect();
elements.sort_unstable_by_key(|&(&element_id, _)| element_id);
let mut ids: Vec<u32> = Vec::with_capacity(elements.len());
let mut counts: Vec<u32> = Vec::with_capacity(elements.len());
let mut rgba: Vec<u8> = Vec::new();
for (&element_id, colors) in elements {
if colors.is_empty() {
continue;
}
ids.push(element_id);
counts.push(colors.len() as u32);
for &c in colors {
rgba.extend_from_slice(&crate::style::Rgba::from_array(c).to_rgba8());
}
}
(ids, counts, rgba)
}
pub fn material_colors_from_flat(
element_ids: &[u32],
counts: &[u32],
rgba: &[u8],
) -> FxHashMap<u32, Vec<[f32; 4]>> {
let mut out: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
let mut offset = 0usize;
for (i, &element_id) in element_ids.iter().enumerate() {
let Some(&count) = counts.get(i) else { break };
let count = count as usize;
let mut colors: Vec<[f32; 4]> = Vec::with_capacity(count);
for c in 0..count {
let base = (offset + c) * 4;
if base + 3 >= rgba.len() {
break;
}
colors.push(
crate::style::Rgba::from_rgba8([
rgba[base],
rgba[base + 1],
rgba[base + 2],
rgba[base + 3],
])
.to_array(),
);
}
offset += count;
if !colors.is_empty() {
out.insert(element_id, colors);
}
}
out
}
pub(crate) fn collect_geometry_style_info(
geometry_styles: &mut FxHashMap<u32, GeometryStyleInfo>,
styled_item: &DecodedEntity,
decoder: &mut EntityDecoder,
) {
let Some(geometry_id) = styled_item.get_ref(0) else {
return;
};
if geometry_styles.contains_key(&geometry_id) {
return;
}
if let Some(style_info) = extract_style_info_from_styled_item(styled_item, decoder) {
geometry_styles.insert(geometry_id, style_info);
}
}
pub(crate) fn extract_style_info_from_styled_item(
styled_item: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<GeometryStyleInfo> {
let style_refs = refs_from_list(styled_item, 1)?;
for style_id in style_refs {
if let Ok(style) = decoder.decode_by_id(style_id) {
if let Some(inner_refs) = refs_from_list(&style, 0) {
for inner_id in inner_refs {
if let Some(info) = extract_surface_style_info(inner_id, decoder) {
return Some(info);
}
}
}
if let Some(info) = extract_surface_style_info(style_id, decoder) {
return Some(info);
}
}
}
None
}
fn extract_surface_style_info(
style_id: u32,
decoder: &mut EntityDecoder,
) -> Option<GeometryStyleInfo> {
let style = decoder.decode_by_id(style_id).ok()?;
let material_name = normalize_style_name(style.get_string(0));
let (color, shading_color) = crate::style::extract_surface_style_colors(style_id, decoder)?;
Some(GeometryStyleInfo {
color,
shading_color,
material_name,
})
}
fn normalize_style_name(raw: Option<&str>) -> Option<String> {
let name = raw?.trim();
if name.is_empty() || name == "$" {
return None;
}
if name.eq_ignore_ascii_case("<unnamed>") || name.eq_ignore_ascii_case("unnamed") {
return None;
}
Some(name.to_string())
}
fn refs_from_list(entity: &DecodedEntity, index: usize) -> Option<Vec<u32>> {
let list = entity.get_list(index)?;
let refs: Vec<u32> = list.iter().filter_map(|v| v.as_entity_ref()).collect();
if refs.is_empty() {
None
} else {
Some(refs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_lite_core::{EntityIndex, EntityScanner};
#[test]
fn find_ifcproject_id_late_in_file() {
let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\n#999123=IFCPROJECT('g',$,'P',$,$,$,$,$,$);\nENDSEC;\n";
assert_eq!(find_ifcproject_id(ifc), Some(999123));
}
#[test]
fn find_ifcproject_id_absent() {
let ifc = b"ISO-10303-21;\nDATA;\n#1=IFCWALL('x',$,$,$,$,$,$,$,$);\nENDSEC;\n";
assert_eq!(find_ifcproject_id(ifc), None);
}
#[test]
fn find_ifcproject_id_skips_string_decoys() {
let ifc = b"DATA;\n#5=IFCWALL('decoy =IFCPROJECT( in a name',$);\n#7=IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(ifc), Some(7));
}
#[test]
fn find_ifcproject_id_handles_whitespace_around_equals() {
let space_after = b"DATA;\n#1=IFCWALL('x',$);\n#1593796= IFCPROJECT('g',$,'P',$,$,$,$,$,$);\n";
assert_eq!(find_ifcproject_id(space_after), Some(1593796));
let space_both = b"DATA;\n#42 = IFCPROJECT('g',$);\n";
assert_eq!(find_ifcproject_id(space_both), Some(42));
let crs_only = b"DATA;\n#9= IFCPROJECTEDCRS('EPSG:32632',$,'WGS84',$,'UTM','32N',$);\n";
assert_eq!(find_ifcproject_id(crs_only), None);
}
#[test]
fn resolve_unit_scales_recovers_degrees_when_measure_past_partial_index() {
const IFC: &[u8] = br#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('u.ifc','2026-06-26T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#10= IFCPROJECT('g',$,'P',$,$,$,$,$,#11);
#11= IFCUNITASSIGNMENT((#12,#13));
#12= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#13= IFCCONVERSIONBASEDUNIT(#14,.PLANEANGLEUNIT.,'DEGREE',#15);
#14= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#16= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#15= IFCMEASUREWITHUNIT(IFCRATIOMEASURE(0.0174532925199433),#16);
ENDSEC;
END-ISO-10303-21;
"#;
let mut partial = EntityIndex::default();
let mut scanner = EntityScanner::new(&IFC);
while let Some((id, _t, start, end)) = scanner.next_entity() {
if id == 15 || id == 14 {
continue; }
partial.insert(id, (start, end));
}
let mut decoder = EntityDecoder::with_index(IFC, partial);
let scales = resolve_unit_scales(IFC, Some(10), &mut decoder);
assert_eq!(scales.project_id, Some(10));
assert!((scales.length_unit_scale - 0.001).abs() < 1e-12);
assert!(
(scales.plane_angle_to_radians - 0.0174532925199433).abs() < 1e-12,
"expected degrees via full-index retry, got {}",
scales.plane_angle_to_radians
);
}
#[test]
fn material_colors_flat_round_trip() {
let mut map: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
map.insert(10, vec![[0.5, 0.5, 0.5, 1.0], [0.7, 0.9, 0.5, 0.2]]);
map.insert(42, vec![[1.0, 0.0, 0.0, 1.0]]);
let (ids, counts, rgba) = flat_material_colors(&map);
let back = material_colors_from_flat(&ids, &counts, &rgba);
assert_eq!(back.len(), 2);
assert_eq!(back[&42].len(), 1);
assert_eq!(back[&10].len(), 2);
for (orig, round) in map[&10].iter().zip(back[&10].iter()) {
for (a, b) in orig.iter().zip(round.iter()) {
assert!((a - b).abs() <= 1.0 / 255.0 + 1e-6);
}
}
}
#[test]
fn flat_wire_arrays_are_sorted_by_id() {
let mut voids: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
voids.insert(300, vec![301, 302]);
voids.insert(7, vec![8]);
voids.insert(90, vec![91]);
let (keys, counts, values) = flat_voids(&voids);
assert_eq!(keys, vec![7, 90, 300]);
assert_eq!(counts, vec![1, 1, 2]);
assert_eq!(values, vec![8, 91, 301, 302]);
let mut colors: FxHashMap<u32, Vec<[f32; 4]>> = FxHashMap::default();
colors.insert(42, vec![[1.0, 0.0, 0.0, 1.0]]);
colors.insert(10, vec![[0.0, 1.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.5]]);
let (ids, counts, rgba) = flat_material_colors(&colors);
assert_eq!(ids, vec![10, 42]);
assert_eq!(counts, vec![2, 1]);
assert_eq!(rgba.len(), 12);
assert_eq!(&rgba[0..4], &[0, 255, 0, 255]);
}
#[test]
fn resolve_unit_scales_resolves_degrees_and_millimetres() {
const IFC: &[u8] = br#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('u.ifc','2026-06-12T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCWALL('w',$,$,$,$,$,$,$,$);
#10=IFCPROJECT('g',$,'P',$,$,$,$,$,#11);
#11=IFCUNITASSIGNMENT((#12,#13));
#12=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#13=IFCCONVERSIONBASEDUNIT(#14,.PLANEANGLEUNIT.,'DEGREE',#15);
#14=IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#15=IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453292519943295),#16);
#16=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
ENDSEC;
END-ISO-10303-21;
"#;
let mut decoder = EntityDecoder::new(IFC);
let scales = resolve_unit_scales(IFC, None, &mut decoder);
assert_eq!(scales.project_id, Some(10));
assert!((scales.length_unit_scale - 0.001).abs() < 1e-12);
assert!((scales.plane_angle_to_radians - 0.017_453_292_519_943_295).abs() < 1e-12);
}
}