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, ArcPolygon, ArcRing, ArcVertex, FillRule,
OverlayInput, OverlayOperation, Polygon, Ring,
};
use axiolid_primitive::HalfSpace;
use crate::boolean_column::{clip_columns, coaxial_columns, is_stepped, ColumnOperand};
use crate::boolean_provenance::{name_side_fragment, OperandRings};
use axiolid_brep_audit::geometric_audit;
use crate::contour_lower::orient_arc_ring;
use crate::extrude_arc::{arc_geometry, extrude_arc_rings_between, extrude_arc_rings_from, Level};
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> {
if let Some(solids) = prism_columns(subject, tool, operator, tolerance)? {
return one_solid(
solids,
"prism boolean produced an empty result",
"exact prism boolean producing disconnected components \
(boolean_prisms_exact_solids returns every piece)",
);
}
let (bottom, top) = prism_span(subject, tool, operator, tolerance)?;
let polygons = prism_sections(subject, tool, operator, tolerance)?;
single_solid(
polygons,
"prism boolean produced an empty cross-section",
"exact prism boolean producing disconnected components \
(boolean_prisms_exact_solids returns every piece)",
|polygon| prism_solid(polygon, subject, tool, (bottom, top), tolerance),
)
}
pub fn boolean_prisms_exact_solids(
subject: &Prism,
tool: &Prism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<Vec<ExactBRep>> {
if let Some(solids) = prism_columns(subject, tool, operator, tolerance)? {
return Ok(solids);
}
let Some((bottom, top)) = empty_span_is_none(prism_span(subject, tool, operator, tolerance))?
else {
return Ok(Vec::new());
};
let polygons = prism_sections(subject, tool, operator, tolerance)?;
polygons
.iter()
.map(|polygon| prism_solid(polygon, subject, tool, (bottom, top), tolerance))
.collect()
}
fn prism_span(
subject: &Prism,
tool: &Prism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<(Scalar, Scalar)> {
validate(subject, "subject")?;
validate(tool, "tool")?;
resolve_span(
(subject.bottom, subject.top),
(tool.bottom, tool.top),
operator,
tolerance,
)
}
fn prism_sections(
subject: &Prism,
tool: &Prism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<Vec<Polygon>> {
let operation = overlay_operation(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:?}"),
})?;
Ok(result.polygons)
}
fn prism_solid(
polygon: &Polygon,
subject: &Prism,
tool: &Prism,
(bottom, top): (Scalar, Scalar),
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
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 single_solid<T>(
pieces: Vec<T>,
empty: &'static str,
disconnected: &'static str,
build: impl FnOnce(&T) -> GeomResult<ExactBRep>,
) -> GeomResult<ExactBRep> {
match pieces.as_slice() {
[] => Err(GeomError::Degenerate(empty.to_owned())),
[only] => build(only),
_ => Err(unsupported(disconnected)),
}
}
fn empty_span_is_none(span: GeomResult<(Scalar, Scalar)>) -> GeomResult<Option<(Scalar, Scalar)>> {
match span {
Ok(span) => Ok(Some(span)),
Err(GeomError::Degenerate(_)) => Ok(None),
Err(error) => Err(error),
}
}
fn lowest_first(a: &[Point2], b: &[Point2]) -> std::cmp::Ordering {
let key = |ring: &[Point2]| {
ring.iter()
.copied()
.min_by(|p, q| p.x.total_cmp(&q.x).then(p.y.total_cmp(&q.y)))
};
match (key(a), key(b)) {
(Some(p), Some(q)) => p.x.total_cmp(&q.x).then(p.y.total_cmp(&q.y)),
(a, b) => a.is_some().cmp(&b.is_some()),
}
}
fn overlay_operation(operator: BooleanOperator) -> GeomResult<OverlayOperation> {
match operator {
BooleanOperator::Intersection => Ok(OverlayOperation::Intersection),
BooleanOperator::Union => Ok(OverlayOperation::Union),
BooleanOperator::Difference => Ok(OverlayOperation::Difference),
_ => Err(unsupported("unknown exact prism boolean operator")),
}
}
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> {
if let Some(solids) = arc_prism_columns(subject, tool, operator, tolerance)? {
return one_solid(
solids,
"arc prism boolean produced an empty result",
"exact arc prism boolean producing disconnected components \
(boolean_arc_prisms_exact_solids returns every piece)",
);
}
let span = arc_prism_span(subject, tool, operator, tolerance)?;
let regions = arc_prism_sections(subject, tool, operator, tolerance)?;
single_solid(
regions,
"arc prism boolean produced an empty cross-section",
"exact arc prism boolean producing disconnected components \
(boolean_arc_prisms_exact_solids returns every piece)",
|region| arc_prism_solid(region, span, tolerance),
)
}
pub fn boolean_arc_prisms_exact_solids(
subject: &ArcPrism,
tool: &ArcPrism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<Vec<ExactBRep>> {
if let Some(solids) = arc_prism_columns(subject, tool, operator, tolerance)? {
return Ok(solids);
}
let Some(span) = empty_span_is_none(arc_prism_span(subject, tool, operator, tolerance))? else {
return Ok(Vec::new());
};
let mut regions = arc_prism_sections(subject, tool, operator, tolerance)?;
regions.sort_by(|a, b| lowest_first(&arc_points(&a.outer), &arc_points(&b.outer)));
regions
.iter()
.map(|region| arc_prism_solid(region, span, tolerance))
.collect()
}
pub fn clip_arc_prism_exact(
prism: &ArcPrism,
half_space: &HalfSpace,
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
validate_arc_ring(&prism.section, tolerance)
.map_err(|error| GeomError::InvalidInput(format!("arc prism section: {error:?}")))?;
if !(prism.bottom.is_finite() && prism.top.is_finite()) {
return Err(GeomError::InvalidInput(
"arc prism heights must be finite".to_owned(),
));
}
if prism.top <= prism.bottom {
return Err(GeomError::InvalidInput(
"arc prism top must lie above its bottom".to_owned(),
));
}
let origin = half_space.boundary.origin;
let normal = half_space.boundary.normal;
if !(origin.is_finite() && normal.is_finite()) || normal.length_squared() == 0.0 {
return Err(GeomError::InvalidInput(
"half-space boundary must have a finite point and a non-zero normal".to_owned(),
));
}
if normal.z == 0.0 {
return Err(unsupported(
"exact arc prism clip by a plane parallel to the extrusion axis",
));
}
let level = Level {
height: normal.dot(origin) / normal.z,
gradient: Vec2::new(-normal.x / normal.z, -normal.y / normal.z),
};
if !(level.height.is_finite() && level.gradient.is_finite()) {
return Err(GeomError::Degenerate(
"half-space boundary is too steep to express as a height".to_owned(),
));
}
let keeps_above = (normal.z > 0.0) == half_space.agreement;
let section = orient_arc_ring(&prism.section, true)?;
let (low, high) = level_range(§ion, level)?;
let linear = tolerance.linear();
let rings = [section];
let flat = |bottom, top| {
extrude_arc_rings_between(&rings, Level::flat(bottom), Level::flat(top), (true, true))
};
let solid = if keeps_above {
if high <= prism.bottom + linear {
flat(prism.bottom, prism.top)?
} else if low >= prism.top - linear {
return Err(GeomError::Degenerate(
"arc prism clip is empty: the plane lies above the prism".to_owned(),
));
} else if low > prism.bottom + linear && high < prism.top - linear {
extrude_arc_rings_between(&rings, level, Level::flat(prism.top), (false, true))?
} else {
return clip_crossing(&rings[0], prism, level, keeps_above, tolerance);
}
} else if low >= prism.top - linear {
flat(prism.bottom, prism.top)?
} else if high <= prism.bottom + linear {
return Err(GeomError::Degenerate(
"arc prism clip is empty: the plane lies below the prism".to_owned(),
));
} else if low > prism.bottom + linear && high < prism.top - linear {
extrude_arc_rings_between(&rings, Level::flat(prism.bottom), level, (true, false))?
} else {
return clip_crossing(&rings[0], prism, level, keeps_above, tolerance);
};
gate_geometry(solid, tolerance)
}
fn level_range(ring: &ArcRing, level: Level) -> GeomResult<(Scalar, Scalar)> {
let mut low = Scalar::INFINITY;
let mut high = Scalar::NEG_INFINITY;
let mut take = |p: Point2| {
let z = level.at(p);
low = low.min(z);
high = high.max(z);
};
let count = ring.vertices.len();
for index in 0..count {
let from = ring.vertices[index];
let to = ring.vertices[(index + 1) % count];
take(from.point);
if from.bulge == 0.0 || level.gradient == Vec2::ZERO {
continue;
}
let arc = arc_geometry(from.point, to.point, from.bulge)?;
let start = (from.point - arc.centre).to_angle();
let direction = level.gradient.to_angle();
for extreme in [direction, direction + core::f64::consts::PI] {
let turned = if arc.sweep > 0.0 {
(extreme - start).rem_euclid(core::f64::consts::TAU)
} else {
(start - extreme).rem_euclid(core::f64::consts::TAU)
};
if turned <= arc.sweep.abs() {
take(arc.centre + Vec2::from_angle(extreme) * arc.radius);
}
}
}
Ok((low, high))
}
fn arc_points(ring: &ArcRing) -> Vec<Point2> {
ring.vertices.iter().map(|vertex| vertex.point).collect()
}
fn one_solid(
mut solids: Vec<ExactBRep>,
empty: &'static str,
disconnected: &'static str,
) -> GeomResult<ExactBRep> {
match solids.len() {
0 => Err(GeomError::Degenerate(empty.to_owned())),
1 => Ok(solids.remove(0)),
_ => Err(unsupported(disconnected)),
}
}
fn prism_columns(
subject: &Prism,
tool: &Prism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<Option<Vec<ExactBRep>>> {
validate(subject, "subject")?;
validate(tool, "tool")?;
if !is_stepped(
(subject.bottom, subject.top),
(tool.bottom, tool.top),
operator,
tolerance,
) {
return Ok(None);
}
let operand = |prism: &Prism| ColumnOperand {
rings: prism
.rings
.iter()
.map(|ring| ArcRing::new(ring.iter().copied().map(ArcVertex::straight).collect()))
.collect(),
bottom: prism.bottom,
top: prism.top,
};
coaxial_columns(&operand(subject), &operand(tool), operator, tolerance).map(Some)
}
fn arc_prism_columns(
subject: &ArcPrism,
tool: &ArcPrism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<Option<Vec<ExactBRep>>> {
validate_arc_prisms(subject, tool, tolerance)?;
if !is_stepped(
(subject.bottom, subject.top),
(tool.bottom, tool.top),
operator,
tolerance,
) {
return Ok(None);
}
let operand = |prism: &ArcPrism| ColumnOperand {
rings: vec![prism.section.clone()],
bottom: prism.bottom,
top: prism.top,
};
coaxial_columns(&operand(subject), &operand(tool), operator, tolerance).map(Some)
}
fn clip_crossing(
section: &ArcRing,
prism: &ArcPrism,
level: Level,
keeps_above: bool,
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
let solids = clip_columns(
section,
(prism.bottom, prism.top),
level,
keeps_above,
tolerance,
)?;
one_solid(
solids,
"arc prism clip is empty",
"exact arc prism clip leaving disconnected pieces",
)
}
fn validate_arc_prisms(
subject: &ArcPrism,
tool: &ArcPrism,
tolerance: Tolerance,
) -> GeomResult<()> {
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(),
));
}
Ok(())
}
fn arc_prism_span(
subject: &ArcPrism,
tool: &ArcPrism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<(Scalar, Scalar)> {
validate_arc_prisms(subject, tool, tolerance)?;
resolve_span(
(subject.bottom, subject.top),
(tool.bottom, tool.top),
operator,
tolerance,
)
}
fn arc_prism_sections(
subject: &ArcPrism,
tool: &ArcPrism,
operator: BooleanOperator,
tolerance: Tolerance,
) -> GeomResult<Vec<ArcPolygon>> {
let operation = overlay_operation(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:?}"),
}
})?;
Ok(result.regions)
}
fn arc_prism_solid(
region: &ArcPolygon,
(bottom, top): (Scalar, Scalar),
tolerance: Tolerance,
) -> GeomResult<ExactBRep> {
let mut rings = Vec::with_capacity(1 + region.holes.len());
rings.push(region.outer.clone());
rings.extend(region.holes.iter().cloned());
let solid = extrude_arc_rings_from(&rings, bottom, 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")),
}
}
pub(crate) 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,
})
}