use std::collections::HashSet;
use ifc_geometry::{context_of, geometric_products, TargetView};
use ifc_model::{EntityId, Model, Value};
use ifc_spatial::SpatialTree;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Unreachable {
NotContainedInSpatialStructure,
NoRepresentationInModelContext {
found: Vec<String>,
},
RepresentationWithoutContext,
}
impl Unreachable {
#[must_use]
pub fn message(&self) -> String {
match self {
Self::NotContainedInSpatialStructure => {
"not contained in the spatial structure: no viewer will traverse to it \
(add an IfcRelContainedInSpatialStructure to a storey)"
.to_string()
}
Self::NoRepresentationInModelContext { found } => format!(
"geometry exists only in {} context(s): a model viewer renders Model \
and will skip it",
if found.is_empty() {
"non-model".to_string()
} else {
found.join(", ")
}
),
Self::RepresentationWithoutContext => {
"representation has no resolvable context: a viewer cannot decide \
when to draw it"
.to_string()
}
}
}
}
#[must_use]
pub fn unreachable_products(model: &Model) -> Vec<(EntityId, Unreachable)> {
let tree = SpatialTree::build(model);
let excused = ExcusedByRelationship::index(model);
let mut findings = Vec::new();
for id in geometric_products(model) {
if model.get(id).is_none() {
continue;
}
if tree.node(id).is_some() {
continue;
}
if tree.container_of(id).is_none() && !excused.contains(id) {
findings.push((id, Unreachable::NotContainedInSpatialStructure));
continue;
}
if let Some(finding) = context_finding(model, id) {
findings.push((id, finding));
}
}
findings
}
fn context_finding(model: &Model, product: EntityId) -> Option<Unreachable> {
let representations = representations_of(model, product);
if representations.is_empty() {
return None;
}
let mut found = Vec::new();
let mut any_context = false;
for representation in representations {
let Some(context) = context_of(model, representation) else {
continue;
};
any_context = true;
match context.target_view() {
Some(TargetView::ModelView | TargetView::NotDefined) | None => return None,
Some(other) => {
let label = format!("{other:?}");
if !found.contains(&label) {
found.push(label);
}
}
}
}
if !any_context {
return Some(Unreachable::RepresentationWithoutContext);
}
Some(Unreachable::NoRepresentationInModelContext { found })
}
fn representations_of(model: &Model, product: EntityId) -> Vec<EntityId> {
const REPRESENTATION: usize = 6;
const REPRESENTATIONS: usize = 2;
let Some(entity) = model.get(product) else {
return Vec::new();
};
let Some(Value::Ref(shape)) = entity.attributes.get(REPRESENTATION) else {
return Vec::new();
};
let Some(shape_entity) = model.get(*shape) else {
return Vec::new();
};
match shape_entity.attributes.get(REPRESENTATIONS) {
Some(Value::List(items)) => items
.iter()
.filter_map(|v| match v {
Value::Ref(r) => Some(*r),
_ => None,
})
.collect(),
_ => Vec::new(),
}
}
struct ExcusedByRelationship {
ids: HashSet<EntityId>,
}
impl ExcusedByRelationship {
const RELATED_SLOT: usize = 5;
const KINDS: [&'static str; 5] = [
"IFCRELAGGREGATES",
"IFCRELNESTS",
"IFCRELVOIDSELEMENT",
"IFCRELFILLSELEMENT",
"IFCRELPROJECTSELEMENT",
];
fn index(model: &Model) -> Self {
let mut ids = HashSet::new();
for (_, entity) in model.iter() {
let upper = entity.type_name.to_ascii_uppercase();
if !Self::KINDS.contains(&upper.as_str()) {
continue;
}
match entity.attributes.get(Self::RELATED_SLOT) {
Some(Value::Ref(r)) => {
ids.insert(*r);
}
Some(Value::List(items)) => {
ids.extend(items.iter().filter_map(|v| match v {
Value::Ref(r) => Some(*r),
_ => None,
}));
}
_ => {}
}
}
Self { ids }
}
fn contains(&self, id: EntityId) -> bool {
self.ids.contains(&id)
}
}
#[cfg(test)]
#[path = "unreachable/tests.rs"]
mod tests;