use ifc_lite_processing::determinism::FIXTURE_IFC;
use ifc_lite_processing::{
classify_type_name, process_geometry_streaming_with_options_and_bootstrap,
QuickMetadataBootstrap, QuickMetadataSpatialNode, StreamingOptions,
PREPASS_CLASS_NONE,
};
fn fixture_with_spatial_tree() -> String {
const TREE: &str = "\
#900=IFCSITE('0DeterminismSite0000A',$,'Site',$,$,#20,$,$,.ELEMENT.,$,$,$,$,$);
#901=IFCBUILDING('0DeterminismBldg0000A',$,'Building',$,$,#20,$,$,.ELEMENT.,$,$,$);
#902=IFCBUILDINGSTOREY('0DeterminismStorey00A',$,'Level 1',$,$,#20,$,$,.ELEMENT.,0.);
#910=IFCRELAGGREGATES('0DeterminismAggPS000A',$,$,$,#1,(#900));
#911=IFCRELAGGREGATES('0DeterminismAggSB000A',$,$,$,#900,(#901));
#912=IFCRELAGGREGATES('0DeterminismAggBS000A',$,$,$,#901,(#902));
#913=IFCRELCONTAINEDINSPATIALSTRUCTURE('0DeterminismCont000A',$,$,$,(#100,#400,#500,#600),#902);
";
FIXTURE_IFC.replacen("ENDSEC;\nEND-ISO", &format!("{TREE}ENDSEC;\nEND-ISO"), 1)
}
fn recase_keywords(ifc: &str, recase: impl Fn(&str) -> String) -> String {
ifc.lines()
.map(|line| {
let Some(rest) = line.strip_prefix('#') else {
return line.to_string();
};
let (Some(eq), Some(paren)) = (rest.find('='), rest.find('(')) else {
return line.to_string();
};
if paren < eq {
return line.to_string();
}
let keyword = &rest[eq + 1..paren];
format!("#{}={}{}", &rest[..eq], recase(keyword), &rest[paren..])
})
.collect::<Vec<_>>()
.join("\n")
}
fn lowercase(keyword: &str) -> String {
keyword.to_ascii_lowercase()
}
fn capitalised(keyword: &str) -> String {
format!("{}{}", &keyword[..1], keyword[1..].to_ascii_lowercase())
}
fn alternating(keyword: &str) -> String {
keyword
.chars()
.enumerate()
.map(|(i, c)| {
if i % 2 == 0 {
c.to_ascii_uppercase()
} else {
c.to_ascii_lowercase()
}
})
.collect()
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Mesh {
express_id: u32,
geometry_item_id: Option<u32>,
ifc_type: String,
positions: Vec<u32>,
normals: Vec<u32>,
indices: Vec<u32>,
color: [u32; 4],
material_name: Option<String>,
}
#[derive(Debug, PartialEq)]
struct Surface {
meshes: Vec<Mesh>,
site_transform: Option<Vec<u64>>,
building_transform: Option<Vec<u64>>,
length_unit_scale: Option<u64>,
entity_count: usize,
geometry_entity_count: usize,
spatial_tree: Option<Tree>,
}
#[derive(Debug, PartialEq)]
struct Tree {
express_id: u32,
type_name: String,
elevation: Option<u64>,
children: Vec<Tree>,
elements: Vec<u32>,
}
fn tree(node: &QuickMetadataSpatialNode) -> Tree {
Tree {
express_id: node.summary.express_id,
type_name: node.summary.type_name.to_ascii_uppercase(),
elevation: node.summary.elevation.map(f64::to_bits),
children: node.children.iter().map(tree).collect(),
elements: node.elements.iter().map(|e| e.express_id).collect(),
}
}
fn surface(ifc: &str) -> Surface {
let mut bootstrap: Option<QuickMetadataBootstrap> = None;
let result = process_geometry_streaming_with_options_and_bootstrap(
ifc.as_bytes(),
StreamingOptions {
emit_quick_metadata_bootstrap: true,
..StreamingOptions::default()
},
|_, _, _| {},
|_| {},
|b| bootstrap = Some(b.clone()),
);
let mut meshes: Vec<_> = result
.meshes
.iter()
.map(|m| Mesh {
express_id: m.express_id,
geometry_item_id: m.geometry_item_id,
ifc_type: m.ifc_type.clone(),
positions: m.positions.iter().map(|v| v.to_bits()).collect(),
normals: m.normals.iter().map(|v| v.to_bits()).collect(),
indices: m.indices.clone(),
color: m.color.map(f32::to_bits),
material_name: m.material_name.clone(),
})
.collect();
meshes.sort();
let bits = |t: &Option<Vec<f64>>| t.as_ref().map(|v| v.iter().map(|x| x.to_bits()).collect());
Surface {
meshes,
site_transform: bits(&result.site_transform),
building_transform: bits(&result.building_transform),
length_unit_scale: result.metadata.length_unit_scale.map(f64::to_bits),
entity_count: result.metadata.entity_count,
geometry_entity_count: result.metadata.geometry_entity_count,
spatial_tree: bootstrap
.expect("quick metadata bootstrap was requested")
.spatial_tree
.as_ref()
.map(tree),
}
}
fn assert_fixture_exercises_the_class(upper: &Surface) {
let tris = |id: u32| -> usize {
upper
.meshes
.iter()
.filter(|m| m.express_id == id)
.map(|m| m.indices.len() / 3)
.sum()
};
assert!(tris(100) > 12, "wall #100 was not voided: {} triangles", tris(100));
assert!(tris(600) > 12, "wall #600 was not voided: {} triangles", tris(600));
let column = upper.meshes.iter().find(|m| m.express_id == 500).expect("column #500 meshed");
assert_eq!(
column.color.map(f32::from_bits).map(|c| (c * 100.0).round() / 100.0),
[0.62, 0.6, 0.55, 1.0],
"column #500 did not take its styled-item colour"
);
let proxy = upper.meshes.iter().find(|m| m.express_id == 400).expect("proxy #400 meshed");
assert_eq!(
proxy.color.map(f32::from_bits).map(|c| (c * 100.0).round() / 100.0),
[0.2, 0.4, 0.8, 0.5],
"proxy #400 did not take its material colour"
);
assert!(upper.site_transform.is_some(), "no IfcSite transform");
assert!(upper.building_transform.is_some(), "no IfcBuilding transform");
let root = upper.spatial_tree.as_ref().expect("spatial tree");
assert_eq!(root.express_id, 1, "root must be the IfcProject");
let storey = &root.children[0].children[0].children[0];
assert_eq!(storey.express_id, 902);
assert_eq!(storey.elements, vec![100, 400, 500, 600]);
}
#[test]
fn recased_keywords_process_identically_to_uppercase() {
let upper_ifc = fixture_with_spatial_tree();
let upper = surface(&upper_ifc);
assert_fixture_exercises_the_class(&upper);
for (label, recase) in [
("lowercase", lowercase as fn(&str) -> String),
("capitalised", capitalised),
("alternating", alternating),
] {
let recased_ifc = recase_keywords(&upper_ifc, recase);
assert_ne!(recased_ifc, upper_ifc, "{label}: recasing changed nothing");
let recased = surface(&recased_ifc);
assert_eq!(
recased, upper,
"{label}-keyword file processed differently from the uppercase original"
);
}
}
#[test]
fn shard_classifier_is_keyword_case_insensitive() {
let keywords = [
"IFCPROJECT",
"IFCSITE",
"IFCSTYLEDITEM",
"IFCINDEXEDCOLOURMAP",
"IFCMATERIALDEFINITIONREPRESENTATION",
"IFCRELASSOCIATESMATERIAL",
"IFCRELVOIDSELEMENT",
"IFCRELFILLSELEMENT",
"IFCRELAGGREGATES",
"IFCMAPPEDITEM",
"IFCRELDEFINESBYTYPE",
"IFCMATERIALLAYERSET",
"IFCMATERIALLAYERSETUSAGE",
"IFCWALLTYPE",
"IFCWALL",
];
for keyword in keywords {
let expected = classify_type_name(keyword);
assert_ne!(expected, PREPASS_CLASS_NONE, "{keyword} must classify as something");
for recased in [lowercase(keyword), capitalised(keyword), alternating(keyword)] {
let got = classify_type_name(&recased);
assert_eq!(
got, expected,
"{recased}: class byte {got:#04x} differs from {keyword}'s {expected:#04x}"
);
}
}
}