use std::collections::BTreeMap;
use axiolid_model::{GeometryGraphBuilder, GeometryNode, GraphError, NodeId};
use ifc_model::{EntityId, Model};
use crate::error::{GeometryError, GeometryResult};
use crate::lower::{LoweredGeometry, ProvenanceMap};
use crate::slots::Slots;
use crate::transform::Transform;
use crate::units::UnitScale;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SessionLimits {
pub max_depth: usize,
pub max_aggregate_elements: usize,
}
impl SessionLimits {
pub const DEFAULT_MAX_DEPTH: usize = 64;
pub const DEFAULT_MAX_AGGREGATE_ELEMENTS: usize = 16_777_216;
}
impl Default for SessionLimits {
fn default() -> Self {
Self {
max_depth: Self::DEFAULT_MAX_DEPTH,
max_aggregate_elements: Self::DEFAULT_MAX_AGGREGATE_ELEMENTS,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum DegenerateFacePolicy {
#[default]
Refuse,
DropAndReport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct MemoKey {
entity: u64,
family: &'static str,
basis: [[u64; 3]; 3],
origin: [u64; 3],
}
impl MemoKey {
fn new(entity: EntityId, family: &'static str, frame: Transform) -> Self {
Self {
entity: entity.0,
family,
basis: frame.basis.map(|axis| axis.map(f64::to_bits)),
origin: frame.origin.map(f64::to_bits),
}
}
}
#[derive(Debug)]
pub struct LoweringSession<'a> {
model: &'a Model,
units: &'a UnitScale,
limits: SessionLimits,
builder: GeometryGraphBuilder,
nodes: usize,
memo: BTreeMap<MemoKey, NodeId>,
active: Vec<EntityId>,
provenance: ProvenanceMap,
texture_maps: Option<BTreeMap<EntityId, Vec<EntityId>>>,
shapes: BTreeMap<NodeId, NodeShape>,
face_policy: DegenerateFacePolicy,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum NodeShape {
Collection(Vec<NodeId>),
Instance {
source: NodeId,
transform: axiolid_core::Transform3,
},
Solid,
Other,
}
impl NodeShape {
fn of(node: &GeometryNode) -> Self {
match node {
GeometryNode::Collection(members) => Self::Collection(members.clone()),
GeometryNode::Instance(instance) => Self::Instance {
source: instance.source,
transform: instance.transform,
},
GeometryNode::Primitive(_)
| GeometryNode::HalfSpace(_)
| GeometryNode::SolidOperation(_)
| GeometryNode::BRep(_)
| GeometryNode::PolygonMesh(_)
| GeometryNode::TriMesh(_) => Self::Solid,
_ => Self::Other,
}
}
}
impl<'a> LoweringSession<'a> {
pub fn new(model: &'a Model, units: &'a UnitScale) -> Self {
Self::with_limits(model, units, SessionLimits::default())
}
pub fn with_limits(model: &'a Model, units: &'a UnitScale, limits: SessionLimits) -> Self {
Self {
model,
units,
limits,
builder: GeometryGraphBuilder::new(),
nodes: 0,
memo: BTreeMap::new(),
active: Vec::new(),
provenance: ProvenanceMap::default(),
texture_maps: None,
shapes: BTreeMap::new(),
face_policy: DegenerateFacePolicy::default(),
}
}
#[must_use]
pub fn with_face_policy(mut self, policy: DegenerateFacePolicy) -> Self {
self.face_policy = policy;
self
}
pub fn face_policy(&self) -> DegenerateFacePolicy {
self.face_policy
}
pub(crate) fn report_dropped_face(&mut self, face: EntityId) {
self.provenance.record_dropped_face(face);
}
pub(crate) fn triangle_texture_maps(&mut self, face_set: EntityId) -> &[EntityId] {
let model = self.model;
let index = self.texture_maps.get_or_insert_with(|| {
let mut index: BTreeMap<EntityId, Vec<EntityId>> = BTreeMap::new();
for &map in model.ids_of_type("IFCINDEXEDTRIANGLETEXTUREMAP") {
let target = model
.get(map)
.and_then(|entity| entity.attributes.get(1))
.and_then(ifc_model::Value::as_ref_id);
if let Some(target) = target {
index.entry(target).or_default().push(map);
}
}
index
});
index.get(&face_set).map_or(&[], Vec::as_slice)
}
pub fn model(&self) -> &'a Model {
self.model
}
pub fn units(&self) -> &'a UnitScale {
self.units
}
pub fn node_count(&self) -> usize {
self.nodes
}
pub fn node(&mut self, node: GeometryNode) -> GeometryResult<NodeId> {
let source = self.active.last().copied();
let shape = NodeShape::of(&node);
let id = self
.builder
.push(node)
.map_err(|error| graph_error(source.unwrap_or(EntityId(0)), error))?;
self.nodes += 1;
self.shapes.insert(id, shape);
if let Some(source) = source {
self.provenance.record(id, source);
}
Ok(id)
}
pub fn node_for(&mut self, entity: EntityId, node: GeometryNode) -> GeometryResult<NodeId> {
let shape = NodeShape::of(&node);
let id = self
.builder
.push(node)
.map_err(|error| graph_error(entity, error))?;
self.nodes += 1;
self.shapes.insert(id, shape);
self.provenance.record(id, entity);
Ok(id)
}
pub(crate) fn shape(&self, node: NodeId) -> Option<&NodeShape> {
self.shapes.get(&node)
}
pub fn provenance(&self) -> &ProvenanceMap {
&self.provenance
}
pub fn entity(
&self,
referrer: EntityId,
id: EntityId,
) -> GeometryResult<&'a ifc_model::Entity> {
self.model.get(id).ok_or(GeometryError::MissingEntity {
referrer,
missing: id,
})
}
pub fn memoized(
&self,
entity: EntityId,
family: &'static str,
frame: Transform,
) -> Option<NodeId> {
self.memo.get(&MemoKey::new(entity, family, frame)).copied()
}
pub fn memoize(
&mut self,
entity: EntityId,
family: &'static str,
frame: Transform,
node: NodeId,
) {
self.memo.insert(MemoKey::new(entity, family, frame), node);
}
pub fn enter(&mut self, entity: EntityId, kind: &'static str) -> GeometryResult<()> {
if self.active.contains(&entity) {
return Err(GeometryError::CyclicChain { entity, kind });
}
if self.active.len() >= self.limits.max_depth {
return Err(GeometryError::ChainTooDeep {
entity,
kind,
limit: self.limits.max_depth,
});
}
self.active.push(entity);
Ok(())
}
pub fn exit(&mut self, entity: EntityId) {
if self.active.last() == Some(&entity) {
self.active.pop();
return;
}
debug_assert!(false, "lowering scopes must exit in LIFO order");
if let Some(index) = self.active.iter().rposition(|&active| active == entity) {
self.active.remove(index);
}
}
pub fn slots(&self, entity: EntityId) -> GeometryResult<Slots<'a>> {
let resolved = self.entity(entity, entity)?;
Ok(Slots::new(entity, resolved))
}
pub fn type_name(&self, entity: EntityId) -> GeometryResult<String> {
Ok(self.entity(entity, entity)?.type_name.to_ascii_uppercase())
}
pub fn unsupported(
&self,
entity: EntityId,
type_name: &str,
detail: &'static str,
) -> GeometryError {
GeometryError::Unsupported {
entity,
type_name: type_name.to_string(),
detail,
}
}
pub fn degenerate(
&self,
entity: EntityId,
type_name: &str,
detail: impl Into<String>,
) -> GeometryError {
GeometryError::Degenerate {
entity,
type_name: type_name.to_string(),
detail: detail.into(),
}
}
pub fn check_aggregate(
&self,
entity: EntityId,
type_name: &str,
what: &'static str,
requested: u128,
) -> GeometryResult<usize> {
let limit = self.limits.max_aggregate_elements;
if requested > limit as u128 {
return Err(GeometryError::AggregateTooLarge {
entity,
type_name: type_name.to_string(),
what,
requested,
limit,
});
}
Ok(requested as usize)
}
pub fn lower_operand(&mut self, entity: EntityId, frame: Transform) -> GeometryResult<NodeId> {
crate::lower::dispatch::lower_representation_item(self, entity, frame)
}
pub fn finish(self, root: NodeId) -> GeometryResult<LoweredGeometry> {
let entity = self.current_entity();
let graph = self
.builder
.finish(vec![root])
.map_err(|error| graph_error(entity, error))?;
Ok(LoweredGeometry {
graph,
root,
provenance: self.provenance,
})
}
fn current_entity(&self) -> EntityId {
self.active.last().copied().unwrap_or(EntityId(0))
}
}
pub(crate) fn graph_error(entity: EntityId, error: GraphError) -> GeometryError {
GeometryError::Degenerate {
entity,
type_name: "geometry graph".to_string(),
detail: error.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_memo_key_separates_entity_family_and_frame() {
let frame = Transform::identity();
let mut moved = Transform::identity();
moved.origin = [1.0, 0.0, 0.0];
let base = MemoKey::new(EntityId(1), "solid", frame);
assert_eq!(base, MemoKey::new(EntityId(1), "solid", frame));
assert_ne!(base, MemoKey::new(EntityId(2), "solid", frame));
assert_ne!(base, MemoKey::new(EntityId(1), "profile", frame));
assert_ne!(base, MemoKey::new(EntityId(1), "solid", moved));
}
#[test]
fn signed_zero_does_not_alias_positive_zero_in_the_key() {
let mut negative = Transform::identity();
negative.origin = [-0.0, 0.0, 0.0];
assert_ne!(
MemoKey::new(EntityId(1), "solid", Transform::identity()),
MemoKey::new(EntityId(1), "solid", negative)
);
}
#[test]
fn the_default_aggregate_budget_is_documented() {
assert_eq!(
SessionLimits::default().max_aggregate_elements,
SessionLimits::DEFAULT_MAX_AGGREGATE_ELEMENTS
);
assert_eq!(SessionLimits::DEFAULT_MAX_AGGREGATE_ELEMENTS, 16_777_216);
}
#[test]
fn the_default_depth_budget_is_documented() {
assert_eq!(
SessionLimits::default().max_depth,
SessionLimits::DEFAULT_MAX_DEPTH
);
}
}