use axiolid_core::{BooleanOperator, Transform3};
use axiolid_model::{GeometryNode, Instance, NodeId, SolidOperation};
use ifc_model::EntityId;
use crate::error::{GeometryError, GeometryResult};
use crate::input::openings::voidings_of;
use crate::lower::session::NodeShape;
use crate::lower::{lower_product_representation, LoweringSession, RepresentationPurpose};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Subtraction {
pub opening: EntityId,
pub relation: EntityId,
pub body: NodeId,
pub result: NodeId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct NetLowering {
pub gross: NodeId,
pub subtractions: Vec<Subtraction>,
pub root: NodeId,
}
impl NetLowering {
pub fn openings(&self) -> Vec<EntityId> {
self.subtractions.iter().map(|s| s.opening).collect()
}
}
const MAX_PART_DEPTH: usize = 64;
pub fn lower_product_net(
session: &mut LoweringSession<'_>,
product: EntityId,
) -> GeometryResult<Option<NetLowering>> {
let Some(gross) = lower_product_representation(session, product, RepresentationPurpose::Body)?
else {
return Ok(None);
};
let voidings = voidings_of(session.model(), product);
if voidings.is_empty() {
return Ok(Some(NetLowering {
gross,
subtractions: Vec::new(),
root: gross,
}));
}
let mut heads = solid_parts(session, gross, product)?;
let mut subtractions = Vec::with_capacity(voidings.len());
for voiding in voidings {
let refuse = |cause: GeometryError| GeometryError::OpeningNotSubtracted {
host: product,
opening: voiding.opening,
cause: Box::new(cause),
};
if voiding.opening == product {
return Err(refuse(degenerate(
session,
voiding.relation,
"IfcRelVoidsElement names the same element as host and opening",
)));
}
let body = match lower_product_representation(
session,
voiding.opening,
RepresentationPurpose::Body,
) {
Ok(Some(body)) => body,
Ok(None) => {
return Err(refuse(degenerate(
session,
voiding.opening,
"the opening has no Body representation, so there is nothing to subtract",
)))
}
Err(error) => return Err(refuse(error)),
};
let tools = solid_parts(session, body, voiding.opening).map_err(refuse)?;
for head in &mut heads {
for &tool in &tools {
*head = session
.node_for(
voiding.relation,
GeometryNode::SolidOperation(SolidOperation::Boolean {
left: *head,
right: tool,
operator: BooleanOperator::Difference,
}),
)
.map_err(refuse)?;
}
}
let result = match heads.as_slice() {
[single] => *single,
_ => session
.node_for(voiding.relation, GeometryNode::Collection(heads.clone()))
.map_err(refuse)?,
};
subtractions.push(Subtraction {
opening: voiding.opening,
relation: voiding.relation,
body,
result,
});
}
let root = subtractions.last().map_or(gross, |step| step.result);
Ok(Some(NetLowering {
gross,
subtractions,
root,
}))
}
fn solid_parts(
session: &mut LoweringSession<'_>,
root: NodeId,
owner: EntityId,
) -> GeometryResult<Vec<NodeId>> {
let mut parts = Vec::new();
collect_parts(session, root, None, owner, 0, &mut parts)?;
if parts.is_empty() {
return Err(degenerate(
session,
owner,
"the Body holds no solid, so there is nothing to subtract with or from",
));
}
Ok(parts)
}
fn collect_parts(
session: &mut LoweringSession<'_>,
node: NodeId,
outer: Option<Transform3>,
owner: EntityId,
depth: usize,
parts: &mut Vec<NodeId>,
) -> GeometryResult<()> {
if depth > MAX_PART_DEPTH {
return Err(GeometryError::ChainTooDeep {
entity: owner,
kind: "body part",
limit: MAX_PART_DEPTH,
});
}
let shape = session.shape(node).cloned();
match shape {
Some(NodeShape::Solid) => parts.push(place(session, node, outer, owner)?),
Some(NodeShape::Instance { source, transform }) => {
if ends_in_solid(session, source) {
parts.push(place(session, node, outer, owner)?);
} else {
let composed = outer.map_or(transform, |outer| outer * transform);
collect_parts(session, source, Some(composed), owner, depth + 1, parts)?;
}
}
Some(NodeShape::Collection(members)) => {
for member in members {
collect_parts(session, member, outer, owner, depth + 1, parts)?;
}
}
Some(NodeShape::Other) | None => {
return Err(degenerate(
session,
owner,
"the Body holds geometry that bounds no volume, so it cannot be \
used in a boolean subtraction",
));
}
}
Ok(())
}
fn ends_in_solid(session: &LoweringSession<'_>, mut node: NodeId) -> bool {
for _ in 0..=MAX_PART_DEPTH {
match session.shape(node) {
Some(NodeShape::Solid) => return true,
Some(NodeShape::Instance { source, .. }) => node = *source,
_ => return false,
}
}
false
}
fn place(
session: &mut LoweringSession<'_>,
node: NodeId,
outer: Option<Transform3>,
owner: EntityId,
) -> GeometryResult<NodeId> {
match outer {
None => Ok(node),
Some(transform) => session.node_for(
owner,
GeometryNode::Instance(Instance {
source: node,
transform,
}),
),
}
}
fn degenerate(session: &LoweringSession<'_>, entity: EntityId, detail: &str) -> GeometryError {
let type_name = session
.model()
.get(entity)
.map_or_else(String::new, |e| e.type_name.to_string());
GeometryError::Degenerate {
entity,
type_name,
detail: detail.to_string(),
}
}