#![allow(missing_docs)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
use std::collections::{HashMap, HashSet};
use glam::{DMat3, DVec3, DVec4};
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::incremental_hull::IncrementalHull;
const EPSILON: f64 = 1e-7;
const ESTIMATED_NEW_VERTICES: usize = 8;
const EDGE_WALK_VERTEX_THRESHOLD: usize = 20;
#[inline]
const fn dvec4_xyz(v: &DVec4) -> DVec3 {
DVec3::new(v.x, v.y, v.z)
}
#[derive(Clone)]
struct SpatialHash {
cells: FxHashMap<(i64, i64, i64), Vec<DVec3>>,
cell_size: f64,
tolerance: f64,
}
impl SpatialHash {
fn new(tolerance: f64) -> Self {
Self {
cells: FxHashMap::default(),
cell_size: tolerance * 2.0,
tolerance,
}
}
fn with_capacity(tolerance: f64, capacity: usize) -> Self {
Self {
cells: FxHashMap::with_capacity_and_hasher(capacity, Default::default()),
cell_size: tolerance * 2.0,
tolerance,
}
}
fn reset(&mut self, tolerance: f64) {
self.cell_size = tolerance * 2.0;
self.tolerance = tolerance;
self.cells.clear();
}
#[inline]
fn cell_coords(&self, p: DVec3) -> (i64, i64, i64) {
#[expect(clippy::cast_possible_truncation)]
let discretize = |v: f64| (v / self.cell_size).floor() as i64;
(discretize(p.x), discretize(p.y), discretize(p.z))
}
fn is_duplicate(&self, point: DVec3) -> bool {
let (cx, cy, cz) = self.cell_coords(point);
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
if let Some(pts) = self.cells.get(&(cx + dx, cy + dy, cz + dz))
&& pts.iter().any(|&p| (p - point).length() < self.tolerance)
{
return true;
}
}
}
}
false
}
#[inline]
fn insert(&mut self, point: DVec3) {
self.cells
.entry(self.cell_coords(point))
.or_default()
.push(point);
}
#[inline]
fn insert_if_unique(&mut self, point: DVec3) -> bool {
if self.is_duplicate(point) {
false
} else {
self.insert(point);
true
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PlaneIdx(pub usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VertexIdx(pub usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct EdgeIdx(pub usize);
#[derive(Clone, Debug)]
pub struct HalfSpace {
pub normal: DVec3,
pub offset: f64,
}
impl HalfSpace {
#[must_use]
pub fn new(normal: DVec3, offset: f64) -> Self {
let len = normal.length();
assert!(len > EPSILON, "Normal vector must be non-zero");
Self {
normal: normal / len,
offset: offset / len,
}
}
#[must_use]
pub fn new_normalized(normal: DVec3, offset: f64) -> Self {
debug_assert!((normal.length() - 1.0).abs() < EPSILON * 100.0);
Self { normal, offset }
}
#[must_use]
pub fn try_new(normal: DVec3, offset: f64) -> Option<Self> {
let len = normal.length();
(len >= EPSILON).then(|| Self {
normal: normal / len,
offset: offset / len,
})
}
#[must_use]
pub fn classify(&self, point: DVec3, epsilon: f64) -> Classification {
let d = self.signed_distance(point);
if d < -epsilon {
Classification::Inside
} else if d > epsilon {
Classification::Outside
} else {
Classification::On
}
}
#[must_use]
pub fn signed_distance(&self, point: DVec3) -> f64 {
self.normal.dot(point) - self.offset
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Classification {
Inside,
On,
Outside,
}
#[derive(Clone, Debug)]
pub struct Vertex {
pub position: DVec4,
pub planes: Vec<PlaneIdx>,
pub edges: Vec<EdgeIdx>,
}
impl Vertex {
#[inline]
#[must_use]
pub fn is_finite(&self) -> bool {
self.position.w.abs() > EPSILON
}
#[must_use]
pub fn to_euclidean(&self) -> Option<DVec3> {
if self.is_finite() {
Some(dvec4_xyz(&self.position) / self.position.w)
} else {
None
}
}
#[must_use]
pub fn direction(&self) -> Option<DVec3> {
if self.is_finite() {
None
} else {
Some(dvec4_xyz(&self.position).normalize())
}
}
}
#[derive(Clone, Debug)]
pub struct Edge {
pub planes: (PlaneIdx, PlaneIdx),
pub vertices: (VertexIdx, VertexIdx),
}
impl Edge {
#[must_use]
pub const fn canonical_planes(p1: PlaneIdx, p2: PlaneIdx) -> (PlaneIdx, PlaneIdx) {
if p1.0 < p2.0 { (p1, p2) } else { (p2, p1) }
}
#[must_use]
pub fn is_bounded(&self, polytope: &IncrementalPolytope) -> bool {
let v0 = polytope.vertex(self.vertices.0);
let v1 = polytope.vertex(self.vertices.1);
matches!((v0, v1), (Some(v0), Some(v1)) if v0.is_finite() && v1.is_finite())
}
#[must_use]
pub fn is_ray(&self, polytope: &IncrementalPolytope) -> bool {
let v0 = polytope.vertex(self.vertices.0);
let v1 = polytope.vertex(self.vertices.1);
match (v0, v1) {
(Some(v0), Some(v1)) => v0.is_finite() != v1.is_finite(),
_ => false,
}
}
#[must_use]
pub fn is_line(&self, polytope: &IncrementalPolytope) -> bool {
let v0 = polytope.vertex(self.vertices.0);
let v1 = polytope.vertex(self.vertices.1);
matches!((v0, v1), (Some(v0), Some(v1)) if !v0.is_finite() && !v1.is_finite())
}
#[must_use]
pub fn direction(&self, polytope: &IncrementalPolytope) -> Option<DVec3> {
let v0 = polytope.vertex(self.vertices.0)?;
let v1 = polytope.vertex(self.vertices.1)?;
match (v0.is_finite(), v1.is_finite()) {
(true, true) => {
let p0 = v0.to_euclidean()?;
let p1 = v1.to_euclidean()?;
Some((p1 - p0).normalize())
}
(true, false) => {
v1.direction()
}
(false, true) => {
Some(-v0.direction()?)
}
(false, false) => {
let p0 = polytope.plane(self.planes.0)?;
let p1 = polytope.plane(self.planes.1)?;
let dir = p0.normal.cross(p1.normal);
if dir.length_squared() < EPSILON * EPSILON {
None
} else {
Some(dir.normalize())
}
}
}
}
#[inline]
#[must_use]
pub fn other_vertex(&self, v: VertexIdx) -> Option<VertexIdx> {
if self.vertices.0 == v {
Some(self.vertices.1)
} else if self.vertices.1 == v {
Some(self.vertices.0)
} else {
None
}
}
#[inline]
#[must_use]
pub fn is_on_plane(&self, plane_idx: PlaneIdx) -> bool {
self.planes.0 == plane_idx || self.planes.1 == plane_idx
}
}
#[derive(Clone, Debug)]
pub enum AddPlaneResult {
StillUnbounded,
Redundant,
Added {
new_vertices: Vec<VertexIdx>,
removed_vertices: Vec<VertexIdx>,
plane_idx: PlaneIdx,
},
Empty,
}
#[derive(Clone, Debug)]
pub struct RemovePlaneResult {
pub removed_vertices: Vec<VertexIdx>,
pub affected_vertices: Vec<VertexIdx>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TopologyError {
EulerMismatch {
vertices: usize,
edges: usize,
faces: usize,
expected: i32,
actual: i32,
},
UnderconstrainedVertex {
vertex: VertexIdx,
plane_count: usize,
},
InvalidIdealVertex {
vertex: VertexIdx,
plane_count: usize,
},
DanglingEdge {
edge: EdgeIdx,
missing_vertex: VertexIdx,
},
DegenerateFace {
plane: PlaneIdx,
vertex_count: usize,
},
DuplicateEdge { planes: (PlaneIdx, PlaneIdx) },
}
impl std::fmt::Display for TopologyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EulerMismatch {
vertices,
edges,
faces,
expected,
actual,
} => {
write!(
f,
"Euler mismatch: V={vertices}, E={edges}, F={faces}, χ={actual} (expected {expected})"
)
}
Self::UnderconstrainedVertex {
vertex,
plane_count,
} => {
write!(
f,
"Vertex {vertex:?} has only {plane_count} incident planes (need ≥3)"
)
}
Self::InvalidIdealVertex {
vertex,
plane_count,
} => {
write!(
f,
"Ideal vertex {vertex:?} has only {plane_count} incident planes (need ≥2)"
)
}
Self::DanglingEdge {
edge,
missing_vertex,
} => {
write!(
f,
"Edge {edge:?} references non-existent vertex {missing_vertex:?}"
)
}
Self::DegenerateFace {
plane,
vertex_count,
} => {
write!(
f,
"Face {plane:?} has only {vertex_count} vertices (need ≥3)"
)
}
Self::DuplicateEdge { planes } => {
write!(f, "Duplicate edge for planes {planes:?}")
}
}
}
}
impl std::error::Error for TopologyError {}
#[derive(Clone)]
#[expect(clippy::struct_excessive_bools)]
pub struct IncrementalPolytope {
planes: Vec<Option<HalfSpace>>,
vertices: Vec<Option<Vertex>>,
edges: Vec<Option<Edge>>,
edge_map: FxHashMap<(PlaneIdx, PlaneIdx), EdgeIdx>,
is_bounded: bool,
epsilon: f64,
vertex_free_list: Vec<VertexIdx>,
edge_free_list: Vec<EdgeIdx>,
plane_free_list: Vec<PlaneIdx>,
active_planes: Vec<PlaneIdx>,
edges_dirty: bool,
dirty_vertices: FxHashSet<VertexIdx>, faces_dirty: bool,
dirty_faces: FxHashSet<PlaneIdx>,
face_orderings: FxHashMap<PlaneIdx, Vec<VertexIdx>>,
normal_hull: IncrementalHull,
hull_dirty: bool,
spatial_hash: SpatialHash,
}
impl IncrementalPolytope {
#[must_use]
pub fn new() -> Self {
Self::with_epsilon(EPSILON)
}
#[must_use]
pub fn with_epsilon(epsilon: f64) -> Self {
Self {
planes: Vec::new(),
vertices: Vec::new(),
edges: Vec::new(),
edge_map: FxHashMap::default(),
is_bounded: false,
epsilon,
vertex_free_list: Vec::new(),
edge_free_list: Vec::new(),
plane_free_list: Vec::new(),
active_planes: Vec::new(),
edges_dirty: false,
dirty_vertices: FxHashSet::default(),
face_orderings: FxHashMap::default(),
dirty_faces: FxHashSet::default(),
faces_dirty: false,
normal_hull: IncrementalHull::new(epsilon),
hull_dirty: true,
spatial_hash: SpatialHash::with_capacity(epsilon * 10.0, 32),
}
}
pub fn clear(&mut self) {
*self = Self::with_epsilon(self.epsilon);
}
#[inline]
#[must_use]
pub const fn plane_count(&self) -> usize {
self.active_planes.len()
}
#[inline]
#[must_use]
pub fn vertex_count(&self) -> usize {
self.vertices.iter().flatten().count()
}
pub fn edge_count(&mut self) -> usize {
self.ensure_edges_valid();
self.edges.iter().flatten().count()
}
#[must_use]
pub fn is_bounded(&mut self) -> bool {
if self.is_bounded {
return true;
}
self.is_bounded = self.check_bounded();
self.is_bounded
}
pub fn validate_topology(&mut self) -> Result<(), TopologyError> {
self.ensure_edges_valid();
for (v_idx, vertex) in self.vertices() {
let plane_count = vertex.planes.len();
if vertex.is_finite() {
if plane_count < 3 {
return Err(TopologyError::UnderconstrainedVertex {
vertex: v_idx,
plane_count,
});
}
} else if plane_count < 2 {
return Err(TopologyError::InvalidIdealVertex {
vertex: v_idx,
plane_count,
});
}
}
for (e_idx, edge) in self.edges_internal() {
if self.vertex(edge.vertices.0).is_none() {
return Err(TopologyError::DanglingEdge {
edge: e_idx,
missing_vertex: edge.vertices.0,
});
}
if self.vertex(edge.vertices.1).is_none() {
return Err(TopologyError::DanglingEdge {
edge: e_idx,
missing_vertex: edge.vertices.1,
});
}
}
let (_, faces) = self.to_mesh();
for (face_idx, face) in faces.iter().enumerate() {
if face.len() < 3 {
let plane_idx = self
.active_planes
.get(face_idx)
.copied()
.unwrap_or(PlaneIdx(face_idx));
return Err(TopologyError::DegenerateFace {
plane: plane_idx,
vertex_count: face.len(),
});
}
}
let v = self.vertex_count();
let e = self.edge_count();
let f = faces.len();
let euler = v as i32 - e as i32 + f as i32;
let expected = if self.is_bounded() { 2 } else { 1 };
if euler != expected {
return Err(TopologyError::EulerMismatch {
vertices: v,
edges: e,
faces: f,
expected,
actual: euler,
});
}
Ok(())
}
#[must_use]
pub fn plane(&self, idx: PlaneIdx) -> Option<&HalfSpace> {
self.planes.get(idx.0)?.as_ref()
}
#[must_use]
pub fn vertex(&self, idx: VertexIdx) -> Option<&Vertex> {
self.vertices.get(idx.0)?.as_ref()
}
pub fn edge(&mut self, idx: EdgeIdx) -> Option<&Edge> {
self.ensure_edges_valid();
self.edges.get(idx.0)?.as_ref()
}
pub fn planes(&self) -> impl Iterator<Item = (PlaneIdx, &HalfSpace)> {
self.planes
.iter()
.enumerate()
.filter_map(|(i, p)| Some((PlaneIdx(i), p.as_ref()?)))
}
pub fn vertices(&self) -> impl Iterator<Item = (VertexIdx, &Vertex)> {
self.vertices
.iter()
.enumerate()
.filter_map(|(i, v)| Some((VertexIdx(i), v.as_ref()?)))
}
pub fn edges(&mut self) -> impl Iterator<Item = (EdgeIdx, &Edge)> {
self.ensure_edges_valid();
self.edges
.iter()
.enumerate()
.filter_map(|(i, e)| Some((EdgeIdx(i), e.as_ref()?)))
}
pub fn edge_by_planes(&mut self, p1: PlaneIdx, p2: PlaneIdx) -> Option<EdgeIdx> {
self.ensure_edges_valid();
self.edge_map.get(&Edge::canonical_planes(p1, p2)).copied()
}
fn edges_internal(&self) -> impl Iterator<Item = (EdgeIdx, &Edge)> {
self.edges
.iter()
.enumerate()
.filter_map(|(i, e)| Some((EdgeIdx(i), e.as_ref()?)))
}
fn edge_internal(&self, idx: EdgeIdx) -> Option<&Edge> {
self.edges.get(idx.0)?.as_ref()
}
fn edge_by_planes_internal(&self, p1: PlaneIdx, p2: PlaneIdx) -> Option<EdgeIdx> {
self.edge_map.get(&Edge::canonical_planes(p1, p2)).copied()
}
pub fn add_plane(&mut self, half_space: HalfSpace) -> AddPlaneResult {
let plane_idx = self.alloc_plane(half_space);
if self.vertex_count() == 0 {
if self.plane_count() < 3 {
return AddPlaneResult::StillUnbounded;
}
if self.plane_count() == 3 {
return match self.try_form_corner_from_three_planes() {
Some((new_verts, _)) => AddPlaneResult::Added {
new_vertices: new_verts,
removed_vertices: vec![],
plane_idx,
},
None => AddPlaneResult::StillUnbounded,
};
}
return match self.try_form_initial_vertices() {
Some((new_verts, _)) => {
self.edges_dirty = true;
self.faces_dirty = true;
AddPlaneResult::Added {
new_vertices: new_verts,
removed_vertices: vec![],
plane_idx,
}
}
None => AddPlaneResult::StillUnbounded,
};
}
let classifications = self.classify_vertices(plane_idx);
let all_outside = classifications
.iter()
.all(|(_, c)| *c == Classification::Outside);
let all_inside_or_on = classifications
.iter()
.all(|(_, c)| matches!(c, Classification::Inside | Classification::On));
if all_outside {
self.free_plane(plane_idx);
self.clear();
return AddPlaneResult::Empty;
}
if all_inside_or_on {
let new_vertices = self.try_form_vertices_with_plane(plane_idx);
if new_vertices.is_empty() {
self.free_plane(plane_idx);
return AddPlaneResult::Redundant;
}
self.mark_vertices_and_faces_dirty(&new_vertices);
return AddPlaneResult::Added {
new_vertices,
removed_vertices: vec![],
plane_idx,
};
}
self.ensure_edges_valid();
let removed_vertex_faces: Vec<PlaneIdx> = classifications
.iter()
.filter(|(_, c)| *c == Classification::Outside)
.filter_map(|(v_idx, _)| self.vertex(*v_idx))
.flat_map(|v| v.planes.clone())
.collect();
let (new_vertices, removed_vertices) = self.clip_by_plane(plane_idx, &classifications);
if self.vertex_count() == 0 {
self.hull_dirty = true;
}
self.mark_faces_dirty_for_vertices(&new_vertices);
self.mark_face_dirty(plane_idx);
for p_idx in removed_vertex_faces {
self.mark_face_dirty(p_idx);
}
AddPlaneResult::Added {
new_vertices,
removed_vertices,
plane_idx,
}
}
fn mark_vertices_and_faces_dirty(&mut self, vertices: &[VertexIdx]) {
for &v_idx in vertices {
self.mark_vertex_dirty(v_idx);
}
let faces: Vec<PlaneIdx> = vertices
.iter()
.filter_map(|&v| self.vertex(v))
.flat_map(|v| v.planes.clone())
.collect();
for p in faces {
self.mark_face_dirty(p);
}
}
fn mark_faces_dirty_for_vertices(&mut self, vertices: &[VertexIdx]) {
let faces: Vec<PlaneIdx> = vertices
.iter()
.filter_map(|&v| self.vertex(v))
.flat_map(|v| v.planes.clone())
.collect();
for p in faces {
self.mark_face_dirty(p);
}
}
fn try_form_vertices_with_plane(&mut self, plane_idx: PlaneIdx) -> Vec<VertexIdx> {
let mut new_vertices = Vec::new();
let mut spatial_hash = SpatialHash::new(self.epsilon * 10.0);
for (_, v) in self.vertices() {
if let Some(pos) = v.to_euclidean() {
spatial_hash.insert(pos);
}
}
let other_planes: Vec<usize> = (0..self.planes.len())
.filter(|&i| i != plane_idx.0 && self.planes[i].is_some())
.collect();
for (&i, &j) in other_planes.iter().tuple_combinations() {
let (Some(p1), Some(p2), Some(p3)) =
(&self.planes[i], &self.planes[j], &self.planes[plane_idx.0])
else {
continue;
};
let Some(point_hom) = intersect_three_planes(p1, p2, p3) else {
continue;
};
let point = dvec4_xyz(&point_hom);
if !self.point_satisfies_all_planes(point) {
continue;
}
if !spatial_hash.insert_if_unique(point) {
continue;
}
let incident = self.find_incident_planes(point, &[i, j, plane_idx.0]);
new_vertices.push(self.alloc_vertex(Vertex {
position: point_hom,
planes: incident,
edges: vec![],
}));
}
if !new_vertices.is_empty() {
self.edges_dirty = true;
}
new_vertices
}
fn find_incident_planes(&self, point: DVec3, known: &[usize]) -> Vec<PlaneIdx> {
let mut incident: Vec<usize> = known.to_vec();
for &plane_idx in &self.active_planes {
if !known.contains(&plane_idx.0)
&& let Some(plane) = &self.planes[plane_idx.0]
&& plane.signed_distance(point).abs() < self.epsilon
{
incident.push(plane_idx.0);
}
}
incident.sort_unstable();
incident.into_iter().map(PlaneIdx).collect()
}
fn classify_vertices(&self, plane_idx: PlaneIdx) -> Vec<(VertexIdx, Classification)> {
let plane = self.planes.get(plane_idx.0).and_then(|p| p.as_ref());
plane.map_or(vec![], |p| {
self.vertices()
.map(|(idx, v)| {
let classification = v.to_euclidean().map_or_else(
|| {
let dir = v.direction().unwrap();
let dot = p.normal.dot(dir);
if dot > self.epsilon {
Classification::Outside
} else if dot < -self.epsilon {
Classification::Inside
} else {
Classification::On
}
},
|pos| p.classify(pos, self.epsilon),
);
(idx, classification)
})
.collect()
})
}
fn check_bounded(&mut self) -> bool {
if self.plane_count() < 4 || self.vertex_count() < 4 {
return false;
}
let all_finite = self.vertices().all(|(_, v)| v.is_finite());
if !all_finite {
return false;
}
if self.hull_dirty {
self.rebuild_normal_hull();
}
self.normal_hull.origin_inside()
}
fn rebuild_normal_hull(&mut self) {
let normals = self
.planes
.iter()
.enumerate()
.filter_map(|(i, p)| p.as_ref().map(|plane| (i as u32, plane.normal)));
self.normal_hull = IncrementalHull::build(normals, self.epsilon);
self.hull_dirty = false;
}
fn try_form_initial_vertices(&mut self) -> Option<(Vec<VertexIdx>, Vec<EdgeIdx>)> {
if self.plane_count() < 4 {
return None;
}
let valid_planes: Vec<usize> = (0..self.planes.len())
.filter(|&i| self.planes[i].is_some())
.collect();
let _ = self.find_initial_simplex(&valid_planes);
self.try_form_initial_vertices_exhaustive(&valid_planes)
}
fn find_initial_simplex(&self, valid_indices: &[usize]) -> Option<[usize; 4]> {
if valid_indices.len() < 4 {
return None;
}
let normals: Vec<(usize, DVec3)> = valid_indices
.iter()
.filter_map(|&i| self.planes[i].as_ref().map(|p| (i, p.normal)))
.collect();
if normals.len() < 4 {
return None;
}
let mut selected = vec![normals[0]];
while selected.len() < 4 {
let best = normals
.iter()
.filter(|(i, _)| !selected.iter().any(|(j, _)| i == j))
.max_by(|(_, n1), (_, n2)| {
let score1 = 1.0
- selected
.iter()
.map(|(_, s)| n1.dot(*s).abs())
.fold(f64::NEG_INFINITY, f64::max);
let score2 = 1.0
- selected
.iter()
.map(|(_, s)| n2.dot(*s).abs())
.fold(f64::NEG_INFINITY, f64::max);
score1.partial_cmp(&score2).unwrap()
});
selected.push(*best?);
}
let det = DMat3::from_cols(selected[0].1, selected[1].1, selected[2].1).determinant();
(det.abs() >= self.epsilon)
.then(|| [selected[0].0, selected[1].0, selected[2].0, selected[3].0])
}
fn try_form_initial_vertices_exhaustive(
&mut self,
valid_planes: &[usize],
) -> Option<(Vec<VertexIdx>, Vec<EdgeIdx>)> {
let mut new_vertices = Vec::new();
let mut seen: HashMap<Vec<usize>, VertexIdx> = HashMap::new();
for (&i, &j, &k) in valid_planes.iter().tuple_combinations() {
let (Some(p1), Some(p2), Some(p3)) =
(&self.planes[i], &self.planes[j], &self.planes[k])
else {
continue;
};
let Some(point_hom) = intersect_three_planes(p1, p2, p3) else {
continue;
};
let point = dvec4_xyz(&point_hom);
if !self.point_satisfies_all_planes(point) {
continue;
}
let mut incident: Vec<usize> = vec![i, j, k];
for &m in valid_planes {
if ![i, j, k].contains(&m)
&& self.planes[m]
.as_ref()
.is_some_and(|p| p.signed_distance(point).abs() < self.epsilon)
{
incident.push(m);
}
}
incident.sort_unstable();
if seen.contains_key(&incident) {
continue;
}
let planes = incident.iter().map(|&p| PlaneIdx(p)).collect();
let v_idx = self.alloc_vertex(Vertex {
position: point_hom,
planes,
edges: vec![],
});
new_vertices.push(v_idx);
seen.insert(incident, v_idx);
}
if new_vertices.is_empty() {
return None;
}
self.edges_dirty = true;
Some((new_vertices, vec![]))
}
#[allow(dead_code)]
fn try_form_line_from_two_planes(&mut self) -> Option<(Vec<VertexIdx>, Vec<EdgeIdx>)> {
let valid_planes: Vec<usize> = (0..self.planes.len())
.filter(|&i| self.planes[i].is_some())
.collect();
if valid_planes.len() != 2 {
return None;
}
let (p0_idx, p1_idx) = (valid_planes[0], valid_planes[1]);
let (Some(p0), Some(p1)) = (&self.planes[p0_idx], &self.planes[p1_idx]) else {
return None;
};
let dir_hom = intersect_two_planes_direction(p0, p1)?;
let dir = dvec4_xyz(&dir_hom).normalize();
let v0_idx = self.alloc_vertex(Vertex {
position: DVec4::new(dir.x, dir.y, dir.z, 0.0),
planes: vec![PlaneIdx(p0_idx), PlaneIdx(p1_idx)],
edges: vec![],
});
let v1_idx = self.alloc_vertex(Vertex {
position: DVec4::new(-dir.x, -dir.y, -dir.z, 0.0),
planes: vec![PlaneIdx(p0_idx), PlaneIdx(p1_idx)],
edges: vec![],
});
let edge_planes = Edge::canonical_planes(PlaneIdx(p0_idx), PlaneIdx(p1_idx));
let e_idx = self.alloc_edge(Edge {
planes: edge_planes,
vertices: (v0_idx, v1_idx),
});
if let Some(v0) = self.vertex_mut(v0_idx) {
v0.edges.push(e_idx);
}
if let Some(v1) = self.vertex_mut(v1_idx) {
v1.edges.push(e_idx);
}
self.edges_dirty = true;
self.faces_dirty = true;
Some((vec![v0_idx, v1_idx], vec![e_idx]))
}
fn try_form_corner_from_three_planes(&mut self) -> Option<(Vec<VertexIdx>, Vec<EdgeIdx>)> {
let valid_planes: Vec<usize> = (0..self.planes.len())
.filter(|&i| self.planes[i].is_some())
.collect();
if valid_planes.len() != 3 {
return None;
}
let (p0_idx, p1_idx, p2_idx) = (valid_planes[0], valid_planes[1], valid_planes[2]);
let p0 = self.planes[p0_idx].clone()?;
let p1 = self.planes[p1_idx].clone()?;
let p2 = self.planes[p2_idx].clone()?;
let corner_hom = intersect_three_planes(&p0, &p1, &p2)?;
let corner_pos = dvec4_xyz(&corner_hom);
let plane_data = [
(p0_idx, p1_idx, p2_idx, &p0, &p1, &p2),
(p1_idx, p2_idx, p0_idx, &p1, &p2, &p0),
(p0_idx, p2_idx, p1_idx, &p0, &p2, &p1),
];
let epsilon = self.epsilon;
let mut ray_directions: Vec<(usize, usize, DVec3)> = Vec::new();
for (pi_idx, pj_idx, _, pi, pj, pk) in plane_data {
let Some(dir_hom) = intersect_two_planes_direction(pi, pj) else {
continue;
};
let dir = dvec4_xyz(&dir_hom).normalize();
let test_point_pos = corner_pos + dir * epsilon * 100.0;
let test_point_neg = corner_pos - dir * epsilon * 100.0;
let dist_pos = pk.signed_distance(test_point_pos);
let dist_neg = pk.signed_distance(test_point_neg);
let outward_dir = if dist_pos < dist_neg { dir } else { -dir };
ray_directions.push((pi_idx, pj_idx, outward_dir));
}
let corner_idx = self.alloc_vertex(Vertex {
position: corner_hom,
planes: vec![PlaneIdx(p0_idx), PlaneIdx(p1_idx), PlaneIdx(p2_idx)],
edges: vec![],
});
let mut new_vertices = vec![corner_idx];
let mut new_edges = Vec::new();
for (pi_idx, pj_idx, outward_dir) in ray_directions {
let ideal_idx = self.alloc_vertex(Vertex {
position: DVec4::new(outward_dir.x, outward_dir.y, outward_dir.z, 0.0),
planes: vec![PlaneIdx(pi_idx), PlaneIdx(pj_idx)],
edges: vec![],
});
new_vertices.push(ideal_idx);
let edge_planes = Edge::canonical_planes(PlaneIdx(pi_idx), PlaneIdx(pj_idx));
let e_idx = self.alloc_edge(Edge {
planes: edge_planes,
vertices: (corner_idx, ideal_idx),
});
new_edges.push(e_idx);
if let Some(v) = self.vertex_mut(corner_idx) {
v.edges.push(e_idx);
}
if let Some(v) = self.vertex_mut(ideal_idx) {
v.edges.push(e_idx);
}
}
self.edges_dirty = true;
self.faces_dirty = true;
Some((new_vertices, new_edges))
}
fn build_edges_from_vertices(&mut self, vertices: &[VertexIdx]) -> Vec<EdgeIdx> {
let mut new_edges = Vec::new();
for i in 0..vertices.len() {
for j in (i + 1)..vertices.len() {
let (v1_idx, v2_idx) = (vertices[i], vertices[j]);
let (Some(v1), Some(v2)) = (self.vertex(v1_idx), self.vertex(v2_idx)) else {
continue;
};
let shared: Vec<_> = v1
.planes
.iter()
.filter(|p| v2.planes.contains(p))
.copied()
.collect();
if shared.len() < 2 {
continue;
}
let edge_planes = Edge::canonical_planes(shared[0], shared[1]);
if self
.edge_by_planes_internal(edge_planes.0, edge_planes.1)
.is_some()
{
continue;
}
let e_idx = self.alloc_edge(Edge {
planes: edge_planes,
vertices: (v1_idx, v2_idx),
});
new_edges.push(e_idx);
if let Some(v) = self.vertex_mut(v1_idx) {
v.edges.push(e_idx);
}
if let Some(v) = self.vertex_mut(v2_idx) {
v.edges.push(e_idx);
}
}
}
new_edges
}
#[expect(clippy::too_many_lines)]
fn clip_by_plane(
&mut self,
plane_idx: PlaneIdx,
classifications: &[(VertexIdx, Classification)],
) -> (Vec<VertexIdx>, Vec<VertexIdx>) {
let class_map: HashMap<VertexIdx, Classification> =
classifications.iter().copied().collect();
let class_of = |v: VertexIdx| -> Classification {
class_map.get(&v).copied().unwrap_or(Classification::Inside)
};
let mut on_vertices = Vec::new();
let mut inside_or_on_vertices = Vec::new();
let mut removed_vertices = Vec::new();
for &(v_idx, class) in classifications {
match class {
Classification::On => {
on_vertices.push(v_idx);
inside_or_on_vertices.push(v_idx);
}
Classification::Inside => inside_or_on_vertices.push(v_idx),
Classification::Outside => removed_vertices.push(v_idx),
}
}
for &v_idx in &on_vertices {
if let Some(v) = self.vertex_mut(v_idx)
&& !v.planes.contains(&plane_idx)
{
v.planes.push(plane_idx);
v.planes.sort();
}
}
let mut new_vertices = Vec::new();
let mut edges_to_remove: Vec<(EdgeIdx, VertexIdx, VertexIdx)> = Vec::new();
let mut new_vertex_on_edge: HashMap<EdgeIdx, VertexIdx> = HashMap::new();
let edge_indices: Vec<EdgeIdx> = self.edges_internal().map(|(i, _)| i).collect();
for e_idx in edge_indices {
let Some(edge) = self.edge_internal(e_idx) else {
continue;
};
let (ev0, ev1) = edge.vertices;
let edge_planes = edge.planes;
let (c1, c2) = (class_of(ev0), class_of(ev1));
match (c1, c2) {
(
Classification::Inside | Classification::On,
Classification::Inside | Classification::On,
) => {}
(Classification::Outside, Classification::Outside) => {
edges_to_remove.push((e_idx, ev0, ev1));
}
_ => {
let v0 = self.vertex(ev0).unwrap();
let v1 = self.vertex(ev1).unwrap();
let plane = self.planes[plane_idx.0].as_ref().unwrap();
let new_vertex_pos = if v0.is_finite() && v1.is_finite() {
let p1 = v0.to_euclidean().unwrap();
let p2 = v1.to_euclidean().unwrap();
compute_edge_plane_intersection(p1, p2, plane, self.epsilon)
.map(|p| DVec4::new(p.x, p.y, p.z, 1.0))
} else if v0.is_finite() {
let p = v0.to_euclidean().unwrap();
let d = v1.direction().unwrap();
let result = compute_ray_plane_intersection(p, d, plane, self.epsilon);
result.map(|p| DVec4::new(p.x, p.y, p.z, 1.0))
} else if v1.is_finite() {
let p = v1.to_euclidean().unwrap();
let d = v0.direction().unwrap();
let result = compute_ray_plane_intersection(p, d, plane, self.epsilon);
result.map(|p| DVec4::new(p.x, p.y, p.z, 1.0))
} else {
let p0 = self.planes[edge_planes.0.0].as_ref().unwrap();
let p1 = self.planes[edge_planes.1.0].as_ref().unwrap();
intersect_three_planes(p0, p1, plane)
};
if let Some(intersection) = new_vertex_pos {
let mut planes = vec![edge_planes.0, edge_planes.1, plane_idx];
planes.sort();
let new_v = self.alloc_vertex(Vertex {
position: intersection,
planes,
edges: vec![],
});
new_vertices.push(new_v);
new_vertex_on_edge.insert(e_idx, new_v);
}
edges_to_remove.push((e_idx, ev0, ev1));
}
}
}
for (e_idx, v1, v2) in edges_to_remove {
if let Some(v) = self.vertex_mut(v1) {
v.edges.retain(|&e| e != e_idx);
}
if let Some(v) = self.vertex_mut(v2) {
v.edges.retain(|&e| e != e_idx);
}
self.free_edge(e_idx);
}
for &new_v_idx in new_vertex_on_edge.values() {
let Some(new_v) = self.vertex(new_v_idx) else {
continue;
};
let new_v_planes = new_v.planes.clone();
for &v_idx in &inside_or_on_vertices {
if v_idx == new_v_idx {
continue;
}
let Some(v) = self.vertex(v_idx) else {
continue;
};
let mut shared = [PlaneIdx(0), PlaneIdx(0)];
let mut shared_count = 0;
for &p in &new_v_planes {
if v.planes.contains(&p) {
if shared_count < 2 {
shared[shared_count] = p;
}
shared_count += 1;
if shared_count >= 2 {
break;
}
}
}
if shared_count >= 2 {
let canonical = Edge::canonical_planes(shared[0], shared[1]);
if self
.edge_by_planes_internal(canonical.0, canonical.1)
.is_none()
{
let e_idx = self.alloc_edge(Edge {
planes: canonical,
vertices: (v_idx, new_v_idx),
});
if let Some(v) = self.vertex_mut(v_idx) {
v.edges.push(e_idx);
}
if let Some(v) = self.vertex_mut(new_v_idx) {
v.edges.push(e_idx);
}
}
}
}
}
for &v_idx in &removed_vertices {
self.free_vertex(v_idx);
}
let mut additional_vertices = Vec::new();
{
let mut spatial_hash = SpatialHash::new(self.epsilon * 10.0);
for (_, v) in self.vertices() {
if let Some(pos) = v.to_euclidean() {
spatial_hash.insert(pos);
}
}
let other_planes: Vec<PlaneIdx> = self
.active_planes
.iter()
.filter(|&&p| p != plane_idx)
.copied()
.collect();
for i in 0..other_planes.len() {
for j in (i + 1)..other_planes.len() {
let p1_idx = other_planes[i];
let p2_idx = other_planes[j];
let (Some(p1), Some(p2), Some(p3)) = (
&self.planes[p1_idx.0],
&self.planes[p2_idx.0],
&self.planes[plane_idx.0],
) else {
continue;
};
let Some(point_hom) = intersect_three_planes(p1, p2, p3) else {
continue;
};
let point = dvec4_xyz(&point_hom);
if !self.point_satisfies_all_planes(point) {
continue;
}
if !spatial_hash.insert_if_unique(point) {
continue;
}
let incident =
self.find_incident_planes(point, &[p1_idx.0, p2_idx.0, plane_idx.0]);
let new_v = self.alloc_vertex(Vertex {
position: point_hom,
planes: incident,
edges: vec![],
});
additional_vertices.push(new_v);
new_vertices.push(new_v);
}
}
}
if !additional_vertices.is_empty() {
self.edges_dirty = true;
}
let mut face_vertices = new_vertices.clone();
face_vertices.extend(&on_vertices);
if face_vertices.len() >= 2 {
self.build_face_edges(plane_idx, &face_vertices);
}
(new_vertices, removed_vertices)
}
#[expect(
clippy::cast_precision_loss,
reason = "vertex count is small enough that f64 mantissa is sufficient"
)]
fn build_face_edges(&mut self, plane_idx: PlaneIdx, face_vertices: &[VertexIdx]) {
if face_vertices.len() < 2 {
return;
}
let plane_normal = match &self.planes[plane_idx.0] {
Some(plane) => plane.normal,
None => return,
};
let mut finite_pairs: Vec<(VertexIdx, DVec3)> = Vec::new();
let mut ideal_pairs: Vec<(VertexIdx, DVec3)> = Vec::new();
for &v_idx in face_vertices {
if let Some(v) = self.vertex(v_idx) {
if let Some(pos) = v.to_euclidean() {
finite_pairs.push((v_idx, pos));
} else if let Some(dir) = v.direction() {
ideal_pairs.push((v_idx, dir));
}
}
}
let total_vertices = finite_pairs.len() + ideal_pairs.len();
if total_vertices < 2 {
return;
}
if finite_pairs.is_empty() && ideal_pairs.len() >= 2 {
self.build_ideal_edges(plane_idx, &ideal_pairs);
return;
}
if !ideal_pairs.is_empty() && !finite_pairs.is_empty() {
self.build_ray_edges(plane_idx, &finite_pairs, &ideal_pairs);
}
if finite_pairs.len() < 2 {
return;
}
let centroid: DVec3 =
finite_pairs.iter().map(|(_, pos)| *pos).sum::<DVec3>() / finite_pairs.len() as f64;
let (u_axis, v_axis) = create_plane_basis(&plane_normal);
let mut vertex_angles: Vec<(VertexIdx, f64)> = finite_pairs
.iter()
.map(|&(v_idx, pos)| {
let local = pos - centroid;
let u = local.dot(u_axis);
let v = local.dot(v_axis);
let angle = v.atan2(u);
(v_idx, angle)
})
.collect();
vertex_angles.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
for i in 0..vertex_angles.len() {
let v1_idx = vertex_angles[i].0;
let v2_idx = vertex_angles[(i + 1) % vertex_angles.len()].0;
if v1_idx == v2_idx {
continue;
}
let v1 = self.vertex(v1_idx).unwrap();
let v2 = self.vertex(v2_idx).unwrap();
let other_shared: Option<PlaneIdx> = v1
.planes
.iter()
.filter(|&&p| p != plane_idx)
.find(|&p| v2.planes.contains(p))
.copied();
if let Some(other_plane) = other_shared {
let edge_planes = Edge::canonical_planes(plane_idx, other_plane);
if self
.edge_by_planes_internal(edge_planes.0, edge_planes.1)
.is_none()
{
let edge = Edge {
planes: edge_planes,
vertices: (v1_idx, v2_idx),
};
let e_idx = self.alloc_edge(edge);
if let Some(v) = self.vertex_mut(v1_idx) {
v.edges.push(e_idx);
}
if let Some(v) = self.vertex_mut(v2_idx) {
v.edges.push(e_idx);
}
}
}
}
}
fn build_ideal_edges(&mut self, plane_idx: PlaneIdx, ideal_pairs: &[(VertexIdx, DVec3)]) {
if ideal_pairs.len() < 2 {
return;
}
let Some(plane) = &self.planes[plane_idx.0] else {
return;
};
let (u_axis, v_axis) = create_plane_basis(&plane.normal);
let mut sorted: Vec<(VertexIdx, f64)> = ideal_pairs
.iter()
.map(|&(v_idx, dir)| {
let u = dir.dot(u_axis);
let v = dir.dot(v_axis);
(v_idx, v.atan2(u))
})
.collect();
sorted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
for i in 0..sorted.len() {
let v1_idx = sorted[i].0;
let v2_idx = sorted[(i + 1) % sorted.len()].0;
if v1_idx == v2_idx {
continue;
}
self.try_create_edge_between(plane_idx, v1_idx, v2_idx);
}
}
fn build_ray_edges(
&mut self,
plane_idx: PlaneIdx,
finite_pairs: &[(VertexIdx, DVec3)],
ideal_pairs: &[(VertexIdx, DVec3)],
) {
for &(ideal_idx, _) in ideal_pairs {
let ideal_planes: Vec<PlaneIdx> = self
.vertex(ideal_idx)
.map(|v| v.planes.clone())
.unwrap_or_default();
for &(finite_idx, _) in finite_pairs {
let finite_planes: Vec<PlaneIdx> = self
.vertex(finite_idx)
.map(|v| v.planes.clone())
.unwrap_or_default();
let shared_plane = ideal_planes
.iter()
.filter(|&&p| p != plane_idx)
.find(|&p| finite_planes.contains(p));
if shared_plane.is_some() {
self.try_create_edge_between(plane_idx, finite_idx, ideal_idx);
}
}
}
}
fn try_create_edge_between(
&mut self,
plane_idx: PlaneIdx,
v1_idx: VertexIdx,
v2_idx: VertexIdx,
) {
let v1_planes: Vec<PlaneIdx> = self
.vertex(v1_idx)
.map(|v| v.planes.clone())
.unwrap_or_default();
let v2_planes: Vec<PlaneIdx> = self
.vertex(v2_idx)
.map(|v| v.planes.clone())
.unwrap_or_default();
let other_shared = v1_planes
.iter()
.filter(|&&p| p != plane_idx)
.find(|&p| v2_planes.contains(p));
if let Some(&other_plane) = other_shared {
let edge_planes = Edge::canonical_planes(plane_idx, other_plane);
if self
.edge_by_planes_internal(edge_planes.0, edge_planes.1)
.is_none()
{
let edge = Edge {
planes: edge_planes,
vertices: (v1_idx, v2_idx),
};
let e_idx = self.alloc_edge(edge);
if let Some(v) = self.vertex_mut(v1_idx) {
v.edges.push(e_idx);
}
if let Some(v) = self.vertex_mut(v2_idx) {
v.edges.push(e_idx);
}
}
}
}
fn point_satisfies_all_planes(&self, point: DVec3) -> bool {
self.planes
.iter()
.filter_map(|p| p.as_ref())
.all(|p| p.normal.dot(point) <= p.offset + self.epsilon)
}
fn point_satisfies_planes_except<const N: usize>(
&self,
point: DVec3,
skip: [PlaneIdx; N],
) -> bool {
for &plane_idx in &self.active_planes {
if skip.contains(&plane_idx) {
continue;
}
if let Some(plane) = &self.planes[plane_idx.0]
&& plane.normal.dot(point) > plane.offset + self.epsilon
{
return false;
}
}
true
}
pub fn to_mesh(&mut self) -> (Vec<DVec3>, Vec<Vec<usize>>) {
self.ensure_faces_valid();
let mut vertex_positions = Vec::new();
let mut idx_map: HashMap<VertexIdx, usize> = HashMap::new();
for (v_idx, vertex) in self.vertices() {
if let Some(pos) = vertex.to_euclidean() {
idx_map.insert(v_idx, vertex_positions.len());
vertex_positions.push(pos);
}
}
let mut faces = Vec::new();
for ordered_vertices in self.face_orderings.values() {
if ordered_vertices.len() < 3 {
continue;
}
let face: Vec<usize> = ordered_vertices
.iter()
.filter_map(|v_idx| idx_map.get(v_idx).copied())
.collect();
if face.len() >= 3 {
faces.push(face);
}
}
(vertex_positions, faces)
}
pub fn to_mesh_clipped(&mut self, min: DVec3, max: DVec3) -> (Vec<DVec3>, Vec<Vec<usize>>) {
if self.is_bounded() {
return self.to_mesh();
}
let mut clipped = self.clone();
clipped.add_plane(HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), max.x));
clipped.add_plane(HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), -min.x));
clipped.add_plane(HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), max.y));
clipped.add_plane(HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), -min.y));
clipped.add_plane(HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), max.z));
clipped.add_plane(HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), -min.z));
clipped.to_mesh()
}
#[must_use]
#[expect(
clippy::cast_precision_loss,
reason = "vertex count is small enough that f64 mantissa is sufficient"
)]
pub fn to_mesh_no_rebuild(&self) -> (Vec<DVec3>, Vec<Vec<usize>>) {
let mut vertex_positions = Vec::new();
let mut idx_map: HashMap<VertexIdx, usize> = HashMap::new();
for (v_idx, vertex) in self.vertices() {
if let Some(pos) = vertex.to_euclidean() {
idx_map.insert(v_idx, vertex_positions.len());
vertex_positions.push(pos);
}
}
let mut faces = Vec::new();
for plane_idx in 0..self.planes.len() {
let Some(plane) = &self.planes[plane_idx] else {
continue;
};
let plane_idx = PlaneIdx(plane_idx);
let face_vertices: Vec<VertexIdx> = if self.vertices.len() >= EDGE_WALK_VERTEX_THRESHOLD
{
self.find_face_vertices_via_edges(plane_idx)
.unwrap_or_else(|| {
self.vertices()
.filter(|(_, v)| v.planes.contains(&plane_idx))
.map(|(idx, _)| idx)
.collect()
})
} else {
self.vertices()
.filter(|(_, v)| v.planes.contains(&plane_idx))
.map(|(idx, _)| idx)
.collect()
};
if face_vertices.len() < 3 {
continue;
}
let finite_pairs: Vec<(VertexIdx, DVec3)> = face_vertices
.iter()
.filter_map(|&v_idx| {
self.vertex(v_idx)
.and_then(|v| v.to_euclidean().map(|pos| (v_idx, pos)))
})
.collect();
if finite_pairs.len() < 3 {
continue;
}
let centroid: DVec3 =
finite_pairs.iter().map(|(_, pos)| *pos).sum::<DVec3>() / finite_pairs.len() as f64;
let (u_axis, v_axis) = create_plane_basis(&plane.normal);
let mut vertex_angles: Vec<(VertexIdx, f64)> = finite_pairs
.iter()
.map(|&(v_idx, pos)| {
let local = pos - centroid;
let u = local.dot(u_axis);
let v = local.dot(v_axis);
(v_idx, v.atan2(u))
})
.collect();
vertex_angles.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
if vertex_angles.len() >= 3 {
let p0 = self
.vertex(vertex_angles[0].0)
.and_then(Vertex::to_euclidean);
let p1 = self
.vertex(vertex_angles[1].0)
.and_then(Vertex::to_euclidean);
let p2 = self
.vertex(vertex_angles[2].0)
.and_then(Vertex::to_euclidean);
if let (Some(p0), Some(p1), Some(p2)) = (p0, p1, p2) {
let face_normal = (p1 - p0).cross(p2 - p0);
if face_normal.dot(plane.normal) < 0.0 {
vertex_angles.reverse();
}
}
}
let face: Vec<usize> = vertex_angles
.iter()
.filter_map(|(v_idx, _)| idx_map.get(v_idx).copied())
.collect();
if face.len() >= 3 {
faces.push(face);
}
}
(vertex_positions, faces)
}
fn alloc_vertex(&mut self, vertex: Vertex) -> VertexIdx {
if let Some(idx) = self.vertex_free_list.pop() {
self.vertices[idx.0] = Some(vertex);
idx
} else {
let idx = VertexIdx(self.vertices.len());
self.vertices.push(Some(vertex));
idx
}
}
fn free_vertex(&mut self, idx: VertexIdx) {
if self.vertices[idx.0].is_some() {
self.vertices[idx.0] = None;
self.vertex_free_list.push(idx);
}
}
fn alloc_edge(&mut self, edge: Edge) -> EdgeIdx {
let key = Edge::canonical_planes(edge.planes.0, edge.planes.1);
let idx = if let Some(idx) = self.edge_free_list.pop() {
self.edges[idx.0] = Some(edge);
idx
} else {
let idx = EdgeIdx(self.edges.len());
self.edges.push(Some(edge));
idx
};
self.edge_map.insert(key, idx);
idx
}
fn free_edge(&mut self, idx: EdgeIdx) {
if let Some(edge) = &self.edges[idx.0] {
let key = Edge::canonical_planes(edge.planes.0, edge.planes.1);
self.edge_map.remove(&key);
self.edges[idx.0] = None;
self.edge_free_list.push(idx);
}
}
fn vertex_mut(&mut self, idx: VertexIdx) -> Option<&mut Vertex> {
self.vertices.get_mut(idx.0).and_then(|v| v.as_mut())
}
#[expect(clippy::cast_possible_truncation, reason = "plane indices fit in u32")]
fn alloc_plane(&mut self, plane: HalfSpace) -> PlaneIdx {
let normal = plane.normal;
let idx = if let Some(idx) = self.plane_free_list.pop() {
self.planes[idx.0] = Some(plane);
idx
} else {
let idx = PlaneIdx(self.planes.len());
self.planes.push(Some(plane));
idx
};
self.active_planes.push(idx);
if !self.hull_dirty {
self.normal_hull.insert(idx.0 as u32, normal);
}
idx
}
#[expect(clippy::cast_possible_truncation, reason = "plane indices fit in u32")]
fn free_plane(&mut self, idx: PlaneIdx) {
if self.planes[idx.0].is_some() {
self.planes[idx.0] = None;
self.plane_free_list.push(idx);
if let Some(pos) = self.active_planes.iter().position(|&p| p == idx) {
self.active_planes.swap_remove(pos);
}
if !self.hull_dirty {
self.normal_hull.remove(idx.0 as u32);
}
}
}
fn ensure_edges_valid(&mut self) {
if !self.edges_dirty {
return;
}
let has_edges = self.edges.iter().any(Option::is_some);
if self.dirty_vertices.is_empty() || !has_edges {
self.rebuild_all_edges();
} else {
self.rebuild_edges_incremental();
}
self.edges_dirty = false;
self.dirty_vertices.clear();
}
fn rebuild_all_edges(&mut self) {
self.edges.clear();
self.edge_map.clear();
self.edge_free_list.clear();
for v in self.vertices.iter_mut().filter_map(|v| v.as_mut()) {
v.edges.clear();
}
let vertex_indices: Vec<VertexIdx> = self.vertices().map(|(idx, _)| idx).collect();
self.build_edges_from_vertices(&vertex_indices);
}
fn rebuild_edges_incremental(&mut self) {
let dirty_vertices = std::mem::take(&mut self.dirty_vertices);
if dirty_vertices.is_empty() {
return;
}
let mut plane_to_verts: HashMap<PlaneIdx, Vec<VertexIdx>> = HashMap::new();
for (v_idx, vertex) in self.vertices() {
for &p in &vertex.planes {
plane_to_verts.entry(p).or_default().push(v_idx);
}
}
for &dirty_v in &dirty_vertices {
let Some(dirty_vertex) = self.vertex(dirty_v) else {
continue;
};
let old_edges: Vec<EdgeIdx> = dirty_vertex.edges.clone();
for e_idx in old_edges {
if let Some(edge) = self.edge_internal(e_idx) {
let other_v = if edge.vertices.0 == dirty_v {
edge.vertices.1
} else {
edge.vertices.0
};
if !dirty_vertices.contains(&other_v)
&& let Some(other) = self.vertex_mut(other_v)
{
other.edges.retain(|&e| e != e_idx);
}
}
self.free_edge(e_idx);
}
if let Some(v) = self.vertex_mut(dirty_v) {
v.edges.clear();
}
let dirty_planes: Vec<PlaneIdx> = match self.vertex(dirty_v) {
Some(v) => v.planes.clone(),
None => continue,
};
let mut candidates: HashSet<VertexIdx> = HashSet::new();
for &plane_idx in &dirty_planes {
if let Some(verts) = plane_to_verts.get(&plane_idx) {
candidates.extend(verts.iter().filter(|&&v| v != dirty_v).copied());
}
}
for other_v in candidates {
let Some(other_vertex) = self.vertex(other_v) else {
continue;
};
let shared: Vec<PlaneIdx> = dirty_planes
.iter()
.filter(|p| other_vertex.planes.contains(p))
.copied()
.collect();
if shared.len() >= 2 {
let edge_planes = Edge::canonical_planes(shared[0], shared[1]);
if let Some(existing_e_idx) =
self.edge_by_planes_internal(edge_planes.0, edge_planes.1)
{
if let Some(v) = self.vertex_mut(dirty_v)
&& !v.edges.contains(&existing_e_idx)
{
v.edges.push(existing_e_idx);
}
continue;
}
let edge = Edge {
planes: edge_planes,
vertices: (dirty_v, other_v),
};
let e_idx = self.alloc_edge(edge);
if let Some(v) = self.vertex_mut(dirty_v) {
v.edges.push(e_idx);
}
if let Some(v) = self.vertex_mut(other_v) {
v.edges.push(e_idx);
}
}
}
}
}
fn mark_vertex_dirty(&mut self, v_idx: VertexIdx) {
self.dirty_vertices.insert(v_idx); self.edges_dirty = true;
}
fn ensure_faces_valid(&mut self) {
if !self.faces_dirty {
return;
}
if self.dirty_faces.is_empty() || self.face_orderings.is_empty() {
self.rebuild_all_face_orderings();
} else {
self.rebuild_faces_incremental();
}
self.faces_dirty = false;
self.dirty_faces.clear();
}
fn rebuild_all_face_orderings(&mut self) {
self.face_orderings.clear();
for plane_idx in 0..self.planes.len() {
let plane_idx = PlaneIdx(plane_idx);
self.rebuild_face_ordering(plane_idx);
}
}
fn rebuild_faces_incremental(&mut self) {
let dirty_faces = std::mem::take(&mut self.dirty_faces);
for plane_idx in dirty_faces {
self.face_orderings.remove(&plane_idx);
self.rebuild_face_ordering(plane_idx);
}
}
fn find_face_vertices_via_edges(&self, plane_idx: PlaneIdx) -> Option<Vec<VertexIdx>> {
let (start_edge_idx, start_edge) = self
.edges_internal()
.find(|(_, e)| e.is_on_plane(plane_idx))?;
let start_v = start_edge.vertices.0;
let mut current_v = start_v;
let mut prev_edge_idx = start_edge_idx;
let mut face_verts = Vec::new();
loop {
face_verts.push(current_v);
let current_vertex = self.vertex(current_v)?;
let next_edge_idx = current_vertex.edges.iter().copied().find(|&e_idx| {
e_idx != prev_edge_idx
&& self
.edge_internal(e_idx)
.is_some_and(|e| e.is_on_plane(plane_idx))
})?;
let next_edge = self.edge_internal(next_edge_idx)?;
let next_v = next_edge.other_vertex(current_v)?;
if next_v == start_v {
break;
}
current_v = next_v;
prev_edge_idx = next_edge_idx;
}
Some(face_verts)
}
#[expect(
clippy::cast_precision_loss,
reason = "vertex count is small enough that f64 mantissa is sufficient"
)]
fn rebuild_face_ordering(&mut self, plane_idx: PlaneIdx) {
let Some(plane) = &self.planes[plane_idx.0] else {
return;
};
let face_vertices: Vec<VertexIdx> = if self.vertices.len() >= EDGE_WALK_VERTEX_THRESHOLD {
self.find_face_vertices_via_edges(plane_idx)
.unwrap_or_else(|| {
self.vertices()
.filter(|(_, v)| v.planes.contains(&plane_idx))
.map(|(idx, _)| idx)
.collect()
})
} else {
self.vertices()
.filter(|(_, v)| v.planes.contains(&plane_idx))
.map(|(idx, _)| idx)
.collect()
};
if face_vertices.len() < 3 {
return;
}
let finite_pairs: Vec<(VertexIdx, DVec3)> = face_vertices
.iter()
.filter_map(|&v_idx| {
self.vertex(v_idx)
.and_then(|v| v.to_euclidean().map(|pos| (v_idx, pos)))
})
.collect();
if finite_pairs.len() < 3 {
return;
}
let centroid: DVec3 =
finite_pairs.iter().map(|(_, pos)| *pos).sum::<DVec3>() / finite_pairs.len() as f64;
let (u_axis, v_axis) = create_plane_basis(&plane.normal);
let mut vertex_angles: Vec<(VertexIdx, f64)> = finite_pairs
.iter()
.map(|&(v_idx, pos)| {
let local = pos - centroid;
let u = local.dot(u_axis);
let v = local.dot(v_axis);
(v_idx, v.atan2(u))
})
.collect();
vertex_angles.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
if vertex_angles.len() >= 3 {
let p0 = self
.vertex(vertex_angles[0].0)
.and_then(Vertex::to_euclidean);
let p1 = self
.vertex(vertex_angles[1].0)
.and_then(Vertex::to_euclidean);
let p2 = self
.vertex(vertex_angles[2].0)
.and_then(Vertex::to_euclidean);
if let (Some(p0), Some(p1), Some(p2)) = (p0, p1, p2) {
let face_normal = (p1 - p0).cross(p2 - p0);
if face_normal.dot(plane.normal) < 0.0 {
vertex_angles.reverse();
}
}
}
let ordered: Vec<VertexIdx> = vertex_angles.into_iter().map(|(v, _)| v).collect();
self.face_orderings.insert(plane_idx, ordered);
}
fn mark_face_dirty(&mut self, plane_idx: PlaneIdx) {
self.dirty_faces.insert(plane_idx); self.faces_dirty = true;
}
pub fn remove_plane(&mut self, plane_idx: PlaneIdx) -> Option<RemovePlaneResult> {
self.planes.get(plane_idx.0).and_then(|p| p.as_ref())?;
self.ensure_edges_valid();
let mut adjacent_planes_set: std::collections::HashSet<PlaneIdx> =
std::collections::HashSet::new();
let mut removed_vertex_data: Vec<(VertexIdx, DVec4, Vec<PlaneIdx>)> = Vec::new();
let mut kept_vertex_indices: Vec<VertexIdx> = Vec::new();
for (v_idx, vertex) in self.vertices() {
if !vertex.planes.contains(&plane_idx) {
continue;
}
for &p in &vertex.planes {
if p != plane_idx {
adjacent_planes_set.insert(p);
}
}
let remaining_count = vertex.planes.len() - 1;
if remaining_count < 3 {
let remaining_planes: Vec<PlaneIdx> = vertex
.planes
.iter()
.filter(|&&p| p != plane_idx)
.copied()
.collect();
removed_vertex_data.push((v_idx, vertex.position, remaining_planes));
} else {
kept_vertex_indices.push(v_idx);
}
}
let adjacent_planes: Vec<PlaneIdx> = adjacent_planes_set.into_iter().collect();
let removed_vertices: Vec<VertexIdx> =
removed_vertex_data.iter().map(|(idx, _, _)| *idx).collect();
for &v_idx in &kept_vertex_indices {
if let Some(v) = self.vertex_mut(v_idx) {
v.planes.retain(|&p| p != plane_idx);
}
}
let removed_vertex_set: std::collections::HashSet<VertexIdx> =
removed_vertices.iter().copied().collect();
let edges_to_remove: Vec<(EdgeIdx, VertexIdx, VertexIdx)> = self
.edges_internal()
.filter(|(_, e)| {
e.planes.0 == plane_idx
|| e.planes.1 == plane_idx
|| removed_vertex_set.contains(&e.vertices.0)
|| removed_vertex_set.contains(&e.vertices.1)
})
.map(|(idx, e)| (idx, e.vertices.0, e.vertices.1))
.collect();
for (e_idx, v1_idx, v2_idx) in edges_to_remove {
if !removed_vertex_set.contains(&v1_idx)
&& let Some(v) = self.vertex_mut(v1_idx)
{
v.edges.retain(|&e| e != e_idx);
}
if !removed_vertex_set.contains(&v2_idx)
&& let Some(v) = self.vertex_mut(v2_idx)
{
v.edges.retain(|&e| e != e_idx);
}
self.free_edge(e_idx);
}
for &v_idx in &removed_vertices {
self.free_vertex(v_idx);
}
self.free_plane(plane_idx);
if self.vertex_count() == 0 {
self.hull_dirty = true;
}
let reconstructed_vertices =
self.reconstruct_vertices_topology_based(&removed_vertex_data, &adjacent_planes);
for &p in &adjacent_planes {
self.mark_face_dirty(p);
}
for &v_idx in &reconstructed_vertices {
self.mark_vertex_dirty(v_idx);
}
self.edges_dirty = true;
self.faces_dirty = true;
self.is_bounded = false;
self.hull_dirty = true;
self.face_orderings.remove(&plane_idx);
Some(RemovePlaneResult {
removed_vertices,
affected_vertices: kept_vertex_indices,
})
}
pub fn reconstruct_vertices_after_removal(
&mut self,
adjacent_planes: &[PlaneIdx],
_removed_vertices: &[VertexIdx],
) -> Vec<VertexIdx> {
let mut new_vertices = Vec::new();
let mut spatial_hash = SpatialHash::new(self.epsilon * 10.0);
for (_, v) in self.vertices() {
if let Some(pos) = v.to_euclidean() {
spatial_hash.insert(pos);
}
}
for i in 0..adjacent_planes.len() {
for j in (i + 1)..adjacent_planes.len() {
for k in (j + 1)..adjacent_planes.len() {
let p1_idx = adjacent_planes[i];
let p2_idx = adjacent_planes[j];
let p3_idx = adjacent_planes[k];
let Some(p1) = &self.planes[p1_idx.0] else {
continue;
};
let Some(p2) = &self.planes[p2_idx.0] else {
continue;
};
let Some(p3) = &self.planes[p3_idx.0] else {
continue;
};
if let Some(point_hom) = intersect_three_planes(p1, p2, p3) {
let point = dvec4_xyz(&point_hom);
if !self.point_satisfies_all_planes(point) {
continue;
}
if !spatial_hash.insert_if_unique(point) {
continue;
}
let mut incident_planes = vec![p1_idx.0, p2_idx.0, p3_idx.0];
for plane_idx in 0..self.planes.len() {
if plane_idx != p1_idx.0
&& plane_idx != p2_idx.0
&& plane_idx != p3_idx.0
&& let Some(plane) = &self.planes[plane_idx]
{
let dist = plane.signed_distance(point).abs();
if dist < self.epsilon {
incident_planes.push(plane_idx);
}
}
}
incident_planes.sort_unstable();
let plane_indices: Vec<PlaneIdx> =
incident_planes.iter().map(|&p| PlaneIdx(p)).collect();
let vertex = Vertex {
position: point_hom,
planes: plane_indices,
edges: vec![],
};
let v_idx = self.alloc_vertex(vertex);
new_vertices.push(v_idx);
}
}
}
}
new_vertices
}
#[expect(clippy::too_many_lines)]
fn reconstruct_vertices_topology_based(
&mut self,
removed_vertex_data: &[(VertexIdx, DVec4, Vec<PlaneIdx>)],
adjacent_planes: &[PlaneIdx],
) -> Vec<VertexIdx> {
let mut new_vertices = Vec::with_capacity(ESTIMATED_NEW_VERTICES);
self.spatial_hash.reset(self.epsilon * 10.0);
let mut ideal_vertices_seen: FxHashSet<(PlaneIdx, PlaneIdx, bool)> = FxHashSet::default();
let existing_finite_positions: Vec<DVec3> = self
.vertices()
.filter_map(|(_, v)| v.to_euclidean())
.collect();
let existing_ideal_keys: Vec<(PlaneIdx, PlaneIdx, bool)> = self
.vertices()
.filter_map(|(_, v)| {
if !v.is_finite() && v.planes.len() >= 2 {
let p1 = v.planes[0];
let p2 = v.planes[1];
let (min_p, max_p) = if p1.0 < p2.0 { (p1, p2) } else { (p2, p1) };
let dir = dvec4_xyz(&v.position);
let positive = dir.x > 0.0
|| (dir.x == 0.0 && (dir.y > 0.0 || (dir.y == 0.0 && dir.z > 0.0)));
Some((min_p, max_p, positive))
} else {
None
}
})
.collect();
for pos in existing_finite_positions {
self.spatial_hash.insert(pos);
}
for key in existing_ideal_keys {
ideal_vertices_seen.insert(key);
}
for (_v_idx, position_hom, remaining_planes) in removed_vertex_data {
if remaining_planes.len() < 2 {
continue;
}
let Some(position) = (if position_hom.w.abs() > EPSILON {
Some(dvec4_xyz(position_hom) / position_hom.w)
} else {
None
}) else {
continue;
};
for (i, &p1_idx) in remaining_planes.iter().enumerate() {
let Some(p1) = self.planes[p1_idx.0].clone() else {
continue;
};
for &p2_idx in remaining_planes.iter().skip(i + 1) {
let Some(p2) = self.planes[p2_idx.0].clone() else {
continue;
};
let line_dir = p1.normal.cross(p2.normal);
if line_dir.length_squared() < self.epsilon * self.epsilon {
continue;
}
let line_dir = line_dir.normalize();
for sign in [-1.0, 1.0] {
let dir = sign * line_dir;
if let Some((hit_plane_idx, new_point)) =
self.line_search_next_plane(position, dir, p1_idx, p2_idx)
{
if !self.spatial_hash.insert_if_unique(new_point) {
continue;
}
let mut incident_planes = Vec::with_capacity(4);
incident_planes.extend([p1_idx.0, p2_idx.0, hit_plane_idx.0]);
for (idx, plane_opt) in self.planes.iter().enumerate() {
if idx == p1_idx.0 || idx == p2_idx.0 || idx == hit_plane_idx.0 {
continue;
}
if let Some(plane) = plane_opt
&& plane.signed_distance(new_point).abs() < self.epsilon
{
incident_planes.push(idx);
}
}
incident_planes.sort_unstable();
incident_planes.dedup();
let plane_indices: Vec<PlaneIdx> =
incident_planes.iter().map(|&p| PlaneIdx(p)).collect();
let vertex = Vertex {
position: DVec4::new(new_point.x, new_point.y, new_point.z, 1.0),
planes: plane_indices,
edges: vec![],
};
let v_idx = self.alloc_vertex(vertex);
new_vertices.push(v_idx);
} else {
let (min_p, max_p) = if p1_idx.0 < p2_idx.0 {
(p1_idx, p2_idx)
} else {
(p2_idx, p1_idx)
};
let positive = dir.x > 0.0
|| (dir.x == 0.0 && (dir.y > 0.0 || (dir.y == 0.0 && dir.z > 0.0)));
if !ideal_vertices_seen.insert((min_p, max_p, positive)) {
continue;
}
let plane_indices = vec![p1_idx, p2_idx];
let vertex = Vertex {
position: DVec4::new(dir.x, dir.y, dir.z, 0.0),
planes: plane_indices,
edges: vec![],
};
let v_idx = self.alloc_vertex(vertex);
new_vertices.push(v_idx);
}
}
}
}
}
if adjacent_planes.len() >= 3 {
for i in 0..adjacent_planes.len() {
let p1_idx = adjacent_planes[i];
let Some(p1) = self.planes[p1_idx.0].clone() else {
continue;
};
for j in (i + 1)..adjacent_planes.len() {
let p2_idx = adjacent_planes[j];
let Some(p2) = self.planes[p2_idx.0].clone() else {
continue;
};
#[expect(clippy::needless_range_loop)]
for k in j + 1..adjacent_planes.len() {
let p3_idx = adjacent_planes[k];
let Some(p3) = self.planes[p3_idx.0].clone() else {
continue;
};
let Some(point_hom) = intersect_three_planes(&p1, &p2, &p3) else {
continue;
};
if point_hom.w.abs() < EPSILON {
continue;
}
let point = dvec4_xyz(&point_hom) / point_hom.w;
if !self.point_satisfies_all_planes(point) {
continue;
}
if !self.spatial_hash.insert_if_unique(point) {
continue;
}
let mut incident_planes = Vec::with_capacity(4);
incident_planes.extend([p1_idx.0, p2_idx.0, p3_idx.0]);
for (idx, plane_opt) in self.planes.iter().enumerate() {
if idx == p1_idx.0 || idx == p2_idx.0 || idx == p3_idx.0 {
continue;
}
if let Some(plane) = plane_opt
&& plane.signed_distance(point).abs() < self.epsilon
{
incident_planes.push(idx);
}
}
incident_planes.sort_unstable();
incident_planes.dedup();
let plane_indices: Vec<PlaneIdx> =
incident_planes.iter().map(|&p| PlaneIdx(p)).collect();
let vertex = Vertex {
position: DVec4::new(point.x, point.y, point.z, 1.0),
planes: plane_indices,
edges: vec![],
};
let v_idx = self.alloc_vertex(vertex);
new_vertices.push(v_idx);
}
}
}
}
new_vertices
}
fn line_search_next_plane(
&self,
start: DVec3,
dir: DVec3,
ignore1: PlaneIdx,
ignore2: PlaneIdx,
) -> Option<(PlaneIdx, DVec3)> {
let mut best_t = f64::MAX;
let mut best_plane = None;
for &plane_idx in &self.active_planes {
if plane_idx == ignore1 || plane_idx == ignore2 {
continue;
}
if let Some(plane) = &self.planes[plane_idx.0] {
let n_dot_dir = plane.normal.dot(dir);
if n_dot_dir.abs() < self.epsilon {
continue;
}
let t = (plane.offset - plane.normal.dot(start)) / n_dot_dir;
if t > self.epsilon && t < best_t {
let candidate_point = start + t * dir;
let skip_planes = [ignore1, ignore2, plane_idx];
if self.point_satisfies_planes_except(candidate_point, skip_planes) {
best_t = t;
best_plane = Some((plane_idx, candidate_point));
}
}
}
}
best_plane
}
}
impl Default for IncrementalPolytope {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn intersect_three_planes(p1: &HalfSpace, p2: &HalfSpace, p3: &HalfSpace) -> Option<DVec4> {
let constraints = DVec3::new(p1.offset, p2.offset, p3.offset);
let coeffs = DMat3::from_cols(p1.normal, p2.normal, p3.normal).transpose();
let det = coeffs.determinant();
if det.abs() < EPSILON {
return None; }
let inv = coeffs.inverse();
let pos = inv * constraints;
Some(DVec4::new(pos.x, pos.y, pos.z, 1.0))
}
fn intersect_two_planes_direction(p0: &HalfSpace, p1: &HalfSpace) -> Option<DVec4> {
let dir = p0.normal.cross(p1.normal);
if dir.length_squared() < EPSILON * EPSILON {
return None; }
let dir = dir.normalize();
Some(DVec4::new(dir.x, dir.y, dir.z, 0.0))
}
#[must_use]
pub fn compute_edge_plane_intersection(
v1: DVec3,
v2: DVec3,
plane: &HalfSpace,
epsilon: f64,
) -> Option<DVec3> {
let d1 = plane.signed_distance(v1);
let d2 = plane.signed_distance(v2);
let v1_on = d1.abs() < epsilon;
let v2_on = d2.abs() < epsilon;
if v1_on || v2_on {
return None;
}
let crosses = (d1 > epsilon && d2 < -epsilon) || (d1 < -epsilon && d2 > epsilon);
if !crosses {
return None;
}
let dir = v2 - v1;
let denom = plane.normal.dot(dir);
if denom.abs() < epsilon {
return None;
}
let t = (plane.offset - plane.normal.dot(v1)) / denom;
let t = t.clamp(0.0, 1.0);
Some(v1 + t * dir)
}
#[must_use]
fn compute_ray_plane_intersection(
origin: DVec3,
dir: DVec3,
plane: &HalfSpace,
epsilon: f64,
) -> Option<DVec3> {
let d_origin = plane.signed_distance(origin);
if d_origin.abs() < epsilon {
return None;
}
let denom = plane.normal.dot(dir);
if denom.abs() < epsilon {
return None;
}
let t = (plane.offset - plane.normal.dot(origin)) / denom;
if t < 0.0 {
return None;
}
Some(origin + t * dir)
}
fn create_plane_basis(normal: &DVec3) -> (DVec3, DVec3) {
let arbitrary = if normal.x.abs() < 0.9 {
DVec3::new(1.0, 0.0, 0.0)
} else {
DVec3::new(0.0, 1.0, 0.0)
};
let u = normal.cross(arbitrary).normalize();
let v = normal.cross(u).normalize();
(u, v)
}
#[cfg(test)]
#[expect(clippy::unreadable_literal)]
mod tests {
use super::*;
#[test]
fn test_halfspace_classification() {
let hs = HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 1.0);
assert_eq!(
hs.classify(DVec3::new(0.0, 0.0, 0.0), EPSILON),
Classification::Inside
);
assert_eq!(
hs.classify(DVec3::new(1.0, 0.0, 0.0), EPSILON),
Classification::On
);
assert_eq!(
hs.classify(DVec3::new(2.0, 0.0, 0.0), EPSILON),
Classification::Outside
);
}
#[test]
fn test_three_plane_intersection() {
let p1 = HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.0);
let p2 = HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.0);
let p3 = HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.0);
let point_hom = intersect_three_planes(&p1, &p2, &p3).unwrap();
let point = dvec4_xyz(&point_hom); assert!((point - DVec3::ZERO).length() < EPSILON);
assert!((point_hom.w - 1.0).abs() < EPSILON); }
#[test]
fn test_parallel_planes_no_intersection() {
let p1 = HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.0);
let p2 = HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 1.0);
let p3 = HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.0);
assert!(intersect_three_planes(&p1, &p2, &p3).is_none());
}
#[test]
fn test_edge_plane_intersection() {
let plane = HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5);
let v1 = DVec3::new(0.0, 0.0, 0.0);
let v2 = DVec3::new(1.0, 0.0, 0.0);
let intersection = compute_edge_plane_intersection(v1, v2, &plane, EPSILON).unwrap();
assert!((intersection.x - 0.5).abs() < EPSILON);
assert!(intersection.y.abs() < EPSILON);
assert!(intersection.z.abs() < EPSILON);
}
#[test]
fn test_polytope_storage() {
let mut poly = IncrementalPolytope::new();
let v1 = poly.alloc_vertex(Vertex {
position: DVec4::new(0.0, 0.0, 0.0, 1.0),
planes: vec![PlaneIdx(0), PlaneIdx(1), PlaneIdx(2)],
edges: vec![],
});
assert!(poly.vertex(v1).is_some());
poly.free_vertex(v1);
assert!(poly.vertex(v1).is_none());
let v2 = poly.alloc_vertex(Vertex {
position: DVec4::new(1.0, 0.0, 0.0, 1.0),
planes: vec![PlaneIdx(0), PlaneIdx(1), PlaneIdx(3)],
edges: vec![],
});
assert_eq!(v1.0, v2.0); }
#[test]
fn test_edge_map() {
let mut poly = IncrementalPolytope::new();
let edge = Edge {
planes: (PlaneIdx(0), PlaneIdx(1)),
vertices: (VertexIdx(0), VertexIdx(1)),
};
let idx = poly.alloc_edge(edge);
assert_eq!(poly.edge_by_planes(PlaneIdx(0), PlaneIdx(1)), Some(idx));
assert_eq!(poly.edge_by_planes(PlaneIdx(1), PlaneIdx(0)), Some(idx));
}
#[test]
fn test_cube_construction() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5), ];
for plane in planes {
poly.add_plane(plane);
}
assert!(poly.is_bounded());
assert_eq!(poly.vertex_count(), 8); assert_eq!(poly.edge_count(), 12);
let (vertices, faces) = poly.to_mesh();
assert_eq!(vertices.len(), 8);
assert_eq!(faces.len(), 6);
for v in &vertices {
assert!((v.x.abs() - 0.5).abs() < EPSILON);
assert!((v.y.abs() - 0.5).abs() < EPSILON);
assert!((v.z.abs() - 0.5).abs() < EPSILON);
}
}
#[test]
fn test_tetrahedron_construction() {
let mut poly = IncrementalPolytope::new();
let sqrt3 = 3.0_f64.sqrt();
let sqrt6 = 6.0_f64.sqrt();
let normals = [
DVec3::new(0.0, -1.0 / sqrt3, sqrt6 / 3.0).normalize(),
DVec3::new(1.0, 1.0 / sqrt3, sqrt6 / 3.0).normalize(),
DVec3::new(-1.0, 1.0 / sqrt3, sqrt6 / 3.0).normalize(),
DVec3::new(0.0, 0.0, -1.0),
];
for normal in &normals {
poly.add_plane(HalfSpace::new(*normal, 1.0));
}
assert!(poly.is_bounded());
assert_eq!(poly.vertex_count(), 4); assert_eq!(poly.edge_count(), 6);
let (vertices, faces) = poly.to_mesh();
assert_eq!(vertices.len(), 4);
assert_eq!(faces.len(), 4); }
#[test]
fn test_redundant_plane() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
let result = poly.add_plane(HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 1.0));
assert!(matches!(result, AddPlaneResult::Redundant));
assert_eq!(poly.vertex_count(), 8); }
#[test]
fn test_clipping_cube() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
let diagonal = DVec3::new(1.0, 1.0, 1.0).normalize();
let result = poly.add_plane(HalfSpace::new(diagonal, 0.0));
assert!(matches!(result, AddPlaneResult::Added { .. }));
assert!(poly.vertex_count() > 0);
assert!(poly.is_bounded());
}
#[test]
fn test_lazy_edge_construction() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
assert_eq!(poly.vertex_count(), 8);
assert_eq!(poly.edge_count(), 12);
}
#[test]
fn test_plane_removal_basic() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5), ];
for plane in planes {
poly.add_plane(plane);
}
assert_eq!(poly.vertex_count(), 8);
assert!(poly.is_bounded());
let result = poly.remove_plane(PlaneIdx(0));
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.removed_vertices.len(), 4);
assert_eq!(poly.vertex_count(), 8);
let finite_count = poly.vertices().filter(|(_, v)| v.is_finite()).count();
let ideal_count = poly.vertices().filter(|(_, v)| !v.is_finite()).count();
assert_eq!(finite_count, 4); assert_eq!(ideal_count, 4);
assert_eq!(poly.plane_count(), 5);
assert!(!poly.is_bounded());
}
#[test]
fn test_plane_removal_invalid() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
let result = poly.remove_plane(PlaneIdx(100));
assert!(result.is_none());
assert_eq!(poly.vertex_count(), 8);
}
#[test]
fn test_plane_removal_and_readd() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5), ];
for plane in planes {
poly.add_plane(plane);
}
poly.remove_plane(PlaneIdx(0));
let vertex_count_after_remove = poly.vertex_count();
let finite_before = poly.vertices().filter(|(_, v)| v.is_finite()).count();
let ideal_before = poly.vertices().filter(|(_, v)| !v.is_finite()).count();
eprintln!(
"After remove: vertex_count={vertex_count_after_remove}, finite={finite_before}, ideal={ideal_before}"
);
for (v_idx, v) in poly.vertices() {
eprintln!(
" vertex {:?}: finite={}, planes={:?}, edges={:?}",
v_idx,
v.is_finite(),
v.planes,
v.edges
);
}
assert_eq!(vertex_count_after_remove, 8);
assert_eq!(finite_before, 4);
assert_eq!(ideal_before, 4);
let edge_count = poly.edge_count();
eprintln!("Edge count before re-add: {edge_count}");
eprintln!("After ensure_edges_valid:");
for (v_idx, v) in poly.vertices() {
eprintln!(
" vertex {:?}: finite={}, planes={:?}, edges={:?}",
v_idx,
v.is_finite(),
v.planes,
v.edges
);
}
let test_plane = HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5);
eprintln!(
"Test plane: normal={:?}, offset={}",
test_plane.normal, test_plane.offset
);
for (v_idx, v) in poly.vertices() {
if v.is_finite() {
let pos = v.to_euclidean().unwrap();
let dist = test_plane.normal.dot(pos) - test_plane.offset;
eprintln!(" vertex {v_idx:?} (finite): pos={pos:?}, dist={dist:.3}");
} else {
let dir = v.direction().unwrap();
let dot = test_plane.normal.dot(dir);
eprintln!(" vertex {v_idx:?} (ideal): dir={dir:?}, dot={dot:.3}");
}
}
let result = poly.add_plane(test_plane);
eprintln!("Re-add result: {result:?}");
assert!(matches!(result, AddPlaneResult::Added { .. }));
assert_eq!(poly.vertex_count(), 8);
let finite_after = poly.vertices().filter(|(_, v)| v.is_finite()).count();
let ideal_after = poly.vertices().filter(|(_, v)| !v.is_finite()).count();
assert_eq!(finite_after, 8);
assert_eq!(ideal_after, 0);
}
#[test]
fn test_face_ordering_cached() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
let (vertices1, faces1) = poly.to_mesh();
let (vertices2, faces2) = poly.to_mesh();
assert_eq!(vertices1.len(), vertices2.len());
assert_eq!(faces1.len(), faces2.len());
for (f1, f2) in faces1.iter().zip(faces2.iter()) {
assert_eq!(f1, f2);
}
}
#[test]
fn test_planes_iterator() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
assert_eq!(poly.plane_count(), 6);
assert_eq!(poly.planes().count(), 6);
poly.remove_plane(PlaneIdx(0));
assert_eq!(poly.plane_count(), 5);
assert_eq!(poly.planes().count(), 5);
let plane_indices: Vec<PlaneIdx> = poly.planes().map(|(idx, _)| idx).collect();
assert!(!plane_indices.contains(&PlaneIdx(0))); assert!(plane_indices.contains(&PlaneIdx(1)));
assert!(plane_indices.contains(&PlaneIdx(2)));
}
#[test]
fn test_to_mesh_no_rebuild() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
let (vertices, faces) = poly.to_mesh_no_rebuild();
assert_eq!(vertices.len(), 8);
assert_eq!(faces.len(), 6);
}
#[test]
fn test_three_plane_corner_case() {
let mut poly = IncrementalPolytope::new();
poly.add_plane(HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.0)); poly.add_plane(HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.0)); poly.add_plane(HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.0));
assert_eq!(poly.vertex_count(), 4);
let finite_count = poly.vertices().filter(|(_, v)| v.is_finite()).count();
let ideal_count = poly.vertices().filter(|(_, v)| !v.is_finite()).count();
assert_eq!(finite_count, 1, "Should have exactly 1 finite vertex");
assert_eq!(ideal_count, 3, "Should have exactly 3 ideal vertices");
let finite_vertex = poly
.vertices()
.find(|(_, v)| v.is_finite())
.map(|(_, v)| v.to_euclidean().unwrap());
assert!(finite_vertex.is_some());
let pos = finite_vertex.unwrap();
assert!(pos.length() < EPSILON, "Finite vertex should be at origin");
assert!(!poly.is_bounded());
assert_eq!(poly.edge_count(), 3);
}
#[test]
fn test_plane_removal_makes_unbounded() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5), HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5), ];
for plane in planes {
poly.add_plane(plane);
}
assert!(poly.is_bounded(), "Cube should be bounded");
assert_eq!(poly.vertex_count(), 8);
let result = poly.remove_plane(PlaneIdx(0));
assert!(result.is_some(), "Plane removal should succeed");
assert!(
!poly.is_bounded(),
"Cube with one face removed should be unbounded"
);
assert_eq!(poly.plane_count(), 5);
}
#[test]
fn test_clipping_preserves_edges() {
let mut poly = IncrementalPolytope::new();
let planes = [
HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.5),
HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 0.5),
];
for plane in planes {
poly.add_plane(plane);
}
let initial_edge_count = poly.edge_count();
assert_eq!(initial_edge_count, 12, "Cube should have 12 edges");
let diagonal = DVec3::new(1.0, 1.0, 1.1).normalize();
let result = poly.add_plane(HalfSpace::new(diagonal, 0.0));
assert!(matches!(result, AddPlaneResult::Added { .. }));
let edge_count_after_clip = poly.edge_count();
assert!(
edge_count_after_clip > 0,
"Should have edges after clipping"
);
let edge_data: Vec<_> = poly
.edges()
.map(|(e_idx, e)| (e_idx, e.vertices, e.planes))
.collect();
for (e_idx, vertices, planes) in &edge_data {
assert!(
poly.vertex(vertices.0).is_some() && poly.vertex(vertices.1).is_some(),
"Edge {e_idx:?} should connect valid vertices"
);
assert!(
poly.plane(planes.0).is_some() && poly.plane(planes.1).is_some(),
"Edge {e_idx:?} should reference valid planes"
);
}
let vertex_edges: Vec<_> = poly
.vertices()
.map(|(v_idx, v)| (v_idx, v.edges.clone()))
.collect();
for (v_idx, edges) in &vertex_edges {
for &e_idx in edges {
assert!(
poly.edge(e_idx).is_some(),
"Vertex {v_idx:?} references invalid edge {e_idx:?}"
);
}
}
}
#[test]
fn test_unbounded_to_mesh_clipped() {
let mut poly = IncrementalPolytope::new();
poly.add_plane(HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 0.0));
poly.add_plane(HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 0.0));
poly.add_plane(HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 0.0));
assert!(!poly.is_bounded(), "3-plane corner should be unbounded");
let min = DVec3::new(-2.0, -2.0, -2.0);
let max = DVec3::new(0.0, 0.0, 0.0);
let (vertices, faces) = poly.to_mesh_clipped(min, max);
assert!(!vertices.is_empty(), "Clipped mesh should have vertices");
assert!(!faces.is_empty(), "Clipped mesh should have faces");
for v in &vertices {
assert!(
v.x >= min.x - EPSILON && v.x <= max.x + EPSILON,
"Vertex x={} outside bounds",
v.x
);
assert!(
v.y >= min.y - EPSILON && v.y <= max.y + EPSILON,
"Vertex y={} outside bounds",
v.y
);
assert!(
v.z >= min.z - EPSILON && v.z <= max.z + EPSILON,
"Vertex z={} outside bounds",
v.z
);
}
}
#[test]
fn test_icosahedron_construction() {
let normals = vec![
DVec3::new(0.934172, 0.356822, 0.000000),
DVec3::new(0.934172, -0.356822, 0.000000),
DVec3::new(-0.934172, 0.356822, 0.000000),
DVec3::new(-0.934172, -0.356822, 0.000000),
DVec3::new(0.000000, 0.934172, 0.356822),
DVec3::new(0.000000, 0.934172, -0.356822),
DVec3::new(0.356822, 0.000000, -0.934172),
DVec3::new(-0.356822, 0.000000, -0.934172),
DVec3::new(0.000000, -0.934172, -0.356822),
DVec3::new(0.000000, -0.934172, 0.356822),
DVec3::new(0.356822, 0.000000, 0.934172),
DVec3::new(-0.356822, 0.000000, 0.934172),
DVec3::new(0.577350, 0.577350, -0.577350),
DVec3::new(0.577350, 0.577350, 0.577350),
DVec3::new(-0.577350, 0.577350, -0.577350),
DVec3::new(-0.577350, 0.577350, 0.577350),
DVec3::new(0.577350, -0.577350, -0.577350),
DVec3::new(0.577350, -0.577350, 0.577350),
DVec3::new(-0.577350, -0.577350, -0.577350),
DVec3::new(-0.577350, -0.577350, 0.577350),
];
let mut poly = IncrementalPolytope::new();
for normal in &normals {
poly.add_plane(HalfSpace::new(normal.normalize(), 2.0));
}
assert!(poly.is_bounded(), "Polytope should be bounded");
assert_eq!(poly.vertex_count(), 12);
assert_eq!(poly.edge_count(), 30);
let (vertices, faces) = poly.to_mesh();
assert_eq!(vertices.len(), 12);
assert_eq!(faces.len(), 20);
}
#[test]
fn test_edge_case_polytope() {
#[rustfmt::skip]
let planes_data: [(f64, f64, f64, f64); 11] = [
(-0.2840365469455719, -0.9563649892807007, -0.0684785395860672, -4.3069167137146),
(-0.4269607663154602, -0.9034146666526794, 0.039323657751083374, -4.109272480010986),
(0.4125642478466034, 0.9070128798484802, -0.08437052369117737, 4.1034626960754395),
(-0.28315502405166626, 0.8755069375038147, 0.3915492296218872, 3.8051040172576904),
(-0.958615243434906, 0.28470486402511597, 0., 0.425659716129303),
(-0.019496172666549683, -0.06564456969499588, 0.9976526498794556, 0.5745928287506104),
(0.958615243434906, -0.28470486402511597, -0., -0.39629310369491577),
(0.019496172666549683, 0.06564456969499588, -0.9976526498794556, -0.5452262163162231),
(0.28315502405166626, -0.8755069375038147, -0.3915492296218872, -3.7757372856140137),
(0.9590741395950317, 0.25848281383514404, 0.11560016870498657, 1.9914697408676147),
(-0.9590741395950317, -0.25848281383514404, -0.11560016870498657, -1.962103009223938),
];
let mut poly = IncrementalPolytope::new();
for (nx, ny, nz, d) in planes_data {
let normal = DVec3::new(nx, ny, nz);
poly.add_plane(HalfSpace::new(normal, d));
}
assert!(poly.is_bounded(), "Polytope should be bounded");
let (vertices, faces) = poly.to_mesh();
#[expect(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
let euler = vertices.len() as i32 - poly.edge_count() as i32 + faces.len() as i32;
assert_eq!(euler, 2, "Euler's formula should hold");
}
#[test]
fn test_validate_topology_cube() {
let mut poly = IncrementalPolytope::new();
poly.add_plane(HalfSpace::new(DVec3::new(1.0, 0.0, 0.0), 1.0));
poly.add_plane(HalfSpace::new(DVec3::new(-1.0, 0.0, 0.0), 1.0));
poly.add_plane(HalfSpace::new(DVec3::new(0.0, 1.0, 0.0), 1.0));
poly.add_plane(HalfSpace::new(DVec3::new(0.0, -1.0, 0.0), 1.0));
poly.add_plane(HalfSpace::new(DVec3::new(0.0, 0.0, 1.0), 1.0));
poly.add_plane(HalfSpace::new(DVec3::new(0.0, 0.0, -1.0), 1.0));
assert!(
poly.validate_topology().is_ok(),
"Cube should have valid topology"
);
}
#[test]
fn test_validate_topology_after_removal() {
let mut poly = IncrementalPolytope::new();
let golden = f64::midpoint(1.0, 5.0_f64.sqrt());
for i in 0..20 {
let theta = std::f64::consts::TAU * f64::from(i) / golden;
let phi = (1.0 - 2.0 * (f64::from(i) + 0.5) / 20.0).acos();
let x = phi.sin() * theta.cos();
let y = phi.sin() * theta.sin();
let z = phi.cos();
poly.add_plane(HalfSpace::new(DVec3::new(x, y, z), 1.0));
}
assert!(
poly.validate_topology().is_ok(),
"Should be valid before removal"
);
poly.remove_plane(PlaneIdx(5));
assert!(
poly.validate_topology().is_ok(),
"Should be valid after removal"
);
}
}