use crate::error::{GeometryError, GeometryResult};
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod item_slot {
pub const MAPPING_SOURCE: usize = 0;
pub const MAPPING_TARGET: usize = 1;
}
pub(crate) mod map_slot {
pub const MAPPING_ORIGIN: usize = 0;
pub const MAPPED_REPRESENTATION: usize = 1;
}
const MAX_NESTING_DEPTH: usize = 32;
#[derive(Debug, Clone, Copy)]
pub struct MappedItem<'m> {
slots: Slots<'m>,
}
impl<'m> MappedItem<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn mapping_source(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(item_slot::MAPPING_SOURCE, "MappingSource")
}
pub fn mapping_target(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(item_slot::MAPPING_TARGET, "MappingTarget")
}
}
#[derive(Debug, Clone, Copy)]
pub struct RepresentationMap<'m> {
slots: Slots<'m>,
}
impl<'m> RepresentationMap<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn mapping_origin(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(map_slot::MAPPING_ORIGIN, "MappingOrigin")
}
pub fn mapped_representation(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(map_slot::MAPPED_REPRESENTATION, "MappedRepresentation")
}
}
#[derive(Debug, Default)]
pub struct MappingWalker {
visited: Vec<EntityId>,
}
impl MappingWalker {
pub fn new() -> Self {
Self::default()
}
pub fn depth(&self) -> usize {
self.visited.len()
}
pub fn enter(&mut self, item: EntityId) -> GeometryResult<()> {
if self.visited.contains(&item) {
return Err(GeometryError::CyclicChain {
entity: item,
kind: "mapped item",
});
}
if self.visited.len() >= MAX_NESTING_DEPTH {
return Err(GeometryError::ChainTooDeep {
entity: item,
kind: "mapped item",
limit: MAX_NESTING_DEPTH,
});
}
self.visited.push(item);
Ok(())
}
pub fn exit(&mut self) {
self.visited.pop();
}
pub fn resolve(&mut self, model: &Model, item_id: EntityId) -> GeometryResult<MappedInstance> {
let entity = model.get(item_id).ok_or(GeometryError::MissingEntity {
referrer: item_id,
missing: item_id,
})?;
let item = MappedItem::new(item_id, entity);
let source_id = item.mapping_source()?;
let target_id = item.mapping_target()?;
let source = model.get(source_id).ok_or(GeometryError::MissingEntity {
referrer: item_id,
missing: source_id,
})?;
let map = RepresentationMap::new(source_id, source);
Ok(MappedInstance {
item: item_id,
mapping_origin: map.mapping_origin()?,
mapped_representation: map.mapped_representation()?,
mapping_target: target_id,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MappedInstance {
pub item: EntityId,
pub mapping_origin: EntityId,
pub mapped_representation: EntityId,
pub mapping_target: EntityId,
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn model_with_mapping() -> Model {
let mut model = Model::new();
model.insert(
EntityId(14),
Entity::new(
"IFCREPRESENTATIONMAP",
vec![Value::Ref(EntityId(2)), Value::Ref(EntityId(13))],
),
);
model.insert(
EntityId(19),
Entity::new(
"IFCMAPPEDITEM",
vec![Value::Ref(EntityId(14)), Value::Ref(EntityId(18))],
),
);
model
}
#[test]
fn reads_both_halves_of_the_mapping() {
let model = model_with_mapping();
let mut walker = MappingWalker::new();
let resolved = walker.resolve(&model, EntityId(19)).unwrap();
assert_eq!(resolved.mapping_origin, EntityId(2));
assert_eq!(resolved.mapped_representation, EntityId(13));
assert_eq!(resolved.mapping_target, EntityId(18));
}
#[test]
fn the_same_map_may_be_instanced_repeatedly() {
let mut walker = MappingWalker::new();
for _ in 0..100 {
walker
.enter(EntityId(19))
.expect("sibling instances are legal");
walker.exit();
}
assert_eq!(walker.depth(), 0);
}
#[test]
fn revisiting_an_item_while_inside_it_is_a_cycle() {
let mut walker = MappingWalker::new();
walker.enter(EntityId(19)).unwrap();
let err = walker.enter(EntityId(19)).unwrap_err();
assert!(
matches!(err, GeometryError::CyclicChain { .. }),
"got {err}"
);
}
#[test]
fn nesting_is_bounded() {
let mut walker = MappingWalker::new();
for i in 0..MAX_NESTING_DEPTH {
walker.enter(EntityId(i as u64)).unwrap();
}
let err = walker.enter(EntityId(9999)).unwrap_err();
assert!(
matches!(err, GeometryError::ChainTooDeep { .. }),
"got {err}"
);
}
#[test]
fn dangling_mapping_source_is_reported() {
let mut model = Model::new();
model.insert(
EntityId(19),
Entity::new(
"IFCMAPPEDITEM",
vec![Value::Ref(EntityId(999)), Value::Ref(EntityId(18))],
),
);
let mut walker = MappingWalker::new();
assert!(matches!(
walker.resolve(&model, EntityId(19)).unwrap_err(),
GeometryError::MissingEntity { .. }
));
}
}