use std::cell::RefCell;
use std::fmt;
thread_local! {
static PENDING_MAPPED_BOOL_FAILURES: RefCell<Vec<BoolFailure>> =
const { RefCell::new(Vec::new()) };
}
pub fn push_pending_mapped_bool_failures(failures: Vec<BoolFailure>) {
if failures.is_empty() {
return;
}
PENDING_MAPPED_BOOL_FAILURES.with(|cell| cell.borrow_mut().extend(failures));
}
pub fn take_pending_mapped_bool_failures() -> Vec<BoolFailure> {
PENDING_MAPPED_BOOL_FAILURES.with(|cell| std::mem::take(&mut *cell.borrow_mut()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoolOp {
Difference,
Union,
Intersection,
Unknown,
}
impl fmt::Display for BoolOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BoolOp::Difference => f.write_str("DIFFERENCE"),
BoolOp::Union => f.write_str("UNION"),
BoolOp::Intersection => f.write_str("INTERSECTION"),
BoolOp::Unknown => f.write_str("UNKNOWN"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BoolFailureReason {
OperandTooLarge {
polys_a: usize,
polys_b: usize,
},
EmptyOperand,
DegenerateOperand,
NoBoundsOverlap,
KernelOutputInvalid,
SolidSolidDifferenceSkipped,
PolygonalBoundedHalfSpaceFallback,
CutterUnionUnavailable,
UnknownBooleanOperator(String),
ManifoldOutputDegenerate {
host_tris: usize,
result_tris: usize,
},
KernelError(String),
DifferenceEmptiedHost,
}
impl BoolFailureReason {
pub fn label(&self) -> &'static str {
match self {
BoolFailureReason::OperandTooLarge { .. } => "OperandTooLarge",
BoolFailureReason::EmptyOperand => "EmptyOperand",
BoolFailureReason::DegenerateOperand => "DegenerateOperand",
BoolFailureReason::NoBoundsOverlap => "NoBoundsOverlap",
BoolFailureReason::KernelOutputInvalid => "KernelOutputInvalid",
BoolFailureReason::SolidSolidDifferenceSkipped => "SolidSolidDifferenceSkipped",
BoolFailureReason::PolygonalBoundedHalfSpaceFallback => {
"PolygonalBoundedHalfSpaceFallback"
}
BoolFailureReason::CutterUnionUnavailable => "CutterUnionUnavailable",
BoolFailureReason::UnknownBooleanOperator(_) => "UnknownBooleanOperator",
BoolFailureReason::ManifoldOutputDegenerate { .. } => "ManifoldOutputDegenerate",
BoolFailureReason::KernelError(_) => "KernelError",
BoolFailureReason::DifferenceEmptiedHost => "DifferenceEmptiedHost",
}
}
}
impl fmt::Display for BoolFailureReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BoolFailureReason::OperandTooLarge { polys_a, polys_b } => write!(
f,
"operand polygon counts ({polys_a}, {polys_b}) exceed BSP cap"
),
BoolFailureReason::EmptyOperand => f.write_str("operand mesh empty"),
BoolFailureReason::DegenerateOperand => f.write_str("operand polygons degenerate"),
BoolFailureReason::NoBoundsOverlap => f.write_str("operand bounds disjoint"),
BoolFailureReason::KernelOutputInvalid => {
f.write_str("CSG kernel output had non-finite vertices")
}
BoolFailureReason::SolidSolidDifferenceSkipped => {
f.write_str("solid-vs-solid IfcBooleanResult.DIFFERENCE skipped (BSP unsafe)")
}
BoolFailureReason::PolygonalBoundedHalfSpaceFallback => f.write_str(
"IfcPolygonalBoundedHalfSpace degraded to unbounded plane clip",
),
BoolFailureReason::CutterUnionUnavailable => f.write_str(
"cutter union not watertight; deferred to sequential per-cutter subtraction",
),
BoolFailureReason::UnknownBooleanOperator(op) => {
write!(f, "unknown IfcBooleanResult operator '{op}'")
}
BoolFailureReason::DifferenceEmptiedHost => f.write_str(
"DIFFERENCE removed the entire host; reverted to un-cut",
),
BoolFailureReason::ManifoldOutputDegenerate {
host_tris,
result_tris,
} => write!(
f,
"Manifold difference returned implausibly small result ({result_tris} triangles from {host_tris}-triangle host) — fell back to BSP"
),
BoolFailureReason::KernelError(msg) => write!(f, "kernel error: {msg}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoolFailure {
pub op: BoolOp,
pub reason: BoolFailureReason,
pub product_id: Option<u32>,
}
impl BoolFailure {
pub fn new(op: BoolOp, reason: BoolFailureReason) -> Self {
Self {
op,
reason,
product_id: None,
}
}
pub fn with_product_id(mut self, product_id: u32) -> Self {
self.product_id = Some(product_id);
self
}
}
impl fmt::Display for BoolFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.product_id {
Some(id) => write!(f, "[product #{id}] {} failed: {}", self.op, self.reason),
None => write!(f, "{} failed: {}", self.op, self.reason),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_includes_operands() {
let f = BoolFailure::new(
BoolOp::Difference,
BoolFailureReason::OperandTooLarge {
polys_a: 36,
polys_b: 12,
},
);
let rendered = f.to_string();
assert!(rendered.contains("DIFFERENCE"));
assert!(rendered.contains("36"));
assert!(rendered.contains("12"));
}
#[test]
fn with_product_id_attaches_id() {
let f = BoolFailure::new(BoolOp::Union, BoolFailureReason::EmptyOperand)
.with_product_id(12345);
assert_eq!(f.product_id, Some(12345));
assert!(f.to_string().contains("12345"));
}
#[test]
fn solid_solid_skip_renders_meaningfully() {
let f = BoolFailure::new(BoolOp::Difference, BoolFailureReason::SolidSolidDifferenceSkipped);
let rendered = f.to_string();
assert!(rendered.contains("solid-vs-solid"));
assert!(rendered.contains("DIFFERENCE"));
}
}