use std::collections::{BTreeMap, BTreeSet};
use axiolid_contracts::{BackendId, GeomError, GeomResult, Operation, Sign};
use axiolid_core::Point3;
use axiolid_mesh::TriMesh;
use crate::boolean::ScalarBoolean;
use crate::{orient3d, triangle_triangle_relation, TriangleTriangleRelation};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Operand {
Subject,
Tool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EdgeKey {
operand: Operand,
low: u32,
high: u32,
}
impl EdgeKey {
pub fn new(operand: Operand, first: u32, second: u32) -> Self {
Self {
operand,
low: first.min(second),
high: first.max(second),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NodeKey {
Vertex {
operand: Operand,
index: u32,
},
EdgeSurface {
edge: EdgeKey,
at: PointKey,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PointKey {
bits: [u64; 3],
}
impl PointKey {
pub fn new(point: Point3) -> Self {
Self {
bits: [point.x + 0.0, point.y + 0.0, point.z + 0.0].map(f64::to_bits),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct IntersectionSegment {
pub start: NodeKey,
pub end: NodeKey,
}
impl IntersectionSegment {
pub fn between(start: NodeKey, end: NodeKey) -> GeomResult<Self> {
if start == end {
return Err(GeomError::Degenerate(
"an intersection segment collapsed to one source-topology node".into(),
));
}
Ok(Self {
start: start.min(end),
end: start.max(end),
})
}
}
fn plane_sign(triangle: [Point3; 3], point: Point3) -> Sign {
orient3d(triangle[0], triangle[1], triangle[2], point)
.sign()
.expect("certified predicates are total")
}
fn crossing_nodes(
face_vertices: [u32; 3],
face_operand: Operand,
face_points: [Point3; 3],
other: [Point3; 3],
) -> Vec<(NodeKey, Point3)> {
let signs = face_points.map(|point| plane_sign(other, point));
let mut nodes = Vec::new();
for corner in 0..3 {
let next = (corner + 1) % 3;
if signs[corner] == Sign::Zero {
nodes.push((
NodeKey::Vertex {
operand: face_operand,
index: face_vertices[corner],
},
face_points[corner],
));
continue;
}
if signs[next] != Sign::Zero && signs[corner] != signs[next] {
let crossing = plane_crossing(face_points[corner], face_points[next], other);
let key = NodeKey::EdgeSurface {
edge: EdgeKey::new(face_operand, face_vertices[corner], face_vertices[next]),
at: PointKey::new(crossing),
};
nodes.push((key, crossing));
}
}
nodes
}
fn plane_crossing(start: Point3, end: Point3, triangle: [Point3; 3]) -> Point3 {
let normal = (triangle[1] - triangle[0]).cross(triangle[2] - triangle[0]);
let start_height = (start - triangle[0]).dot(normal);
let end_height = (end - triangle[0]).dot(normal);
let span = start_height - end_height;
if span == 0.0 {
return start.midpoint(end);
}
start + (end - start) * (start_height / span)
}
fn point_in_triangle(point: Point3, triangle: [Point3; 3]) -> bool {
let normal = (triangle[1] - triangle[0]).cross(triangle[2] - triangle[0]);
let mut positive = false;
let mut negative = false;
for corner in 0..3 {
let next = (corner + 1) % 3;
let apex = triangle[corner] + normal;
match plane_sign([triangle[corner], triangle[next], apex], point) {
Sign::Positive => positive = true,
Sign::Negative => negative = true,
_ => {}
}
}
!(positive && negative)
}
#[derive(Debug, Clone, Default)]
pub struct IntersectionCurve {
pub segments: Vec<IntersectionSegment>,
pub positions: BTreeMap<NodeKey, Point3>,
pub subject_face_segments: BTreeMap<u32, Vec<IntersectionSegment>>,
pub tool_face_segments: BTreeMap<u32, Vec<IntersectionSegment>>,
}
pub fn intersection_segments(subject: &TriMesh, tool: &TriMesh) -> GeomResult<IntersectionCurve> {
let mut segments = BTreeSet::new();
let mut positions = BTreeMap::new();
let mut subject_face_segments: BTreeMap<u32, Vec<IntersectionSegment>> = BTreeMap::new();
let mut tool_face_segments: BTreeMap<u32, Vec<IntersectionSegment>> = BTreeMap::new();
for subject_face in 0..subject.triangle_count() {
let (subject_indices, subject_points) = face(subject, subject_face)?;
for tool_face in 0..tool.triangle_count() {
let (tool_indices, tool_points) = face(tool, tool_face)?;
match triangle_triangle_relation(subject_points, tool_points) {
TriangleTriangleRelation::Disjoint => continue,
TriangleTriangleRelation::Coplanar => {
if crate::coplanar::coplanar_overlap(subject_points, tool_points).is_empty() {
continue;
}
return Err(GeomError::Unsupported {
backend: BackendId::new("scalar-intersection"),
operation: Operation::MeshBoolean,
});
}
TriangleTriangleRelation::DegenerateTriangle => {
return Err(GeomError::Degenerate(
"a source face is degenerate; heal the mesh before intersecting".into(),
))
}
TriangleTriangleRelation::Proper | TriangleTriangleRelation::Touching => {}
}
let subject_normal = (subject_points[1] - subject_points[0])
.cross(subject_points[2] - subject_points[0]);
let tool_normal =
(tool_points[1] - tool_points[0]).cross(tool_points[2] - tool_points[0]);
let mut nodes = Vec::new();
for (key, point) in crossing_nodes(
subject_indices,
Operand::Subject,
subject_points,
tool_points,
) {
if point_in_triangle(point, tool_points) {
nodes.push((key, point));
}
}
for (key, point) in
crossing_nodes(tool_indices, Operand::Tool, tool_points, subject_points)
{
if point_in_triangle(point, subject_points) {
nodes.push((key, point));
}
}
nodes.sort_by(|left, right| {
point_bits(left.1)
.cmp(&point_bits(right.1))
.then_with(|| left.0.cmp(&right.0))
});
nodes.dedup_by(|left, right| point_bits(left.1) == point_bits(right.1));
nodes.sort_by(|left, right| left.0.cmp(&right.0));
nodes.dedup_by(|left, right| left.0 == right.0);
if nodes.len() < 2 {
for (key, point) in nodes {
positions.insert(key, point);
}
continue;
}
let axis = subject_normal.cross(tool_normal);
if axis.length_squared() == 0.0 {
for (key, point) in nodes {
positions.insert(key, point);
}
continue;
}
nodes.sort_by(|left, right| {
let left_t = left.1.dot(axis);
let right_t = right.1.dot(axis);
left_t
.partial_cmp(&right_t)
.expect("finite coordinates give an orderable projection")
});
let interval = if nodes.len() == 2 {
[nodes[0], nodes[1]]
} else {
[nodes[nodes.len() / 2 - 1], nodes[nodes.len() / 2]]
};
for (key, point) in nodes.iter().copied() {
positions.insert(key, point);
}
if interval[0].0 == interval[1].0 {
continue;
}
let segment = IntersectionSegment::between(interval[0].0, interval[1].0)?;
segments.insert(segment);
let subject_key = u32::try_from(subject_face).map_err(|_| face_count_error())?;
let tool_key = u32::try_from(tool_face).map_err(|_| face_count_error())?;
subject_face_segments
.entry(subject_key)
.or_default()
.push(segment);
tool_face_segments
.entry(tool_key)
.or_default()
.push(segment);
}
}
for list in subject_face_segments
.values_mut()
.chain(tool_face_segments.values_mut())
{
list.sort_unstable();
list.dedup();
}
Ok(IntersectionCurve {
segments: segments.into_iter().collect(),
positions,
subject_face_segments,
tool_face_segments,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Polyline {
pub nodes: Vec<NodeKey>,
pub closed: bool,
}
pub fn assemble_polylines(segments: &[IntersectionSegment]) -> GeomResult<Vec<Polyline>> {
let mut adjacency: BTreeMap<NodeKey, Vec<NodeKey>> = BTreeMap::new();
for segment in segments {
adjacency
.entry(segment.start)
.or_default()
.push(segment.end);
adjacency
.entry(segment.end)
.or_default()
.push(segment.start);
}
for (node, neighbours) in &mut adjacency {
neighbours.sort_unstable();
neighbours.dedup();
if neighbours.len() > 2 {
return Err(GeomError::NotManifold(format!(
"intersection curve branches at {node:?} with degree {}",
neighbours.len()
)));
}
if neighbours.len() < 2 {
return Err(GeomError::Unsupported {
backend: ScalarBoolean::ID,
operation: Operation::MeshBoolean,
});
}
}
let mut visited = BTreeSet::new();
let mut polylines = Vec::new();
let endpoints: Vec<NodeKey> = adjacency
.iter()
.filter(|(_, neighbours)| neighbours.len() == 1)
.map(|(node, _)| *node)
.collect();
for start in endpoints {
if visited.contains(&start) {
continue;
}
polylines.push(walk(start, &adjacency, &mut visited, false));
}
let cycle_starts: Vec<NodeKey> = adjacency.keys().copied().collect();
for start in cycle_starts {
if visited.contains(&start) {
continue;
}
polylines.push(walk(start, &adjacency, &mut visited, true));
}
Ok(polylines)
}
fn walk(
start: NodeKey,
adjacency: &BTreeMap<NodeKey, Vec<NodeKey>>,
visited: &mut BTreeSet<NodeKey>,
closed: bool,
) -> Polyline {
let mut nodes = vec![start];
visited.insert(start);
let mut current = start;
let mut previous = None;
loop {
let Some(neighbours) = adjacency.get(¤t) else {
break;
};
let next = neighbours
.iter()
.copied()
.find(|candidate| Some(*candidate) != previous && !visited.contains(candidate));
let Some(next) = next else {
break;
};
nodes.push(next);
visited.insert(next);
previous = Some(current);
current = next;
}
Polyline { nodes, closed }
}
fn face(mesh: &TriMesh, index: usize) -> GeomResult<([u32; 3], [Point3; 3])> {
let base = index * 3;
let indices: [u32; 3] = mesh
.indices
.get(base..base + 3)
.ok_or_else(|| GeomError::Degenerate("face index out of range".into()))?
.try_into()
.map_err(|_| GeomError::Degenerate("face index slice is not three wide".into()))?;
let mut points = [Point3::ZERO; 3];
for (slot, vertex) in indices.iter().enumerate() {
points[slot] = *mesh
.positions
.get(*vertex as usize)
.ok_or_else(|| GeomError::Degenerate("face references a missing vertex".into()))?;
}
Ok((indices, points))
}
fn face_count_error() -> GeomError {
GeomError::Degenerate("mesh has more faces than a u32 index can name".into())
}
fn point_bits(point: Point3) -> [u64; 3] {
[point.x + 0.0, point.y + 0.0, point.z + 0.0].map(f64::to_bits)
}