use axiolid_brep::{ExactBRep, FaceName, Operand, SweptFace};
use axiolid_contracts::{GeomError, GeomResult, Operation};
use axiolid_core::{BooleanOperator, Frame2, Point2, Scalar, Tolerance, Vec2, Vec3};
use axiolid_overlay::{
arc_overlay, overlay, validate_arc_ring, ArcRing, FillRule, OverlayInput, OverlayOperation,
Polygon, Ring,
};
use crate::boolean_provenance::{name_side_fragment, OperandRings};
use axiolid_brep_audit::geometric_audit;
use crate::extrude_arc::extrude_arc_ring;
use crate::extrude_exact::extrude_polygon_rings_named;
use crate::BACKEND_ID;
pub fn unsupported(input: &'static str) -> GeomError {
GeomError::UnsupportedInput {
backend: BACKEND_ID,
operation: Operation::MeshBoolean,
input,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Prism {
pub rings: Vec<Vec<Point2>>,
pub bottom: Scalar,
pub top: Scalar,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ArcPrism {
pub section: ArcRing,
pub bottom: Scalar,
pub top: Scalar,
}
pub fn boolean_prisms_exact(
subject: &Prism,
tool: &Prism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
validate(subject, "subject")?;
validate(tool, "tool")?;
let (bottom, top) = resolve_span(
(subject.bottom, subject.top),
(tool.bottom, tool.top),
operator,
tolerance,
)?;
let operation = match operator {
BooleanOperator::Intersection => OverlayOperation::Intersection,
BooleanOperator::Union => OverlayOperation::Union,
BooleanOperator::Difference => OverlayOperation::Difference,
_ => return Err(unsupported("unknown exact prism boolean operator")),
};
let frame = Frame2 {
origin: Vec2::ZERO,
x: Vec2::X,
y: Vec2::Y,
};
let result = overlay(
&OverlayInput {
frame,
polygons: to_polygons(subject),
},
&OverlayInput {
frame,
polygons: to_polygons(tool),
},
operation,
FillRule::NonZero,
tolerance,
)
.map_err(|error| GeomError::BackendContractViolation {
backend: BACKEND_ID,
detail: format!("exact prism cross-section overlay failed: {error:?}"),
})?;
if result.polygons.is_empty() {
return Err(GeomError::Degenerate(
"prism boolean produced an empty cross-section".to_owned(),
));
}
if result.polygons.len() > 1 {
return Err(unsupported(
"exact prism boolean producing disconnected components",
));
}
let polygon = &result.polygons[0];
let mut rings = Vec::with_capacity(1 + polygon.holes.len());
rings.push(polygon.outer.points.clone());
for hole in &polygon.holes {
rings.push(hole.points.clone());
}
if bottom.abs() > tolerance.linear() {
return Err(unsupported(
"exact prism boolean whose result does not start at z = 0",
));
}
let subject_rings = OperandRings {
operand: Operand::Subject,
rings: &subject.rings,
};
let tool_rings = OperandRings {
operand: Operand::Tool,
rings: &tool.rings,
};
let operands = [subject_rings, tool_rings];
let mut solid =
extrude_polygon_rings_named(&rings, Vec3::Z * (top - bottom), &mut |(start, end)| {
name_side_fragment(start, end, &operands)
})?;
name_caps(&mut solid, subject, tool, bottom, top, tolerance);
gate_geometry(solid, tolerance)
}
fn validate(prism: &Prism, role: &'static str) -> GeomResult<()> {
if prism.rings.is_empty() {
return Err(GeomError::InvalidInput(format!(
"{role} prism has no cross-section rings"
)));
}
for ring in &prism.rings {
if ring.len() < 3 {
return Err(GeomError::InvalidInput(format!(
"{role} prism ring needs at least three points"
)));
}
if !ring.iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
return Err(GeomError::InvalidInput(format!(
"{role} prism ring has a non-finite point"
)));
}
}
if !prism.bottom.is_finite() || !prism.top.is_finite() {
return Err(GeomError::InvalidInput(format!(
"{role} prism heights must be finite"
)));
}
if prism.top <= prism.bottom {
return Err(GeomError::InvalidInput(format!(
"{role} prism top must lie above its bottom"
)));
}
Ok(())
}
fn to_polygons(prism: &Prism) -> Vec<Polygon> {
let mut rings = prism.rings.iter();
let outer = Ring {
points: rings.next().cloned().unwrap_or_default(),
};
let holes = rings.map(|r| Ring { points: r.clone() }).collect();
vec![Polygon { outer, holes }]
}
fn name_caps(
solid: &mut ExactBRep,
subject: &Prism,
tool: &Prism,
bottom: Scalar,
top: Scalar,
tolerance: Tolerance,
) {
let start = cap_operand(subject.bottom, tool.bottom, bottom, tolerance);
let end = cap_operand(subject.top, tool.top, top, tolerance);
solid.name_caps(
start.map(|operand| FaceName::swept(SweptFace::StartCap).fragment(operand)),
end.map(|operand| FaceName::swept(SweptFace::EndCap).fragment(operand)),
);
}
fn cap_operand(
subject: Scalar,
tool: Scalar,
result: Scalar,
tolerance: Tolerance,
) -> Option<Operand> {
if tolerance.eq(subject, result) {
Some(Operand::Subject)
} else if tolerance.eq(tool, result) {
Some(Operand::Tool)
} else {
None
}
}
pub fn boolean_arc_prisms_exact(
subject: &ArcPrism,
tool: &ArcPrism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
for (section, role) in [(&subject.section, "subject"), (&tool.section, "tool")] {
validate_arc_ring(section, tolerance).map_err(|error| {
GeomError::InvalidInput(format!("{role} arc prism section: {error:?}"))
})?;
}
if !subject.bottom.is_finite()
|| !subject.top.is_finite()
|| !tool.bottom.is_finite()
|| !tool.top.is_finite()
{
return Err(GeomError::InvalidInput(
"arc prism heights must be finite".to_owned(),
));
}
if subject.top <= subject.bottom || tool.top <= tool.bottom {
return Err(GeomError::InvalidInput(
"arc prism top must lie above its bottom".to_owned(),
));
}
let (bottom, top) = resolve_span(
(subject.bottom, subject.top),
(tool.bottom, tool.top),
operator,
tolerance,
)?;
let operation = match operator {
BooleanOperator::Intersection => OverlayOperation::Intersection,
BooleanOperator::Union => OverlayOperation::Union,
BooleanOperator::Difference => OverlayOperation::Difference,
_ => return Err(unsupported("unknown exact prism boolean operator")),
};
let result =
arc_overlay(&subject.section, &tool.section, operation, tolerance).map_err(|error| {
GeomError::BackendContractViolation {
backend: BACKEND_ID,
detail: format!("arc prism cross-section overlay failed: {error:?}"),
}
})?;
if result.regions.is_empty() {
return Err(GeomError::Degenerate(
"arc prism boolean produced an empty cross-section".to_owned(),
));
}
if result.regions.len() > 1 {
return Err(unsupported(
"exact arc prism boolean producing disconnected components",
));
}
let region = &result.regions[0];
if !region.holes.is_empty() {
return Err(unsupported(
"exact arc prism boolean whose result has an interior hole",
));
}
if bottom.abs() > tolerance.linear() {
return Err(unsupported(
"exact arc prism boolean whose result does not start at z = 0",
));
}
let solid = extrude_arc_ring(®ion.outer, Vec3::Z * (top - bottom))?;
gate_geometry(solid, tolerance)
}
fn resolve_span(
subject: (Scalar, Scalar),
tool: (Scalar, Scalar),
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<(Scalar, Scalar)> {
match operator {
BooleanOperator::Intersection => {
let bottom = subject.0.max(tool.0);
let top = subject.1.min(tool.1);
if top - bottom <= tolerance.linear() {
return Err(GeomError::Degenerate(
"prism intersection is empty along the extrusion axis".to_owned(),
));
}
Ok((bottom, top))
}
BooleanOperator::Union => {
if !tolerance.eq(subject.0, tool.0) || !tolerance.eq(subject.1, tool.1) {
return Err(unsupported(
"exact prism union with differing extrusion spans",
));
}
Ok(subject)
}
BooleanOperator::Difference => {
if tool.0 > subject.0 + tolerance.linear() || tool.1 < subject.1 - tolerance.linear() {
return Err(unsupported(
"exact prism difference with a tool shorter than the subject",
));
}
Ok(subject)
}
_ => Err(unsupported("unknown exact prism boolean operator")),
}
}
fn gate_geometry(solid: ExactBRep, tolerance: Tolerance) -> GeomResult<ExactBRep> {
let health = geometric_audit(&solid, tolerance);
if health.is_consistent() {
return Ok(solid);
}
let detail = match health.worst_error() {
Some(error) => format!(
"boolean result failed its geometric audit: {} defect(s), worst deviation {error:e}",
health.defects().len()
),
None => format!(
"boolean result failed its geometric audit: {} defect(s)",
health.defects().len()
),
};
Err(GeomError::BackendContractViolation {
backend: BACKEND_ID,
detail,
})
}