use crate::types::response::{
QuickMetadataEntitySummary, QuickMetadataPrunedEdge, QuickMetadataPrunedEdgeKind as EdgeKind,
QuickMetadataSpatialNode,
};
use ifc_lite_core::limits::LARGE_COORD_THRESHOLD_METERS;
use ifc_lite_core::{keyword_eq, IfcType, StepListItems, IFC_TYPES};
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
#[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) contained: Vec<u32>,
pub(super) elements: Vec<u32>,
pub(super) named_as_child: bool,
}
fn is_quick_spatial_type(ifc_type: IfcType) -> bool {
ifc_type == IfcType::IfcProject
|| (ifc_type.is_subtype_of(IfcType::IfcSpatialElement)
&& !ifc_type.is_subtype_of(IfcType::IfcExternalSpatialStructureElement))
}
static QUICK_SPATIAL_TYPE_NAMES: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
IFC_TYPES
.iter()
.filter(|ifc_type| is_quick_spatial_type(**ifc_type))
.map(|ifc_type| ifc_type.as_str())
.collect()
});
#[inline]
pub fn is_quick_spatial_type_ci(type_name: &str) -> bool {
QUICK_SPATIAL_TYPE_NAMES
.iter()
.any(|candidate| keyword_eq(type_name, candidate))
}
pub(super) fn parse_step_arguments(entity_bytes: &[u8]) -> Vec<&[u8]> {
StepListItems::of_record(entity_bytes).map(Iterator::collect).unwrap_or_default()
}
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> {
match StepListItems::of_list(token) {
Some(items) => items.filter_map(parse_step_ref).collect(),
None => parse_step_ref(token).into_iter().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() < LARGE_COORD_THRESHOLD_METERS)
}
const MAX_QUICK_SPATIAL_TREE_DEPTH: usize = 60;
pub(super) fn build_quick_spatial_tree_node(
express_id: u32,
nodes: &HashMap<u32, QuickSpatialNodeEntry>,
element_summaries: &HashMap<u32, QuickMetadataEntitySummary>,
) -> Result<(QuickMetadataSpatialNode, Vec<QuickMetadataPrunedEdge>), String> {
let mut placed = HashMap::with_capacity(nodes.len());
placed.insert(express_id, None);
let mut pruned = Vec::new();
let containment = ContainmentPlan::new(express_id, nodes);
build_subtree(
express_id,
0,
nodes,
element_summaries,
&containment,
&mut placed,
&mut pruned,
)
.map(|tree| (tree, pruned))
}
struct ContainmentPlan {
settled: HashSet<u32>,
}
impl ContainmentPlan {
fn new(root: u32, nodes: &HashMap<u32, QuickSpatialNodeEntry>) -> Self {
let aggregated: HashSet<u32> = nodes
.values()
.flat_map(|n| n.children.iter().copied())
.collect();
let mut reached = HashSet::from([root]);
let mut stack = vec![root];
while let Some(id) = stack.pop() {
let Some(node) = nodes.get(&id) else { continue };
let contained = node.contained.iter().filter(|c| !aggregated.contains(c));
for &child in node.children.iter().chain(contained) {
if reached.insert(child) {
stack.push(child);
}
}
}
reached.retain(|id| aggregated.contains(id));
Self { settled: reached }
}
fn follows(&self, child: u32) -> bool {
!self.settled.contains(&child)
}
}
fn build_subtree(
express_id: u32,
depth: usize,
nodes: &HashMap<u32, QuickSpatialNodeEntry>,
element_summaries: &HashMap<u32, QuickMetadataEntitySummary>,
containment: &ContainmentPlan,
placed: &mut HashMap<u32, Option<u32>>,
pruned: &mut Vec<QuickMetadataPrunedEdge>,
) -> Result<QuickMetadataSpatialNode, String> {
let node = nodes
.get(&express_id)
.ok_or_else(|| format!("Quick spatial node #{express_id} not found"))?;
let mut children = Vec::with_capacity(node.children.len() + node.contained.len());
let aggregated = node.children.iter().map(|&id| (id, true));
let contained = node.contained.iter().map(|&id| (id, false));
for (child_id, via_aggregate) in aggregated.chain(contained) {
if !via_aggregate && (placed.contains_key(&child_id) || !containment.follows(child_id)) {
continue;
}
let skipped = match placed.get(&child_id) {
Some(None) => Some(EdgeKind::BackEdge),
Some(Some(parent)) if *parent == express_id => Some(EdgeKind::SiblingRepeat),
Some(Some(_)) => Some(EdgeKind::SecondParent),
None if depth == MAX_QUICK_SPATIAL_TREE_DEPTH => Some(EdgeKind::DepthLimit),
None => None,
};
if let Some(kind) = skipped {
pruned.push(QuickMetadataPrunedEdge {
parent_express_id: express_id,
child_express_id: child_id,
kind,
});
continue;
}
placed.insert(child_id, None);
children.push(build_subtree(
child_id,
depth + 1,
nodes,
element_summaries,
containment,
placed,
pruned,
)?);
placed.insert(child_id, Some(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: !children.is_empty() || !node.elements.is_empty(),
element_count: Some(node.elements.len()),
elevation: node.elevation,
},
children,
elements,
})
}
#[cfg(test)]
#[path = "quick_metadata_tests.rs"]
mod tests;