use axiolid_brep::ExactBRep;
use axiolid_core::{Point3, Scalar, Tolerance, Vec3};
use axiolid_curve::Curve3;
use axiolid_surface::Surface;
use axiolid_topology::{Face, LoopId, Orientation};
use core::fmt;
use crate::MassProperties;
pub(crate) const COMPONENTS: usize = 8;
pub(crate) type Sums = [Scalar; COMPONENTS];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExactMeasureError {
NonPlanarFace(&'static str),
MissingSurface,
DanglingReference,
Degenerate,
}
pub(crate) const EVALUATION: ExactMeasureError =
ExactMeasureError::NonPlanarFace("surface or pcurve not evaluable on the face");
pub(crate) const NOT_CONVERGED: ExactMeasureError = ExactMeasureError::NonPlanarFace(
"curved face integral short of its error bound; tessellate and use MeshMeasure",
);
impl fmt::Display for ExactMeasureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonPlanarFace(why) => write!(
f,
"exact measurement cannot integrate a face ({why}). \
Tessellate and use MeshMeasure for an approximate answer."
),
Self::MissingSurface => f.write_str("a face has no support surface"),
Self::DanglingReference => {
f.write_str("a boundary element references missing geometry")
}
Self::Degenerate => f.write_str("the boundary encloses no volume"),
}
}
}
impl std::error::Error for ExactMeasureError {}
pub(crate) fn family(surface: &Surface) -> &'static str {
match surface {
Surface::Plane(_) => "plane",
Surface::Cylinder(_) => "cylindrical",
Surface::Cone(_) => "conical",
Surface::Sphere(_) => "spherical",
Surface::EllipticalCylinder(_) => "elliptical-cylindrical",
Surface::Torus(_) => "toroidal",
_ => "non-planar",
}
}
pub fn exact_properties(
brep: &ExactBRep,
tolerance: Tolerance,
) -> Result<MassProperties, ExactMeasureError> {
let topology = brep.topology();
let scale = characteristic_length(brep);
let linear = tolerance.linear().max(1e-9 * scale);
let mut area = 0.0;
let mut volume = 0.0;
let mut volume_weighted = Point3::ZERO;
let mut moments = Vec3::ZERO;
let mut shell_sense = vec![Orientation::Forward; topology.faces().len()];
for shell in topology.shells() {
for &(face_id, sense) in &shell.faces {
if let Some(slot) = shell_sense.get_mut(face_id.index()) {
*slot = sense;
}
}
}
for (face_index, face) in topology.faces().iter().enumerate() {
let surface_id = face.surface.ok_or(ExactMeasureError::MissingSurface)?;
let surface = brep
.surfaces()
.get(surface_id.index())
.ok_or(ExactMeasureError::DanglingReference)?;
let flip_face = (shell_sense[face_index] == Orientation::Reversed)
^ (face.orientation == Orientation::Reversed);
if matches!(surface, Surface::Plane(_)) && straight_edged(brep, face)? {
let mut vector_area = Vec3::ZERO;
for bound in &face.bounds {
let mut ring = ring_positions(brep, bound.loop_id)?;
if ring.len() < 3 {
continue;
}
if flip_face ^ (bound.orientation == Orientation::Reversed) {
ring.reverse();
}
accumulate_fan(
&ring,
&mut vector_area,
&mut volume,
&mut volume_weighted,
&mut moments,
);
}
area += vector_area.length();
continue;
}
let sums = crate::exact_face::face_sums(brep, face, surface, linear, scale)?;
let sign = if flip_face { -1.0 } else { 1.0 };
area += sums[0].abs();
volume += sign * sums[1];
volume_weighted += Vec3::new(sums[2], sums[3], sums[4]) * sign;
moments += Vec3::new(sums[5], sums[6], sums[7]) * sign;
}
if !volume.is_finite() || volume.abs() < Scalar::EPSILON {
return Err(ExactMeasureError::Degenerate);
}
Ok(MassProperties {
area,
signed_volume: volume,
centroid: volume_weighted / volume,
second_moment_diagonal: moments,
})
}
fn straight_edged(
brep: &ExactBRep,
face: &Face<axiolid_brep::SurfaceId>,
) -> Result<bool, ExactMeasureError> {
let topology = brep.topology();
for bound in &face.bounds {
let wire = topology
.loops()
.get(bound.loop_id.index())
.ok_or(ExactMeasureError::DanglingReference)?;
for use_ in &wire.edges {
let edge = topology
.edges()
.get(use_.edge.index())
.ok_or(ExactMeasureError::DanglingReference)?;
let curve = edge
.curve
.and_then(|id| brep.curves3().get(id.index()))
.ok_or(ExactMeasureError::DanglingReference)?;
if !matches!(curve, Curve3::Line(_)) {
return Ok(false);
}
}
}
Ok(true)
}
fn characteristic_length(brep: &ExactBRep) -> Scalar {
let reach = brep
.topology()
.vertices()
.iter()
.map(|vertex| vertex.position.length())
.fold(0.0, Scalar::max);
let extent = brep
.surfaces()
.iter()
.map(|surface| match surface {
Surface::Cylinder(c) => c.frame.origin.length() + c.radius,
Surface::EllipticalCylinder(c) => {
c.frame.origin.length() + c.semi_axis_x.max(c.semi_axis_y)
}
Surface::Cone(c) => c.frame.origin.length() + c.radius.abs(),
Surface::Sphere(s) => s.frame.origin.length() + s.radius,
Surface::Torus(t) => t.frame.origin.length() + t.major_radius + t.minor_radius,
_ => 0.0,
})
.filter(|value| value.is_finite())
.fold(0.0, Scalar::max);
let length = reach.max(extent);
if length > 0.0 {
length
} else {
1.0
}
}
fn accumulate_fan(
ring: &[Point3],
area: &mut Vec3,
volume: &mut Scalar,
volume_weighted: &mut Point3,
moments: &mut Vec3,
) {
let anchor = ring[0];
for window in ring[1..].windows(2) {
let (a, b, c) = (anchor, window[0], window[1]);
*area += (b - a).cross(c - a) * 0.5;
let six_v = a.dot(b.cross(c));
*volume += six_v / 6.0;
*volume_weighted += (a + b + c) * (six_v / 6.0 / 4.0);
for axis in 0..3 {
let (pa, pb, pc) = (a[axis], b[axis], c[axis]);
let quadratic = pa * pa + pb * pb + pc * pc + pa * pb + pa * pc + pb * pc;
moments[axis] += six_v * quadratic / 60.0;
}
}
}
fn ring_positions(brep: &ExactBRep, loop_id: LoopId) -> Result<Vec<Point3>, ExactMeasureError> {
let topology = brep.topology();
let wire = topology
.loops()
.get(loop_id.index())
.ok_or(ExactMeasureError::DanglingReference)?;
let mut ring = Vec::with_capacity(wire.edges.len());
for use_ in &wire.edges {
let edge = topology
.edges()
.get(use_.edge.index())
.ok_or(ExactMeasureError::DanglingReference)?;
let vertex_id = match use_.orientation {
Orientation::Forward => edge.start,
Orientation::Reversed => edge.end,
};
let vertex = topology
.vertices()
.get(vertex_id.index())
.ok_or(ExactMeasureError::DanglingReference)?;
ring.push(vertex.position);
}
Ok(ring)
}