use crate::types::response::{QuickMetadataEntitySummary, QuickMetadataSpatialNode};
use std::collections::{HashMap, HashSet};
#[derive(Clone)]
pub(super) struct QuickSpatialNodeEntry {
pub(super) express_id: u32,
pub(super) type_name: String,
pub(super) name: String,
pub(super) elevation: Option<f64>,
pub(super) children: Vec<u32>,
pub(super) elements: Vec<u32>,
pub(super) parent: Option<u32>,
}
#[inline]
pub(super) fn is_quick_spatial_type_ci(type_name: &str) -> bool {
type_name.eq_ignore_ascii_case("IFCPROJECT")
|| type_name.eq_ignore_ascii_case("IFCSITE")
|| type_name.eq_ignore_ascii_case("IFCBUILDING")
|| type_name.eq_ignore_ascii_case("IFCBUILDINGSTOREY")
|| type_name.eq_ignore_ascii_case("IFCSPACE")
|| type_name.eq_ignore_ascii_case("IFCSPATIALZONE")
|| type_name.eq_ignore_ascii_case("IFCFACILITY")
|| type_name.eq_ignore_ascii_case("IFCFACILITYPART")
|| type_name.eq_ignore_ascii_case("IFCBRIDGE")
|| type_name.eq_ignore_ascii_case("IFCBRIDGEPART")
|| type_name.eq_ignore_ascii_case("IFCROAD")
|| type_name.eq_ignore_ascii_case("IFCROADPART")
|| type_name.eq_ignore_ascii_case("IFCRAILWAY")
|| type_name.eq_ignore_ascii_case("IFCRAILWAYPART")
}
pub(super) fn parse_step_arguments(entity_bytes: &[u8]) -> Vec<&[u8]> {
let Some(open_idx) = entity_bytes.iter().position(|byte| *byte == b'(') else {
return Vec::new();
};
let Some(close_idx) = entity_bytes.iter().rposition(|byte| *byte == b')') else {
return Vec::new();
};
if close_idx <= open_idx {
return Vec::new();
}
let args = &entity_bytes[open_idx + 1..close_idx];
let mut parts = Vec::new();
let mut in_string = false;
let mut depth = 0i32;
let mut start = 0usize;
let bytes = args;
let mut index = 0usize;
while index < bytes.len() {
match bytes[index] {
b'\'' => {
if in_string && index + 1 < bytes.len() && bytes[index + 1] == b'\'' {
index += 1;
} else {
in_string = !in_string;
}
}
b'(' if !in_string => depth += 1,
b')' if !in_string => depth -= 1,
b',' if !in_string && depth == 0 => {
parts.push(args[start..index].trim_ascii());
start = index + 1;
}
_ => {}
}
index += 1;
}
if start <= args.len() {
parts.push(args[start..].trim_ascii());
}
parts
}
fn parse_step_string(token: &[u8]) -> Option<String> {
let trimmed = token.trim_ascii();
if trimmed.len() < 2 || trimmed[0] != b'\'' || trimmed[trimmed.len() - 1] != b'\'' {
return None;
}
let unescaped = String::from_utf8_lossy(&trimmed[1..trimmed.len() - 1]).replace("''", "'");
Some(ifc_lite_core::decode_ifc_string(&unescaped).into_owned())
}
pub(super) fn parse_step_ref(token: &[u8]) -> Option<u32> {
std::str::from_utf8(token.trim_ascii().strip_prefix(b"#")?)
.ok()?
.parse()
.ok()
}
pub(super) fn parse_step_ref_list(token: &[u8]) -> Vec<u32> {
let trimmed = token.trim_ascii();
let inner = trimmed
.strip_prefix(b"(")
.and_then(|value| value.strip_suffix(b")"))
.unwrap_or(trimmed);
inner.split(|byte| *byte == b',').filter_map(parse_step_ref).collect()
}
pub(super) fn extract_name_from_args(args: &[&[u8]], fallback: &str) -> String {
args.get(2)
.and_then(|token| parse_step_string(token))
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| fallback.to_string())
}
pub(super) fn extract_storey_elevation_from_args(args: &[&[u8]]) -> Option<f64> {
for index in [9usize, 8usize] {
if let Some(value) = args
.get(index)
.and_then(|token| std::str::from_utf8(token.trim_ascii()).ok())
.and_then(|token| token.parse::<f64>().ok())
{
return Some(value);
}
}
args.iter()
.filter_map(|token| std::str::from_utf8(token.trim_ascii()).ok())
.filter_map(|token| token.parse::<f64>().ok())
.find(|value| value.abs() < 10_000.0)
}
pub(super) fn build_quick_spatial_tree_node(
express_id: u32,
nodes: &HashMap<u32, QuickSpatialNodeEntry>,
element_summaries: &HashMap<u32, QuickMetadataEntitySummary>,
) -> Result<QuickMetadataSpatialNode, String> {
let mut ancestors = HashSet::new();
build_quick_spatial_tree_node_inner(express_id, nodes, element_summaries, &mut ancestors)
}
fn build_quick_spatial_tree_node_inner(
express_id: u32,
nodes: &HashMap<u32, QuickSpatialNodeEntry>,
element_summaries: &HashMap<u32, QuickMetadataEntitySummary>,
ancestors: &mut HashSet<u32>,
) -> Result<QuickMetadataSpatialNode, String> {
let node = nodes
.get(&express_id)
.ok_or_else(|| format!("Quick spatial node #{express_id} not found"))?;
ancestors.insert(express_id);
let mut children = Vec::with_capacity(node.children.len());
for child_id in &node.children {
if ancestors.contains(child_id) {
continue;
}
children.push(build_quick_spatial_tree_node_inner(
*child_id,
nodes,
element_summaries,
ancestors,
)?);
}
ancestors.remove(&express_id);
let elements = node
.elements
.iter()
.map(|element_id| {
element_summaries
.get(element_id)
.cloned()
.unwrap_or(QuickMetadataEntitySummary {
express_id: *element_id,
type_name: "IfcProduct".to_string(),
name: format!("IfcProduct #{}", element_id),
global_id: None,
kind: "element".to_string(),
has_children: false,
element_count: None,
elevation: None,
})
})
.collect();
Ok(QuickMetadataSpatialNode {
summary: QuickMetadataEntitySummary {
express_id: node.express_id,
type_name: node.type_name.clone(),
name: node.name.clone(),
global_id: None,
kind: "spatial".to_string(),
has_children: !node.children.is_empty() || !node.elements.is_empty(),
element_count: Some(node.elements.len()),
elevation: node.elevation,
},
children,
elements,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn node(id: u32, children: Vec<u32>) -> QuickSpatialNodeEntry {
QuickSpatialNodeEntry {
express_id: id,
type_name: "IfcSpace".to_string(),
name: format!("#{id}"),
elevation: None,
children,
elements: vec![],
parent: None,
}
}
#[test]
fn parse_step_string_un_doubles_exactly_once() {
assert_eq!(parse_step_string(b"'O''Brien'").as_deref(), Some("O'Brien"));
assert_eq!(parse_step_string(b"''''''").as_deref(), Some("''"));
assert_eq!(parse_step_string(br"'C:\\temp'").as_deref(), Some(r"C:\temp"));
assert_eq!(parse_step_string(br"'caf\X2\00E9\X0\'").as_deref(), Some("caf\u{e9}"));
assert_eq!(parse_step_string(b"'Plain Name'").as_deref(), Some("Plain Name"));
}
#[test]
fn cyclic_aggregate_graph_does_not_stack_overflow() {
let mut nodes = HashMap::new();
nodes.insert(1, node(1, vec![2]));
nodes.insert(2, node(2, vec![1]));
let summaries = HashMap::new();
let tree = build_quick_spatial_tree_node(1, &nodes, &summaries);
assert!(tree.is_ok(), "cyclic tree should build (cycle pruned), got {tree:?}");
}
#[test]
fn storey_elevation_prefers_index_9_over_index_8() {
let args: Vec<&[u8]> = vec![
b"$", b"$", b"$", b"$", b"$", b"$", b"$", b"$", b"3.5", b"7.25",
];
assert_eq!(
extract_storey_elevation_from_args(&args),
Some(7.25),
"index 9 (the real Elevation attribute) must win over index 8"
);
}
}