use axiolid_contracts::{
Backend, BackendDescriptor, BackendId, CancellationGranularity, ExecutionOptions,
ExecutionTarget, GeomError, GeomResult, Operation, ScratchRequirement, Sign,
};
use axiolid_core::BooleanOperator;
use axiolid_core::Point3;
use axiolid_mesh::TriMesh;
use axiolid_mesh_boolean_contract::{BooleanEvidence, BooleanOutcome, MeshBoolean};
use crate::orient3d;
#[derive(Debug, Default, Clone, Copy)]
pub struct ScalarBoolean;
impl ScalarBoolean {
pub const ID: BackendId = BackendId::new("scalar-reference");
#[must_use]
pub fn new() -> Self {
Self
}
}
impl Backend for ScalarBoolean {
fn descriptor(&self) -> BackendDescriptor {
BackendDescriptor::new(Self::ID, ExecutionTarget::PortableCpu)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Arrangement {
Disjoint,
SubjectInsideTool,
ToolInsideSubject,
Identical,
}
impl MeshBoolean for ScalarBoolean {
fn scratch_requirement(&self) -> ScratchRequirement {
ScratchRequirement::None
}
fn cancellation_granularity(&self) -> CancellationGranularity {
CancellationGranularity::Incremental
}
fn boolean(
&self,
subject: &TriMesh,
tool: &TriMesh,
operation: BooleanOperator,
options: &ExecutionOptions,
) -> GeomResult<BooleanOutcome> {
let arrangement = classify(subject, tool, options)?;
let mesh = match (operation, arrangement) {
(BooleanOperator::Union | BooleanOperator::Intersection, Arrangement::Identical) => {
subject.clone()
}
(
BooleanOperator::Difference | BooleanOperator::SymmetricDifference,
Arrangement::Identical,
) => empty(),
(
BooleanOperator::Union | BooleanOperator::SymmetricDifference,
Arrangement::Disjoint,
) => concatenate(subject, tool),
(BooleanOperator::Intersection, Arrangement::Disjoint) => empty(),
(BooleanOperator::Difference, Arrangement::Disjoint) => subject.clone(),
(BooleanOperator::Union, Arrangement::SubjectInsideTool) => tool.clone(),
(BooleanOperator::Intersection, Arrangement::SubjectInsideTool) => subject.clone(),
(BooleanOperator::Difference, Arrangement::SubjectInsideTool) => empty(),
(BooleanOperator::SymmetricDifference, Arrangement::SubjectInsideTool) => {
concatenate(tool, &reversed(subject))
}
(BooleanOperator::Union, Arrangement::ToolInsideSubject) => subject.clone(),
(BooleanOperator::Intersection, Arrangement::ToolInsideSubject) => tool.clone(),
(
BooleanOperator::Difference | BooleanOperator::SymmetricDifference,
Arrangement::ToolInsideSubject,
) => concatenate(subject, &reversed(tool)),
_ => {
return Err(GeomError::Unsupported {
backend: Self::ID,
operation: Operation::MeshBoolean,
})
}
};
let evidence = BooleanEvidence::record(
subject.triangle_count(),
tool.triangle_count(),
mesh.triangle_count(),
components(&mesh),
)
.with_disjoint_tools(usize::from(arrangement == Arrangement::Disjoint));
Ok(BooleanOutcome::new(mesh, evidence))
}
}
fn exact_sign(certified: axiolid_contracts::Certified) -> Sign {
certified.sign().unwrap_or(Sign::Zero)
}
fn empty() -> TriMesh {
TriMesh::new(Vec::new(), Vec::new())
}
fn concatenate(a: &TriMesh, b: &TriMesh) -> TriMesh {
let offset = a.positions.len() as u32;
let mut positions = a.positions.clone();
positions.extend_from_slice(&b.positions);
let mut indices = a.indices.clone();
indices.extend(b.indices.iter().map(|i| i + offset));
TriMesh::new(positions, indices)
}
fn reversed(mesh: &TriMesh) -> TriMesh {
let mut indices = mesh.indices.clone();
for triangle in indices.chunks_exact_mut(3) {
triangle.swap(0, 1);
}
TriMesh::new(mesh.positions.clone(), indices)
}
fn components(mesh: &TriMesh) -> usize {
if mesh.positions.is_empty() {
return 0;
}
let mut parent: Vec<usize> = (0..mesh.positions.len()).collect();
fn find(parent: &mut [usize], mut node: usize) -> usize {
while parent[node] != node {
parent[node] = parent[parent[node]];
node = parent[node];
}
node
}
for triangle in mesh.indices.chunks_exact(3) {
let root = find(&mut parent, triangle[0] as usize);
for corner in &triangle[1..] {
let other = find(&mut parent, *corner as usize);
if root != other {
parent[other] = root;
}
}
}
let mut roots = std::collections::BTreeSet::new();
for index in &mesh.indices {
let root = find(&mut parent, *index as usize);
roots.insert(root);
}
roots.len()
}
fn classify(
subject: &TriMesh,
tool: &TriMesh,
options: &ExecutionOptions,
) -> GeomResult<Arrangement> {
for (mesh, role) in [(subject, "subject"), (tool, "tool")] {
if mesh.positions.is_empty() || mesh.indices.is_empty() {
return Err(GeomError::InvalidInput(format!(
"{role}: an empty mesh has no interior and cannot be a boolean operand"
)));
}
}
if same_geometry(subject, tool) {
return Ok(Arrangement::Identical);
}
if surfaces_intersect(subject, tool, options)? {
return Err(GeomError::Unsupported {
backend: ScalarBoolean::ID,
operation: Operation::MeshBoolean,
});
}
let subject_in_tool = contains_point(tool, subject.positions[0]);
let tool_in_subject = contains_point(subject, tool.positions[0]);
Ok(match (subject_in_tool, tool_in_subject) {
(true, false) => Arrangement::SubjectInsideTool,
(false, true) => Arrangement::ToolInsideSubject,
(false, false) => Arrangement::Disjoint,
(true, true) => {
return Err(GeomError::Degenerate(
"operands report mutual containment, which is geometrically impossible".into(),
))
}
})
}
fn same_geometry(a: &TriMesh, b: &TriMesh) -> bool {
if a.positions.len() != b.positions.len() || a.indices.len() != b.indices.len() {
return false;
}
if a.positions
.iter()
.zip(&b.positions)
.any(|(p, q)| p.x != q.x || p.y != q.y || p.z != q.z)
{
return false;
}
let mut left: Vec<[u32; 3]> = a
.indices
.chunks_exact(3)
.map(|t| {
let mut v = [t[0], t[1], t[2]];
v.sort_unstable();
v
})
.collect();
let mut right: Vec<[u32; 3]> = b
.indices
.chunks_exact(3)
.map(|t| {
let mut v = [t[0], t[1], t[2]];
v.sort_unstable();
v
})
.collect();
left.sort_unstable();
right.sort_unstable();
left == right
}
fn surfaces_intersect(a: &TriMesh, b: &TriMesh, options: &ExecutionOptions) -> GeomResult<bool> {
for left in a.indices.chunks_exact(3) {
options.check_cancelled()?;
let triangle_a = [
a.positions[left[0] as usize],
a.positions[left[1] as usize],
a.positions[left[2] as usize],
];
for right in b.indices.chunks_exact(3) {
let triangle_b = [
b.positions[right[0] as usize],
b.positions[right[1] as usize],
b.positions[right[2] as usize],
];
if edges_cross_triangle(&triangle_a, &triangle_b)
|| edges_cross_triangle(&triangle_b, &triangle_a)
{
return Ok(true);
}
}
}
Ok(false)
}
fn edges_cross_triangle(edges: &[Point3; 3], face: &[Point3; 3]) -> bool {
let [p, q, r] = *face;
for (start, end) in [
(edges[0], edges[1]),
(edges[1], edges[2]),
(edges[2], edges[0]),
] {
let side_start = exact_sign(orient3d(p, q, r, start));
let side_end = exact_sign(orient3d(p, q, r, end));
if side_start == Sign::Zero || side_end == Sign::Zero || side_start == side_end {
continue;
}
let signs = [
exact_sign(orient3d(start, end, p, q)),
exact_sign(orient3d(start, end, q, r)),
exact_sign(orient3d(start, end, r, p)),
];
let positive = signs.contains(&Sign::Positive);
let negative = signs.contains(&Sign::Negative);
if !(positive && negative) {
return true;
}
}
false
}
fn contains_point(mesh: &TriMesh, point: Point3) -> bool {
const DIRECTIONS: [[f64; 3]; 4] = [
[1.0, 0.0, 0.0],
[1.0, 0.125, 0.0625],
[0.5, 1.0, 0.25],
[0.25, 0.5, 1.0],
];
for direction in DIRECTIONS {
if let Some(inside) = parity_along(mesh, point, direction) {
return inside;
}
}
false
}
fn parity_along(mesh: &TriMesh, origin: Point3, direction: [f64; 3]) -> Option<bool> {
let bounds = mesh.bounds();
let span = (bounds.max.x - bounds.min.x)
.max(bounds.max.y - bounds.min.y)
.max(bounds.max.z - bounds.min.z)
.max(1.0)
* 8.0;
let far = Point3::new(
origin.x + direction[0] * span,
origin.y + direction[1] * span,
origin.z + direction[2] * span,
);
let mut crossings = 0usize;
for triangle in mesh.indices.chunks_exact(3) {
let p = mesh.positions[triangle[0] as usize];
let q = mesh.positions[triangle[1] as usize];
let r = mesh.positions[triangle[2] as usize];
let side_origin = exact_sign(orient3d(p, q, r, origin));
let side_far = exact_sign(orient3d(p, q, r, far));
if side_origin == Sign::Zero {
return Some(false);
}
if side_far == Sign::Zero || side_origin == side_far {
continue;
}
let a = exact_sign(orient3d(origin, far, p, q));
let b = exact_sign(orient3d(origin, far, q, r));
let c = exact_sign(orient3d(origin, far, r, p));
if a == Sign::Zero || b == Sign::Zero || c == Sign::Zero {
return None;
}
if a == b && b == c {
crossings += 1;
}
}
Some(crossings % 2 == 1)
}