use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod slot {
pub const OPERATOR: usize = 0;
pub const FIRST_OPERAND: usize = 1;
pub const SECOND_OPERAND: usize = 2;
}
#[cfg_attr(feature = "lowering", doc = "See [`axiolid_core::BooleanOperator`].")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IfcBooleanOperator {
Union,
Intersection,
Difference,
}
pub use IfcBooleanOperator as BooleanOperator;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("unknown IfcBooleanOperator token")]
pub struct ParseIfcBooleanOperatorError;
impl core::str::FromStr for IfcBooleanOperator {
type Err = ParseIfcBooleanOperatorError;
fn from_str(token: &str) -> Result<Self, Self::Err> {
let bare = token.trim_matches('.');
if bare.eq_ignore_ascii_case("UNION") {
Ok(Self::Union)
} else if bare.eq_ignore_ascii_case("INTERSECTION") {
Ok(Self::Intersection)
} else if bare.eq_ignore_ascii_case("DIFFERENCE") {
Ok(Self::Difference)
} else {
Err(ParseIfcBooleanOperatorError)
}
}
}
impl core::fmt::Display for IfcBooleanOperator {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(formatter, ".{}.", self.as_token())
}
}
#[cfg(feature = "lowering")]
impl From<IfcBooleanOperator> for axiolid_core::BooleanOperator {
fn from(value: IfcBooleanOperator) -> Self {
match value {
IfcBooleanOperator::Union => Self::Union,
IfcBooleanOperator::Intersection => Self::Intersection,
IfcBooleanOperator::Difference => Self::Difference,
}
}
}
impl IfcBooleanOperator {
pub fn parse(token: &str) -> Option<Self> {
token.parse().ok()
}
pub fn as_token(self) -> &'static str {
match self {
Self::Union => "UNION",
Self::Intersection => "INTERSECTION",
Self::Difference => "DIFFERENCE",
}
}
pub fn is_order_sensitive(self) -> bool {
matches!(self, Self::Difference)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperandKind {
BooleanResult,
CsgPrimitive,
HalfSpace,
SolidModel,
TessellatedFaceSet,
}
impl OperandKind {
pub fn classify(type_name: &str) -> Option<Self> {
let n = type_name.to_ascii_uppercase();
let kind = match n.as_str() {
"IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => Self::BooleanResult,
"IFCBLOCK"
| "IFCRECTANGULARPYRAMID"
| "IFCRIGHTCIRCULARCONE"
| "IFCRIGHTCIRCULARCYLINDER"
| "IFCSPHERE"
| "IFCCSGPRIMITIVE3D" => Self::CsgPrimitive,
"IFCHALFSPACESOLID" | "IFCBOXEDHALFSPACE" | "IFCPOLYGONALBOUNDEDHALFSPACE" => {
Self::HalfSpace
}
"IFCTRIANGULATEDFACESET" | "IFCPOLYGONALFACESET" => Self::TessellatedFaceSet,
"IFCCSGSOLID"
| "IFCFACETEDBREP"
| "IFCFACETEDBREPWITHVOIDS"
| "IFCADVANCEDBREP"
| "IFCADVANCEDBREPWITHVOIDS"
| "IFCEXTRUDEDAREASOLID"
| "IFCEXTRUDEDAREASOLIDTAPERED"
| "IFCREVOLVEDAREASOLID"
| "IFCREVOLVEDAREASOLIDTAPERED"
| "IFCSURFACECURVESWEPTAREASOLID"
| "IFCFIXEDREFERENCESWEPTAREASOLID"
| "IFCSWEPTDISKSOLID"
| "IFCSWEPTDISKSOLIDPOLYGONAL" => Self::SolidModel,
_ => return None,
};
Some(kind)
}
pub fn is_nested_boolean(self) -> bool {
matches!(self, Self::BooleanResult)
}
}
#[derive(Debug, Clone, Copy)]
pub struct BooleanResult<'m> {
slots: Slots<'m>,
}
impl<'m> BooleanResult<'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 type_name(&self) -> &'m str {
self.slots.type_name()
}
pub fn operator(&self) -> GeometryResult<IfcBooleanOperator> {
let token = self
.slots
.opt_enum(slot::OPERATOR)
.ok_or_else(|| self.missing("Operator"))?;
IfcBooleanOperator::parse(token).ok_or_else(|| {
self.slots
.degenerate(format!("unknown IfcBooleanOperator '.{token}.'"))
})
}
pub fn first_operand(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::FIRST_OPERAND, "FirstOperand")
}
pub fn second_operand(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::SECOND_OPERAND, "SecondOperand")
}
pub fn operands(&self) -> GeometryResult<(EntityId, EntityId)> {
Ok((self.first_operand()?, self.second_operand()?))
}
pub fn operand_kind(
&self,
model: &'m Model,
operand: EntityId,
) -> GeometryResult<Option<OperandKind>> {
let entity = self.slots.resolve(model, operand)?;
Ok(OperandKind::classify(&entity.type_name))
}
pub fn is_clipping(&self) -> bool {
self.type_name()
.eq_ignore_ascii_case("IFCBOOLEANCLIPPINGRESULT")
}
fn missing(&self, attribute: &'static str) -> crate::GeometryError {
crate::GeometryError::MissingAttribute {
entity: self.slots.id(),
type_name: self.slots.type_name().to_string(),
attribute,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BooleanClippingResult<'m> {
slots: Slots<'m>,
}
impl<'m> BooleanClippingResult<'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 base(&self) -> BooleanResult<'m> {
BooleanResult { slots: self.slots }
}
pub fn checked_operator(&self) -> GeometryResult<IfcBooleanOperator> {
let op = self.base().operator()?;
if op == IfcBooleanOperator::Difference {
Ok(op)
} else {
Err(self.slots.degenerate(format!(
"IfcBooleanClippingResult requires DIFFERENCE, found {}",
op.as_token()
)))
}
}
}
#[cfg(test)]
mod tests;