use chull::ConvexHull;
use glam::{DMat3, DVec3, Vec3};
use std::{collections::HashMap, fmt};
use super::plane::GpuFacetPlane;
#[derive(Debug, Clone, PartialEq)]
pub enum BrepError {
TooFewPlanes { count: usize },
NonNegativeOffset { index: usize, d: f32 },
CoincidentPlanes { i: usize, j: usize },
HullComputationFailed { message: String },
NonTriangularHullFacets { indices_len: usize },
NoPlanesToMatch,
HullVertexUnmatched { distance: f64, tolerance: f64 },
UnboundedRegion { signed_distance: f64 },
IllConditionedTriple {
a: usize,
b: usize,
c: usize,
det: 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: f32 },
}
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::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 after polar inversion); a bounded polyhedron cannot use two duplicate faces"
),
Self::HullComputationFailed { message } => {
write!(f, "Dual convex hull computation failed: {message}")
}
Self::NonTriangularHullFacets { indices_len } => write!(
f,
"internal error: dual hull returned {indices_len} indices, not a multiple of 3 (expected all-triangle facets)"
),
Self::NoPlanesToMatch => write!(
f,
"internal error: a dual hull vertex did not match any input plane (no input planes to match against)"
),
Self::HullVertexUnmatched {
distance,
tolerance,
} => write!(
f,
"internal error: a dual hull vertex did not match any input plane within tolerance (nearest distance {distance:.3e}, tolerance {tolerance:.3e})"
),
Self::UnboundedRegion { signed_distance } => write!(
f,
"planes do not bound a finite region: the origin is not strictly inside the dual convex hull (signed facet distance {signed_distance:.3e}); 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| = {det:.3e} < {MIN_TRIPLE_DETERMINANT:e}); the 3x3 facet-intersection solve is ill-conditioned"
),
Self::NonFiniteVertex { a, b, c, vertex } => write!(
f,
"planes {a}, {b}, {c} produced a non-finite vertex despite a well-conditioned solve ({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 MIN_TRIPLE_DETERMINANT: f64 = 1e-6;
const COINCIDENT_PLANE_EPS: f64 = 1e-7;
const ORIGIN_INTERIOR_EPS: f64 = 1e-9;
const HULL_VERTEX_MATCH_EPS_REL: f64 = 1e-9;
const VERTEX_WELD_EPS_REL: f32 = 1e-4;
#[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,
}
impl GemPolyhedron {
pub fn from_planes(planes: Vec<GpuFacetPlane>) -> Result<Self, BrepError> {
if planes.len() < 4 {
return Err(BrepError::TooFewPlanes {
count: planes.len(),
});
}
let dual_points = dual_points_from_planes(&planes)?;
let hull = ConvexHull::try_new(&dual_points, 1e-6f64, None).map_err(|e| {
BrepError::HullComputationFailed {
message: format!("{e:?}"),
}
})?;
let (vertices_flat, indices_flat) = hull.vertices_indices();
if indices_flat.len() % 3 != 0 {
return Err(BrepError::NonTriangularHullFacets {
indices_len: indices_flat.len(),
});
}
let hull_vertex_plane_idx = map_hull_vertices_to_planes(&dual_points, &vertices_flat)?;
check_origin_interior(&vertices_flat, &indices_flat)?;
let (raw_vertices, mut unordered_facet_polygons) =
reconstruct_vertices(&planes, &indices_flat, &hull_vertex_plane_idx)?;
let scale = raw_vertices
.iter()
.map(|v| v.length())
.fold(1e-6_f32, f32::max);
let weld_eps = VERTEX_WELD_EPS_REL * scale;
let vertices = weld_vertices(&raw_vertices, &mut unordered_facet_polygons, weld_eps);
let facet_polygons = order_facet_polygons(&planes, &vertices, &unordered_facet_polygons);
let triangle_indices = triangulate_polygons(&facet_polygons);
check_euler(&vertices, &facet_polygons)?;
let volume = mesh_volume(&vertices, &triangle_indices);
if !volume.is_finite() || volume < 1e-9 {
return Err(BrepError::DegenerateVolume { volume });
}
let max_r = vertices.iter().map(|v| v.length()).fold(0.0f32, f32::max);
Ok(Self {
vertices,
facet_planes: planes,
facet_polygons,
triangle_indices,
bounding_radius: max_r,
})
}
#[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]
pub fn facet_area(&self, facet_idx: usize) -> f32 {
let poly = &self.facet_polygons[facet_idx];
if poly.len() < 3 {
return 0.0;
}
let v0 = self.vertices[poly[0] as usize];
let mut cross_sum = Vec3::ZERO;
for w in 1..poly.len() - 1 {
let a = self.vertices[poly[w] as usize] - v0;
let b = self.vertices[poly[w + 1] as usize] - v0;
cross_sum += a.cross(b);
}
cross_sum.length() * 0.5
}
#[must_use]
pub fn facet_areas(&self) -> Vec<f32> {
(0..self.facet_polygons.len())
.map(|i| self.facet_area(i))
.collect()
}
#[must_use]
pub fn volume(&self) -> f32 {
mesh_volume(&self.vertices, &self.triangle_indices)
}
#[must_use]
pub fn girdle_outline(&self) -> Vec<Vec3> {
#[derive(Clone, Copy)]
struct Point2 {
idx: usize,
x: f32,
z: f32,
}
fn cross(o: Point2, a: Point2, b: Point2) -> f32 {
(a.z - o.z).mul_add(-(b.x - o.x), (a.x - o.x) * (b.z - o.z))
}
let mut pts: Vec<Point2> = self
.vertices
.iter()
.enumerate()
.map(|(idx, v)| Point2 {
idx,
x: v.x,
z: v.z,
})
.collect();
pts.sort_by(|p, q| {
p.x.partial_cmp(&q.x)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| p.z.partial_cmp(&q.z).unwrap_or(std::cmp::Ordering::Equal))
});
pts.dedup_by(|p, q| (p.x - q.x).abs() < 1e-6 && (p.z - q.z).abs() < 1e-6);
if pts.len() < 3 {
return pts.into_iter().map(|p| self.vertices[p.idx]).collect();
}
let mut lower: Vec<Point2> = Vec::new();
for &p in &pts {
while lower.len() >= 2
&& cross(lower[lower.len() - 2], lower[lower.len() - 1], p) <= 0.0
{
lower.pop();
}
lower.push(p);
}
let mut upper: Vec<Point2> = Vec::new();
for &p in pts.iter().rev() {
while upper.len() >= 2
&& cross(upper[upper.len() - 2], upper[upper.len() - 1], p) <= 0.0
{
upper.pop();
}
upper.push(p);
}
lower.pop();
upper.pop();
lower.extend(upper);
lower.into_iter().map(|p| self.vertices[p.idx]).collect()
}
}
fn dual_points_from_planes(planes: &[GpuFacetPlane]) -> Result<Vec<Vec<f64>>, BrepError> {
let mut dual_points: Vec<Vec<f64>> = Vec::with_capacity(planes.len());
for (i, p) in planes.iter().enumerate() {
if p.d >= 0.0 {
return Err(BrepError::NonNegativeOffset { index: i, d: p.d });
}
let n = Vec3::from_array(p.normal);
let dual_pt = n / (-p.d);
dual_points.push(vec![
f64::from(dual_pt.x),
f64::from(dual_pt.y),
f64::from(dual_pt.z),
]);
}
for i in 0..dual_points.len() {
for j in (i + 1)..dual_points.len() {
let dist_sq: f64 = (0..3)
.map(|k| (dual_points[i][k] - dual_points[j][k]).powi(2))
.sum();
let scale = dual_points[i]
.iter()
.chain(dual_points[j].iter())
.fold(1.0f64, |m, &v| m.max(v.abs()));
if dist_sq.sqrt() < COINCIDENT_PLANE_EPS * scale {
return Err(BrepError::CoincidentPlanes { i, j });
}
}
}
Ok(dual_points)
}
fn map_hull_vertices_to_planes(
dual_points: &[Vec<f64>],
vertices_flat: &[Vec<f64>],
) -> Result<Vec<usize>, BrepError> {
let mut hull_vertex_plane_idx = Vec::with_capacity(vertices_flat.len());
for hv in vertices_flat {
let mut best_idx = 0usize;
let mut best_dist_sq = f64::INFINITY;
for (i, dp) in dual_points.iter().enumerate() {
let dist_sq: f64 = (0..3).map(|k| (dp[k] - hv[k]).powi(2)).sum();
if dist_sq < best_dist_sq {
best_dist_sq = dist_sq;
best_idx = i;
}
}
if !best_dist_sq.is_finite() {
return Err(BrepError::NoPlanesToMatch);
}
let hv_norm = hv.iter().map(|v| v * v).sum::<f64>().sqrt();
let tolerance = HULL_VERTEX_MATCH_EPS_REL * hv_norm.max(1.0);
let best_dist = best_dist_sq.sqrt();
if best_dist > tolerance {
return Err(BrepError::HullVertexUnmatched {
distance: best_dist,
tolerance,
});
}
hull_vertex_plane_idx.push(best_idx);
}
Ok(hull_vertex_plane_idx)
}
fn check_origin_interior(
vertices_flat: &[Vec<f64>],
indices_flat: &[usize],
) -> Result<(), BrepError> {
let mut centroid = [0.0f64; 3];
for p in vertices_flat {
centroid[0] += p[0];
centroid[1] += p[1];
centroid[2] += p[2];
}
let count = vertices_flat.len() as f64;
for c in &mut centroid {
*c /= count;
}
for tri in indices_flat.as_chunks::<3>().0 {
let pa = &vertices_flat[tri[0]];
let pb = &vertices_flat[tri[1]];
let pc = &vertices_flat[tri[2]];
let edge1 = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
let edge2 = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
let mut face_normal = [
edge1[2].mul_add(-edge2[1], edge1[1] * edge2[2]),
edge1[0].mul_add(-edge2[2], edge1[2] * edge2[0]),
edge1[1].mul_add(-edge2[0], edge1[0] * edge2[1]),
];
let to_centroid = [
centroid[0] - pa[0],
centroid[1] - pa[1],
centroid[2] - pa[2],
];
let dot_centroid = face_normal[2].mul_add(
to_centroid[2],
face_normal[1].mul_add(to_centroid[1], face_normal[0] * to_centroid[0]),
);
if dot_centroid > 0.0 {
face_normal = [-face_normal[0], -face_normal[1], -face_normal[2]];
}
let face_normal_len = face_normal[2]
.mul_add(
face_normal[2],
face_normal[1].mul_add(face_normal[1], face_normal[0] * face_normal[0]),
)
.sqrt();
if face_normal_len < 1e-12 {
continue; }
let to_origin = [-pa[0], -pa[1], -pa[2]];
let side = face_normal[2].mul_add(
to_origin[2],
face_normal[1].mul_add(to_origin[1], face_normal[0] * to_origin[0]),
) / face_normal_len;
if side > -ORIGIN_INTERIOR_EPS {
return Err(BrepError::UnboundedRegion {
signed_distance: side,
});
}
}
Ok(())
}
fn reconstruct_vertices(
planes: &[GpuFacetPlane],
indices_flat: &[usize],
hull_vertex_plane_idx: &[usize],
) -> Result<(Vec<Vec3>, Vec<Vec<u32>>), BrepError> {
let mut vertices = Vec::new();
let mut facet_polygons = vec![Vec::new(); planes.len()];
for tri in indices_flat.as_chunks::<3>().0 {
let a_idx = hull_vertex_plane_idx[tri[0]];
let b_idx = hull_vertex_plane_idx[tri[1]];
let c_idx = hull_vertex_plane_idx[tri[2]];
let (na, ma) = planes[a_idx].to_halfspace_f64();
let (nb, mb) = planes[b_idx].to_halfspace_f64();
let (nc, mc) = planes[c_idx].to_halfspace_f64();
let m = DMat3::from_cols(na, nb, nc).transpose();
let det = m.determinant();
if det.abs() < MIN_TRIPLE_DETERMINANT {
return Err(BrepError::IllConditionedTriple {
a: a_idx,
b: b_idx,
c: c_idx,
det,
});
}
let rhs = DVec3::new(ma, mb, mc);
let vertex = m.inverse() * rhs;
if !vertex.is_finite() {
return Err(BrepError::NonFiniteVertex {
a: a_idx,
b: b_idx,
c: c_idx,
vertex,
});
}
let v_idx = vertices.len() as u32;
vertices.push(vertex.as_vec3());
facet_polygons[a_idx].push(v_idx);
facet_polygons[b_idx].push(v_idx);
facet_polygons[c_idx].push(v_idx);
}
Ok((vertices, facet_polygons))
}
fn weld_vertices(vertices: &[Vec3], facet_polygons: &mut [Vec<u32>], eps: f32) -> Vec<Vec3> {
let mut welded: Vec<Vec3> = Vec::new();
let mut remap: Vec<u32> = Vec::with_capacity(vertices.len());
for v in vertices {
let mut found = None;
for (wi, w) in welded.iter().enumerate() {
if (*v - *w).length() < eps {
found = Some(wi as u32);
break;
}
}
if let Some(idx) = found {
remap.push(idx);
} else {
remap.push(welded.len() as u32);
welded.push(*v);
}
}
for poly in facet_polygons.iter_mut() {
for idx in poly.iter_mut() {
*idx = remap[*idx as usize];
}
poly.sort_unstable();
poly.dedup();
}
welded
}
fn order_facet_polygons(
planes: &[GpuFacetPlane],
vertices: &[Vec3],
unordered: &[Vec<u32>],
) -> Vec<Vec<u32>> {
let mut ordered = Vec::with_capacity(planes.len());
for (i, p) in planes.iter().enumerate() {
let mut poly_verts = unordered[i].clone();
if poly_verts.len() < 3 {
ordered.push(Vec::new());
continue;
}
let mut center = Vec3::ZERO;
for &vi in &poly_verts {
center += vertices[vi as usize];
}
center /= poly_verts.len() as f32;
let normal = Vec3::from_array(p.normal);
let tangent = if normal.z.abs() < 0.999 {
normal.cross(Vec3::Z).normalize()
} else {
normal.cross(Vec3::X).normalize()
};
let bitangent = normal.cross(tangent).normalize();
poly_verts.sort_by(|&a, &b| {
let va = vertices[a as usize] - center;
let vb = vertices[b as usize] - center;
let angle_a = va.dot(bitangent).atan2(va.dot(tangent));
let angle_b = vb.dot(bitangent).atan2(vb.dot(tangent));
angle_a
.partial_cmp(&angle_b)
.unwrap_or(std::cmp::Ordering::Equal)
});
ordered.push(poly_verts);
}
ordered
}
fn triangulate_polygons(facet_polygons: &[Vec<u32>]) -> Vec<u32> {
let mut triangle_indices = Vec::new();
for poly in facet_polygons {
if poly.len() < 3 {
continue;
}
for i in 1..(poly.len() - 1) {
triangle_indices.push(poly[0]);
triangle_indices.push(poly[i]);
triangle_indices.push(poly[i + 1]);
}
}
triangle_indices
}
fn check_euler(vertices: &[Vec3], facet_polygons: &[Vec<u32>]) -> Result<(), BrepError> {
let mut edge_counts: HashMap<(u32, u32), u32> = HashMap::new();
for poly in facet_polygons {
if poly.len() < 3 {
continue;
}
for w in 0..poly.len() {
let a = poly[w];
let b = poly[(w + 1) % poly.len()];
let key = if a < b { (a, b) } else { (b, a) };
*edge_counts.entry(key).or_insert(0) += 1;
}
}
let mut bad_edge: Option<((u32, u32), u32)> = None;
for (&edge, &count) in &edge_counts {
if count != 2 {
bad_edge = Some((edge, count));
break;
}
}
if let Some((edge, count)) = bad_edge {
return Err(BrepError::NonManifoldEdge { edge, count });
}
let vertex_count = vertices.len();
let face_count = facet_polygons.iter().filter(|p| p.len() >= 3).count();
let edge_count = edge_counts.len();
let euler = vertex_count as i64 - edge_count as i64 + face_count as i64;
if euler != 2 {
return Err(BrepError::EulerFormulaFailed {
vertices: vertex_count,
edges: edge_count,
faces: face_count,
euler,
});
}
Ok(())
}
fn mesh_volume(vertices: &[Vec3], triangle_indices: &[u32]) -> f32 {
triangle_indices
.as_chunks::<3>()
.0
.iter()
.map(|tri| {
let a = vertices[tri[0] as usize];
let b = vertices[tri[1] as usize];
let c = vertices[tri[2] as usize];
a.dot(b.cross(c))
})
.sum::<f32>()
.abs()
/ 6.0
}
#[cfg(test)]
mod tests {
use super::*;
fn cube_planes(half_extent: f32) -> Vec<GpuFacetPlane> {
vec![
GpuFacetPlane::new(Vec3::X, -half_extent),
GpuFacetPlane::new(-Vec3::X, -half_extent),
GpuFacetPlane::new(Vec3::Y, -half_extent),
GpuFacetPlane::new(-Vec3::Y, -half_extent),
GpuFacetPlane::new(Vec3::Z, -half_extent),
GpuFacetPlane::new(-Vec3::Z, -half_extent),
]
}
#[test]
fn map_hull_vertices_to_planes_matches_cube_dual_points() {
let dual_points: Vec<Vec<f64>> = vec![
vec![1.0, 0.0, 0.0],
vec![-1.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0],
vec![0.0, -1.0, 0.0],
vec![0.0, 0.0, 1.0],
vec![0.0, 0.0, -1.0],
];
let hull_vertices = dual_points.clone();
let idx = map_hull_vertices_to_planes(&dual_points, &hull_vertices)
.expect("cube dual points should all match their own input plane");
assert_eq!(idx, vec![0, 1, 2, 3, 4, 5]);
}
#[test]
fn map_hull_vertices_to_planes_tolerates_permutation() {
let dual_points: Vec<Vec<f64>> = vec![
vec![1.0, 0.0, 0.0],
vec![-1.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0],
vec![0.0, -1.0, 0.0],
vec![0.0, 0.0, 1.0],
vec![0.0, 0.0, -1.0],
];
let permuted_order = [3usize, 0, 5, 1, 4, 2];
let hull_vertices: Vec<Vec<f64>> = permuted_order
.iter()
.map(|&i| dual_points[i].iter().map(|v| v + 1e-12).collect())
.collect();
let idx = map_hull_vertices_to_planes(&dual_points, &hull_vertices)
.expect("permuted, slightly-perturbed dual points should still map within tolerance");
assert_eq!(idx, permuted_order.to_vec());
}
#[test]
fn map_hull_vertices_to_planes_rejects_vertex_with_no_close_input_point() {
let dual_points: Vec<Vec<f64>> = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]];
let hull_vertices: Vec<Vec<f64>> = vec![vec![5.0, 5.0, 5.0]];
assert!(map_hull_vertices_to_planes(&dual_points, &hull_vertices).is_err());
}
#[test]
fn from_planes_cube_has_expected_topology() {
let hull =
GemPolyhedron::from_planes(cube_planes(1.0)).expect("unit cube must reconstruct");
assert_eq!(hull.vertices.len(), 8);
assert_eq!(hull.facet_polygons.len(), 6);
assert!(hull.facet_polygons.iter().all(|p| p.len() == 4));
assert_eq!(hull.triangle_indices.len(), 36); }
#[test]
fn from_planes_is_scale_invariant_for_cube_topology() {
let unit =
GemPolyhedron::from_planes(cube_planes(1.0)).expect("unit-scale cube must reconstruct");
for &scale in &[1000.0_f32, 0.001_f32] {
let scaled = GemPolyhedron::from_planes(cube_planes(scale))
.unwrap_or_else(|e| panic!("cube scaled by {scale} must reconstruct: {e}"));
assert_eq!(
scaled.vertices.len(),
unit.vertices.len(),
"vertex count must be scale-invariant (scale = {scale})"
);
assert_eq!(
scaled.facet_polygons.len(),
unit.facet_polygons.len(),
"facet count must be scale-invariant (scale = {scale})"
);
assert_eq!(
scaled.triangle_indices.len(),
unit.triangle_indices.len(),
"triangle count must be scale-invariant (scale = {scale})"
);
let expected_radius = unit.bounding_radius * scale;
let rel_err = (scaled.bounding_radius - expected_radius).abs() / expected_radius;
assert!(
rel_err < 1e-3,
"bounding radius should scale linearly (scale = {scale}): got {}, expected ~{expected_radius}",
scaled.bounding_radius
);
}
}
#[test]
fn from_planes_round_brilliant_still_reconstructs_with_f64_solve() {
let planes = crate::geometry::cuts::StandardGemCuts::standard_round_brilliant();
let hull =
GemPolyhedron::from_planes(planes).expect("standard round brilliant must reconstruct");
assert_eq!(hull.untouched_planes(), Vec::<usize>::new());
check_euler(&hull.vertices, &hull.facet_polygons)
.expect("round brilliant topology must satisfy Euler's formula");
}
}