use crate::error::{GeometryError, GeometryResult};
use crate::select::subtype::is_a;
use ifc_model::{Entity, EntityId, Model, Value};
fn not_a_member(entity: EntityId, actual: &str, expected: &'static str) -> GeometryError {
GeometryError::WrongEntityType {
entity,
actual: actual.to_string(),
expected,
}
}
fn resolve_and_classify<T>(
model: &Model,
referrer: EntityId,
target: EntityId,
expected: &'static str,
f: impl Fn(&str) -> Option<T>,
) -> GeometryResult<T> {
let entity: &Entity = model.get(target).ok_or(GeometryError::MissingEntity {
referrer,
missing: target,
})?;
f(&entity.type_name).ok_or_else(|| not_a_member(target, &entity.type_name, expected))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis2Placement {
TwoD(EntityId),
ThreeD(EntityId),
}
impl Axis2Placement {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
resolve_and_classify(model, referrer, target, "IfcAxis2Placement", |t| {
if is_a(t, "IFCAXIS2PLACEMENT2D") {
Some(Self::TwoD(target))
} else if is_a(t, "IFCAXIS2PLACEMENT3D") {
Some(Self::ThreeD(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::TwoD(id) | Self::ThreeD(id) => *id,
}
}
pub fn dimension(&self) -> usize {
match self {
Self::TwoD(_) => 2,
Self::ThreeD(_) => 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BooleanOperand {
Solid(EntityId),
HalfSpace(EntityId),
BooleanResult(EntityId),
CsgPrimitive(EntityId),
TessellatedFaceSet(EntityId),
}
impl BooleanOperand {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
resolve_and_classify(model, referrer, target, "IfcBooleanOperand", |t| {
if is_a(t, "IFCHALFSPACESOLID") {
Some(Self::HalfSpace(target))
} else if is_a(t, "IFCBOOLEANRESULT") {
Some(Self::BooleanResult(target))
} else if is_a(t, "IFCCSGPRIMITIVE3D") {
Some(Self::CsgPrimitive(target))
} else if is_a(t, "IFCTESSELLATEDFACESET") {
Some(Self::TessellatedFaceSet(target))
} else if is_a(t, "IFCSOLIDMODEL") {
Some(Self::Solid(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::Solid(id)
| Self::HalfSpace(id)
| Self::BooleanResult(id)
| Self::CsgPrimitive(id)
| Self::TessellatedFaceSet(id) => *id,
}
}
pub fn is_unbounded(&self) -> bool {
matches!(self, Self::HalfSpace(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CsgSelect {
BooleanResult(EntityId),
Primitive(EntityId),
}
impl CsgSelect {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
resolve_and_classify(model, referrer, target, "IfcCsgSelect", |t| {
if is_a(t, "IFCBOOLEANRESULT") {
Some(Self::BooleanResult(target))
} else if is_a(t, "IFCCSGPRIMITIVE3D") {
Some(Self::Primitive(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::BooleanResult(id) | Self::Primitive(id) => *id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolidOrShell {
ClosedShell(EntityId),
Solid(EntityId),
}
impl SolidOrShell {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
resolve_and_classify(model, referrer, target, "IfcSolidOrShell", |t| {
if is_a(t, "IFCCLOSEDSHELL") {
Some(Self::ClosedShell(target))
} else if is_a(t, "IFCSOLIDMODEL") {
Some(Self::Solid(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::ClosedShell(id) | Self::Solid(id) => *id,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TrimmingSelect {
pub point: Option<EntityId>,
pub parameter: Option<f64>,
}
impl TrimmingSelect {
pub fn from_value(value: &Value) -> Self {
let mut out = Self {
point: None,
parameter: None,
};
let items: Vec<&Value> = match value {
Value::List(items) => items.iter().collect(),
single => vec![single],
};
for item in items {
match item {
Value::Ref(id) => out.point = Some(*id),
other => {
if let Some(n) = other.unwrap_typed().as_f64() {
out.parameter = Some(n);
}
}
}
}
out
}
pub fn is_empty(&self) -> bool {
self.point.is_none() && self.parameter.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VectorOrDirection {
Direction(EntityId),
Vector(EntityId),
}
impl VectorOrDirection {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
resolve_and_classify(model, referrer, target, "IfcVectorOrDirection", |t| {
if is_a(t, "IFCVECTOR") {
Some(Self::Vector(target))
} else if is_a(t, "IFCDIRECTION") {
Some(Self::Direction(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::Direction(id) | Self::Vector(id) => *id,
}
}
pub fn has_magnitude(&self) -> bool {
matches!(self, Self::Vector(_))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn model_with(id: u64, type_name: &str) -> Model {
let mut model = Model::new();
model.insert(EntityId(id), Entity::new(type_name, vec![Value::Null; 4]));
model
}
#[test]
fn a_concrete_solid_resolves_as_the_abstract_solid_model_branch() {
let model = model_with(5, "IFCEXTRUDEDAREASOLID");
let operand = BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap();
assert_eq!(operand, BooleanOperand::Solid(EntityId(5)));
assert!(!operand.is_unbounded());
}
#[test]
fn half_space_operands_are_flagged_unbounded() {
for t in ["IFCHALFSPACESOLID", "IFCPOLYGONALBOUNDEDHALFSPACE"] {
let model = model_with(5, t);
let operand = BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap();
assert!(operand.is_unbounded(), "{t} must be unbounded");
}
}
#[test]
fn nested_boolean_results_are_their_own_branch() {
let model = model_with(5, "IFCBOOLEANCLIPPINGRESULT");
assert_eq!(
BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap(),
BooleanOperand::BooleanResult(EntityId(5))
);
}
#[test]
fn an_entity_outside_the_select_is_rejected_with_its_actual_type() {
let model = model_with(5, "IFCWALL");
let err = BooleanOperand::resolve(&model, EntityId(1), EntityId(5)).unwrap_err();
assert!(err.to_string().contains("IFCWALL"), "got {err}");
}
#[test]
fn a_dangling_reference_is_reported_as_missing_not_as_wrong_type() {
let model = Model::new();
assert!(matches!(
BooleanOperand::resolve(&model, EntityId(1), EntityId(99)).unwrap_err(),
GeometryError::MissingEntity { .. }
));
}
#[test]
fn placement_dimension_comes_from_the_target_type() {
let two = model_with(5, "IFCAXIS2PLACEMENT2D");
assert_eq!(
Axis2Placement::resolve(&two, EntityId(1), EntityId(5))
.unwrap()
.dimension(),
2
);
let three = model_with(5, "IFCAXIS2PLACEMENT3D");
assert_eq!(
Axis2Placement::resolve(&three, EntityId(1), EntityId(5))
.unwrap()
.dimension(),
3
);
}
#[test]
fn a_trim_can_carry_a_point_and_a_parameter_together() {
let trim = TrimmingSelect::from_value(&Value::List(vec![
Value::Ref(EntityId(7)),
Value::Typed {
type_name: "IFCPARAMETERVALUE".into(),
value: Box::new(Value::Real(0.75)),
},
]));
assert_eq!(trim.point, Some(EntityId(7)));
assert_eq!(trim.parameter, Some(0.75));
assert!(!trim.is_empty());
}
#[test]
fn a_trim_may_be_a_bare_parameter() {
let trim = TrimmingSelect::from_value(&Value::Typed {
type_name: "IFCPARAMETERVALUE".into(),
value: Box::new(Value::Real(0.0)),
});
assert_eq!(trim.parameter, Some(0.0));
assert_eq!(trim.point, None);
}
#[test]
fn vectors_are_distinguished_from_directions() {
let vector = model_with(5, "IFCVECTOR");
assert!(
VectorOrDirection::resolve(&vector, EntityId(1), EntityId(5))
.unwrap()
.has_magnitude()
);
let direction = model_with(5, "IFCDIRECTION");
assert!(
!VectorOrDirection::resolve(&direction, EntityId(1), EntityId(5))
.unwrap()
.has_magnitude()
);
}
#[test]
fn solid_or_shell_separates_boundaries_from_solids() {
let shell = model_with(5, "IFCCLOSEDSHELL");
assert_eq!(
SolidOrShell::resolve(&shell, EntityId(1), EntityId(5)).unwrap(),
SolidOrShell::ClosedShell(EntityId(5))
);
let solid = model_with(5, "IFCFACETEDBREP");
assert_eq!(
SolidOrShell::resolve(&solid, EntityId(1), EntityId(5)).unwrap(),
SolidOrShell::Solid(EntityId(5))
);
}
#[test]
fn csg_branches_separate_nested_booleans_from_leaf_primitives() {
let mut model = Model::new();
model.insert(EntityId(1), Entity::new("IFCBOOLEANRESULT", vec![]));
model.insert(EntityId(2), Entity::new("IFCBLOCK", vec![]));
model.insert(EntityId(3), Entity::new("IFCPOLYLINE", vec![]));
assert_eq!(
CsgSelect::resolve(&model, EntityId(9), EntityId(1)).unwrap(),
CsgSelect::BooleanResult(EntityId(1))
);
assert_eq!(
CsgSelect::resolve(&model, EntityId(9), EntityId(2)).unwrap(),
CsgSelect::Primitive(EntityId(2)),
"IfcBlock is an IfcCsgPrimitive3D leaf"
);
let error = CsgSelect::resolve(&model, EntityId(9), EntityId(3))
.expect_err("a polyline is not a CSG node");
assert_eq!(error.entity(), Some(EntityId(3)));
}
}