mod helpers;
use glam::{DVec3, Vec3};
use helpers::{
check_conditioning, check_euler, check_feasible, facet_rings, mesh_volume, output_vertices,
triangulate_polygons, weld_candidates,
};
use std::fmt;
use super::{
plane::GpuFacetPlane,
stone_metrics::{
MIN_TRIPLE_DET, escaping_plane_indices, for_each_feasible_triple, max_abs_offset,
pow2_scale_norm,
},
};
#[derive(Debug, Clone, PartialEq)]
pub enum BrepError {
TooFewPlanes {
count: usize,
},
NonFinitePlane {
index: usize,
},
NonUnitNormal {
index: usize,
length: f64,
},
NonNegativeOffset {
index: usize,
d: f32,
},
CoincidentPlanes {
i: usize,
j: usize,
},
UnboundedRegion {
plane: Option<usize>,
},
IllConditionedTriple {
a: usize,
b: usize,
c: usize,
det: f64,
},
InfeasibleVertex {
vertex: usize,
plane: usize,
excess: f64,
},
NonFiniteVertex {
a: usize,
b: usize,
c: usize,
vertex: DVec3,
},
NonManifoldEdge {
edge: (u32, u32),
count: u32,
},
EulerFormulaFailed {
vertices: usize,
edges: usize,
faces: usize,
euler: i64,
},
DegenerateVolume {
volume: f64,
},
}
impl fmt::Display for BrepError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooFewPlanes { count } => write!(
f,
"at least 4 half-space planes are required to bound a finite 3D polyhedron (a tetrahedron is the minimum); got {count}"
),
Self::NonFinitePlane { index } => {
write!(f, "plane {index} has a NaN or infinite normal or offset")
}
Self::NonUnitNormal { index, length } => write!(
f,
"plane {index} has a normal of length {length} (a unit normal is required)"
),
Self::NonNegativeOffset { index, d } => write!(
f,
"Plane {index} offset d ({d}) must be negative to contain origin"
),
Self::CoincidentPlanes { i, j } => write!(
f,
"planes {i} and {j} are coincident (identical half-spaces); a bounded polyhedron cannot use two duplicate faces"
),
Self::UnboundedRegion { plane } => write!(
f,
"planes do not bound a finite region: the solid reaches the blank box (smallest escaping plane {plane:?}); the half-space schedule is unbounded"
),
Self::IllConditionedTriple { a, b, c, det } => write!(
f,
"planes {a}, {b}, {c} meet at a near-parallel triple (|det| = {:.3e} < {MIN_TRIPLE_DET:e}); the 3x3 facet-intersection solve is ill-conditioned",
det.abs()
),
Self::InfeasibleVertex {
vertex,
plane,
excess,
} => write!(
f,
"reconstructed vertex {vertex} lies {excess:.3e} outside plane {plane}"
),
Self::NonFiniteVertex { a, b, c, vertex } => write!(
f,
"planes {a}, {b}, {c} produced a vertex outside f32 range ({vertex:?})"
),
Self::NonManifoldEdge { edge, count } => write!(
f,
"reconstructed mesh is non-manifold: edge {edge:?} is shared by {count} facets (expected exactly 2)"
),
Self::EulerFormulaFailed {
vertices,
edges,
faces,
euler,
} => write!(
f,
"reconstructed polyhedron fails Euler's formula (V - E + F = 2): V={vertices}, E={edges}, F={faces}, V-E+F={euler}"
),
Self::DegenerateVolume { volume } => write!(
f,
"reconstructed polyhedron has non-finite or ~zero volume ({volume}); the half-space schedule does not bound a proper 3D solid"
),
}
}
}
impl std::error::Error for BrepError {}
const UNIT_NORMAL_TOLERANCE: f64 = 1e-4;
const COINCIDENT_PLANE_EPS: f64 = 1e-7;
const INCIDENCE_EPS: f64 = 1e-9;
const SOLVE_RESIDUAL_BOUND: f64 = 1e-12;
const ENUMERATION_DET_FLOOR: f64 = 1e-8;
const VERTEX_WELD_EPS_REL: f64 = 1e-4;
const FEASIBILITY_GATE_WELDS: f64 = 2.0;
const VOLUME_FLOOR_REL: f64 = 1e-9;
const GIRDLE_DEDUP_REL: f64 = 1e-6;
#[derive(Debug, Clone)]
pub struct GemPolyhedron {
pub vertices: Vec<Vec3>,
pub facet_planes: Vec<GpuFacetPlane>,
pub facet_polygons: Vec<Vec<u32>>,
pub triangle_indices: Vec<u32>,
pub bounding_radius: f32,
}
struct MeetCandidate {
planes: [usize; 3],
det: f64,
v: DVec3,
}
struct WeldedVertex {
incident: Vec<usize>,
best: usize,
}
impl GemPolyhedron {
pub fn from_planes(planes: Vec<GpuFacetPlane>) -> Result<Self, BrepError> {
if planes.len() < 4 {
return Err(BrepError::TooFewPlanes {
count: planes.len(),
});
}
validate_planes(&planes)?;
let (halfspaces, scale) = normalised_halfspaces(&planes);
check_coincident(&halfspaces)?;
let candidates = enumerate_candidates(&halfspaces)?;
let weld_radius = VERTEX_WELD_EPS_REL * max_length(candidates.iter().map(|c| c.v));
let welded = weld_candidates(&candidates, weld_radius);
if welded.len() < 4 {
return Err(BrepError::DegenerateVolume { volume: 0.0 });
}
check_conditioning(&candidates, &welded)?;
let positions: Vec<DVec3> = welded.iter().map(|w| candidates[w.best].v).collect();
check_feasible(
&halfspaces,
&positions,
FEASIBILITY_GATE_WELDS * weld_radius,
scale,
)?;
let facet_polygons = facet_rings(&halfspaces, &positions, &welded);
check_euler(positions.len(), &facet_polygons)?;
let triangle_indices = triangulate_polygons(&facet_polygons);
let radius = max_length(positions.iter().copied());
let volume = mesh_volume(&positions, &triangle_indices);
if !volume.is_finite() || volume < VOLUME_FLOOR_REL * radius.powi(3) {
return Err(BrepError::DegenerateVolume {
volume: volume * scale * scale * scale,
});
}
let vertices = output_vertices(&candidates, &welded, scale)?;
#[expect(
clippy::cast_possible_truncation,
reason = "f32 is the GPU/storage precision of the bounding radius"
)]
let bounding_radius = (radius * scale) as f32;
Ok(Self {
vertices,
facet_planes: planes,
facet_polygons,
triangle_indices,
bounding_radius,
})
}
#[must_use]
pub fn untouched_planes(&self) -> Vec<usize> {
self.facet_polygons
.iter()
.enumerate()
.filter(|(_, p)| p.len() < 3)
.map(|(i, _)| i)
.collect()
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "f32 is the storage precision of a facet area; the f64 sum is in the normal float range"
)]
pub fn facet_area(&self, facet_idx: usize) -> f32 {
let poly = &self.facet_polygons[facet_idx];
if poly.len() < 3 {
return 0.0;
}
let at = |i: u32| self.vertices[i as usize].as_dvec3();
let origin = at(poly[0]);
let cross_sum = poly.windows(2).skip(1).fold(DVec3::ZERO, |acc, pair| {
acc + (at(pair[0]) - origin).cross(at(pair[1]) - origin)
});
(cross_sum.length() * 0.5) as f32
}
#[must_use]
pub fn facet_areas(&self) -> Vec<f32> {
(0..self.facet_polygons.len())
.map(|i| self.facet_area(i))
.collect()
}
#[must_use]
#[expect(
clippy::cast_possible_truncation,
reason = "f32 is the storage precision of the volume; the f64 sum is in the normal float range"
)]
pub fn volume(&self) -> f32 {
let positions: Vec<DVec3> = self.vertices.iter().map(|v| v.as_dvec3()).collect();
mesh_volume(&positions, &self.triangle_indices) as f32
}
#[must_use]
pub fn girdle_outline(&self) -> Vec<Vec3> {
let eps = GIRDLE_DEDUP_REL * f64::from(self.bounding_radius);
let mut pts: Vec<(f64, f64, usize)> = self
.vertices
.iter()
.enumerate()
.map(|(idx, v)| (f64::from(v.x), f64::from(v.z), idx))
.collect();
pts.sort_by(|p, q| {
p.0.total_cmp(&q.0)
.then_with(|| p.1.total_cmp(&q.1))
.then(p.2.cmp(&q.2))
});
pts.dedup_by(|later, kept| {
(later.0 - kept.0).abs() <= eps && (later.1 - kept.1).abs() <= eps
});
if pts.len() < 3 {
return pts.into_iter().map(|p| self.vertices[p.2]).collect();
}
let mut lower = half_hull(pts.iter().copied());
let mut upper = half_hull(pts.iter().rev().copied());
lower.pop();
upper.pop();
lower.extend(upper);
lower.into_iter().map(|p| self.vertices[p.2]).collect()
}
}
fn half_hull(points: impl Iterator<Item = (f64, f64, usize)>) -> Vec<(f64, f64, usize)> {
let turn = |o: (f64, f64, usize), a: (f64, f64, usize), b: (f64, f64, usize)| {
(a.1 - o.1).mul_add(-(b.0 - o.0), (a.0 - o.0) * (b.1 - o.1))
};
let mut chain: Vec<(f64, f64, usize)> = Vec::new();
for p in points {
while chain.len() >= 2 && turn(chain[chain.len() - 2], chain[chain.len() - 1], p) <= 0.0 {
chain.pop();
}
chain.push(p);
}
chain
}
fn validate_planes(planes: &[GpuFacetPlane]) -> Result<(), BrepError> {
for (index, p) in planes.iter().enumerate() {
if !(p.d.is_finite() && p.normal.iter().all(|c| c.is_finite())) {
return Err(BrepError::NonFinitePlane { index });
}
let length = p.to_halfspace_f64().0.length();
if (length - 1.0).abs() > UNIT_NORMAL_TOLERANCE {
return Err(BrepError::NonUnitNormal { index, length });
}
if p.d >= 0.0 {
return Err(BrepError::NonNegativeOffset { index, d: p.d });
}
}
Ok(())
}
fn normalised_halfspaces(planes: &[GpuFacetPlane]) -> (Vec<(DVec3, f64)>, f64) {
let raw: Vec<(DVec3, f64)> = planes.iter().map(|p| p.to_halfspace_f64()).collect();
let scale = pow2_scale_norm(max_abs_offset(&raw));
let halfspaces = raw.iter().map(|&(n, m)| (n, m / scale)).collect();
(halfspaces, scale)
}
fn check_coincident(halfspaces: &[(DVec3, f64)]) -> Result<(), BrepError> {
let duals: Vec<DVec3> = halfspaces.iter().map(|&(n, m)| n / m).collect();
for (i, &qi) in duals.iter().enumerate() {
for (j, &qj) in duals.iter().enumerate().skip(i + 1) {
if (qi - qj).length() < COINCIDENT_PLANE_EPS * qi.length().max(qj.length()) {
return Err(BrepError::CoincidentPlanes { i, j });
}
}
}
Ok(())
}
fn enumerate_candidates(halfspaces: &[(DVec3, f64)]) -> Result<Vec<MeetCandidate>, BrepError> {
let real = halfspaces.len();
let mut candidates = Vec::new();
let closed =
for_each_feasible_triple(halfspaces, ENUMERATION_DET_FLOOR, true, |planes, det, v| {
let tolerance = INCIDENCE_EPS + SOLVE_RESIDUAL_BOUND / det.abs();
if planes[2] < real && halfspaces.iter().all(|&(n, m)| n.dot(v) - m <= tolerance) {
candidates.push(MeetCandidate { planes, det, v });
}
});
if closed.is_none() {
return Err(BrepError::UnboundedRegion {
plane: escaping_plane_indices(halfspaces).first().copied(),
});
}
Ok(candidates)
}
fn max_length(points: impl Iterator<Item = DVec3>) -> f64 {
points.map(DVec3::length).fold(0.0, f64::max)
}
#[cfg(test)]
mod tests;