use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation};
use axiolid_core::{BooleanOperator, Point3};
use axiolid_mesh::TriMesh;
use crate::boolean::contains_point_exact;
use crate::intersection::{intersection_segments, IntersectionSegment, NodeKey};
use crate::retriangulate::retriangulate_face;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Side {
Inside,
Outside,
}
fn split_and_classify(
mesh: &TriMesh,
other: &TriMesh,
face_segments: &BTreeMap<u32, Vec<IntersectionSegment>>,
positions: &BTreeMap<NodeKey, Point3>,
) -> GeomResult<Vec<([Point3; 3], Side)>> {
let mut pieces = Vec::new();
for face_index in 0..mesh.triangle_count() {
let base = face_index * 3;
let corners: [Point3; 3] = [
mesh.positions[mesh.indices[base] as usize],
mesh.positions[mesh.indices[base + 1] as usize],
mesh.positions[mesh.indices[base + 2] as usize],
];
let key = u32::try_from(face_index).unwrap_or(u32::MAX);
let empty: Vec<IntersectionSegment> = Vec::new();
let segments = face_segments.get(&key).unwrap_or(&empty);
let patch = retriangulate_face(corners, segments, positions)?;
for triangle in &patch.triangles {
let [a, b, c] = triangle.map(|i| patch.points[i as usize]);
let centroid = Point3::new(
(a.x + b.x + c.x) / 3.0,
(a.y + b.y + c.y) / 3.0,
(a.z + b.z + c.z) / 3.0,
);
let probes = [
centroid,
lerp(centroid, a, 0.25),
lerp(centroid, b, 0.25),
lerp(centroid, c, 0.25),
];
let inside = probes
.into_iter()
.find_map(|probe| contains_point_exact(other, probe));
let side = match inside {
Some(true) => Side::Inside,
Some(false) => Side::Outside,
None => {
return Err(GeomError::Unsupported {
backend: BackendId::new("scalar-assemble"),
operation: Operation::MeshBoolean,
})
}
};
pieces.push(([a, b, c], side));
}
}
Ok(pieces)
}
pub fn exact_boolean(
subject: &TriMesh,
tool: &TriMesh,
operation: BooleanOperator,
) -> GeomResult<TriMesh> {
let curve = intersection_segments(subject, tool)?;
let subject_pieces = split_and_classify(
subject,
tool,
&curve.subject_face_segments,
&curve.positions,
)?;
let tool_pieces =
split_and_classify(tool, subject, &curve.tool_face_segments, &curve.positions)?;
let (keep_subject, keep_tool, flip_tool) = match operation {
BooleanOperator::Union => (Side::Outside, Side::Outside, false),
BooleanOperator::Intersection => (Side::Inside, Side::Inside, false),
BooleanOperator::Difference => (Side::Outside, Side::Inside, true),
_ => {
return Err(axiolid_contracts::GeomError::Unsupported {
backend: axiolid_contracts::BackendId::new("scalar-exact-boolean"),
operation: Operation::MeshBoolean,
})
}
};
let mut positions: Vec<Point3> = Vec::new();
let mut indices: Vec<u32> = Vec::new();
let mut welded: BTreeMap<[u64; 3], u32> = BTreeMap::new();
let mut push = |point: Point3, positions: &mut Vec<Point3>| -> u32 {
let bits = [point.x + 0.0, point.y + 0.0, point.z + 0.0].map(f64::to_bits);
*welded.entry(bits).or_insert_with(|| {
positions.push(point);
(positions.len() - 1) as u32
})
};
for (triangle, side) in &subject_pieces {
if *side != keep_subject {
continue;
}
for corner in triangle {
let index = push(*corner, &mut positions);
indices.push(index);
}
}
for (triangle, side) in &tool_pieces {
if *side != keep_tool {
continue;
}
let ordered = if flip_tool {
[triangle[0], triangle[2], triangle[1]]
} else {
*triangle
};
for corner in &ordered {
let index = push(*corner, &mut positions);
indices.push(index);
}
}
Ok(TriMesh::new(positions, indices))
}
fn lerp(from: Point3, to: Point3, t: f64) -> Point3 {
Point3::new(
from.x + (to.x - from.x) * t,
from.y + (to.y - from.y) * t,
from.z + (to.z - from.z) * t,
)
}