Skip to main content

ifc_geometry/lower/
boolean.rs

1//! Boolean and CSG lowering: exact operation trees, never evaluated here.
2//!
3//! # Why this family proves the session design
4//!
5//! A boolean is the first family whose operands are themselves lowered
6//! solids. Under the old one-graph-per-call shape its operands lived in two
7//! unrelated graphs whose `NodeId`s could not legally be combined. Here both
8//! operands append into the caller's session, so the operation node can refer
9//! to them directly.
10
11use axiolid_core::BooleanOperator;
12use axiolid_model::{GeometryNode, NodeId, SolidOperation};
13use ifc_model::EntityId;
14
15use crate::error::{GeometryError, GeometryResult};
16use crate::lower::session::LoweringSession;
17use crate::transform::Transform;
18
19pub(crate) mod slot {
20    pub const OPERATOR: usize = 0;
21    pub const FIRST_OPERAND: usize = 1;
22    pub const SECOND_OPERAND: usize = 2;
23}
24
25/// Family label used for boolean memoization.
26const BOOLEAN: &str = "boolean";
27
28/// Map an IFC operator enumeration to the neutral operator.
29///
30/// `IfcBooleanClippingResult` is constrained by its where-rule to DIFFERENCE,
31/// but the enumeration itself is shared with `IfcBooleanResult`, so the
32/// mapping lives here once.
33fn operator(entity: EntityId, token: &str) -> GeometryResult<BooleanOperator> {
34    match token.trim_matches('.').to_ascii_uppercase().as_str() {
35        "UNION" => Ok(BooleanOperator::Union),
36        "INTERSECTION" => Ok(BooleanOperator::Intersection),
37        "DIFFERENCE" => Ok(BooleanOperator::Difference),
38        _ => Err(GeometryError::Degenerate {
39            entity,
40            type_name: "IFCBOOLEANRESULT".to_string(),
41            detail: format!("unknown boolean operator {token}"),
42        }),
43    }
44}
45
46/// Append one `IfcBooleanResult` (or clipping result) and return its node.
47///
48/// Both operands are lowered into the same session before the operation node
49/// is appended, which is exactly the ordering the append-only builder needs:
50/// every reference is already a prior node.
51pub fn lower_boolean_result_node(
52    session: &mut LoweringSession<'_>,
53    id: EntityId,
54    frame: Transform,
55) -> GeometryResult<NodeId> {
56    if let Some(node) = session.memoized(id, BOOLEAN, frame) {
57        return Ok(node);
58    }
59
60    session.enter(id, "boolean")?;
61    let result = lower_operands(session, id, frame);
62    session.exit(id);
63    let node = result?;
64
65    session.memoize(id, BOOLEAN, frame, node);
66    Ok(node)
67}
68
69fn lower_operands(
70    session: &mut LoweringSession<'_>,
71    id: EntityId,
72    frame: Transform,
73) -> GeometryResult<NodeId> {
74    let slots = session.slots(id)?;
75    let operator_token = slots
76        .opt_enum(slot::OPERATOR)
77        .ok_or(GeometryError::MissingAttribute {
78            entity: id,
79            type_name: slots.type_name().to_string(),
80            attribute: "Operator",
81        })?
82        .to_string();
83    let operator = operator(id, &operator_token)?;
84    let first = slots.req_ref(slot::FIRST_OPERAND, "FirstOperand")?;
85    let second = slots.req_ref(slot::SECOND_OPERAND, "SecondOperand")?;
86
87    let left = session.lower_operand(first, frame)?;
88    let right = session.lower_operand(second, frame)?;
89
90    session.node_for(
91        id,
92        GeometryNode::SolidOperation(SolidOperation::Boolean {
93            left,
94            right,
95            operator,
96        }),
97    )
98}