use crate::error::{Error, Result};
use crate::model::{Mesh, Model, Triangle, Vertex};
use parry3d::math::Vector as ParryVector;
use parry3d::shape::{Shape, TriMesh as ParryTriMesh};
use std::collections::HashMap;
use std::panic::{AssertUnwindSafe, catch_unwind};
pub type Point3d = (f64, f64, f64);
pub type Vector3 = (f64, f64, f64);
pub type BoundingBox = (Point3d, Point3d);
pub type Point2D = (f64, f64);
const MIN_COORDINATE_ABS_VALUE: f64 = 1e-30;
pub fn compute_mesh_signed_volume(mesh: &Mesh) -> Result<f64> {
if mesh.vertices.is_empty() || mesh.triangles.is_empty() {
return Ok(0.0);
}
let mut volume = 0.0_f64;
for triangle in &mesh.triangles {
if triangle.v1 >= mesh.vertices.len()
|| triangle.v2 >= mesh.vertices.len()
|| triangle.v3 >= mesh.vertices.len()
{
continue; }
let v1 = &mesh.vertices[triangle.v1];
let v2 = &mesh.vertices[triangle.v2];
let v3 = &mesh.vertices[triangle.v3];
volume += v1.x * (v2.y * v3.z - v2.z * v3.y)
+ v2.x * (v3.y * v1.z - v3.z * v1.y)
+ v3.x * (v1.y * v2.z - v1.z * v2.y);
}
volume /= 6.0;
Ok(volume)
}
fn validate_mesh_for_parry3d(mesh: &Mesh) -> Result<()> {
let vertex_count = mesh.vertices.len();
if vertex_count > u32::MAX as usize {
return Err(Error::InvalidFormat(format!(
"Mesh has {} vertices, which exceeds u32::MAX ({})",
vertex_count,
u32::MAX
)));
}
for (i, vertex) in mesh.vertices.iter().enumerate() {
if !vertex.x.is_finite() || !vertex.y.is_finite() || !vertex.z.is_finite() {
return Err(Error::InvalidFormat(format!(
"Vertex {} has non-finite coordinates: ({}, {}, {})",
i, vertex.x, vertex.y, vertex.z
)));
}
let f32_max = f32::MAX as f64;
if vertex.x.abs() > f32_max || vertex.y.abs() > f32_max || vertex.z.abs() > f32_max {
return Err(Error::InvalidFormat(format!(
"Vertex {} has coordinates outside f32 range: ({}, {}, {}). \
Values must be within ±{:.0}",
i, vertex.x, vertex.y, vertex.z, f32_max
)));
}
for (coord_name, coord_val) in [("x", vertex.x), ("y", vertex.y), ("z", vertex.z)] {
let abs_val = coord_val.abs();
if abs_val > 0.0 && abs_val < MIN_COORDINATE_ABS_VALUE {
return Err(Error::InvalidFormat(format!(
"Vertex {} has extremely small {} coordinate: {}. \
Non-zero values must be >= {:.2e} in absolute value to prevent \
numerical instability in geometric computations.",
i, coord_name, coord_val, MIN_COORDINATE_ABS_VALUE
)));
}
}
}
for (i, triangle) in mesh.triangles.iter().enumerate() {
if triangle.v1 >= vertex_count || triangle.v2 >= vertex_count || triangle.v3 >= vertex_count
{
return Err(Error::InvalidFormat(format!(
"Triangle {} has invalid vertex indices: ({}, {}, {}) but only {} vertices exist",
i, triangle.v1, triangle.v2, triangle.v3, vertex_count
)));
}
if triangle.v1 > u32::MAX as usize
|| triangle.v2 > u32::MAX as usize
|| triangle.v3 > u32::MAX as usize
{
return Err(Error::InvalidFormat(format!(
"Triangle {} has indices that exceed u32::MAX: ({}, {}, {})",
i, triangle.v1, triangle.v2, triangle.v3
)));
}
}
Ok(())
}
fn safe_create_trimesh(vertices: Vec<ParryVector>, indices: Vec<[u32; 3]>) -> Result<ParryTriMesh> {
let result = catch_unwind(AssertUnwindSafe(|| ParryTriMesh::new(vertices, indices)));
match result {
Ok(Ok(trimesh)) => Ok(trimesh),
Ok(Err(e)) => Err(Error::InvalidFormat(format!(
"Failed to create TriMesh: {}",
e
))),
Err(_) => Err(Error::InvalidFormat(
"parry3d BVH builder panicked during TriMesh creation. \
This can happen with certain edge cases in mesh geometry."
.to_string(),
)),
}
}
pub fn compute_mesh_volume(mesh: &Mesh) -> Result<f64> {
if mesh.vertices.is_empty() || mesh.triangles.is_empty() {
return Ok(0.0);
}
validate_mesh_for_parry3d(mesh)?;
let vertices: Vec<ParryVector> = mesh
.vertices
.iter()
.map(|v| ParryVector::new(v.x as f32, v.y as f32, v.z as f32))
.collect();
let indices: Vec<[u32; 3]> = mesh
.triangles
.iter()
.map(|t| [t.v1 as u32, t.v2 as u32, t.v3 as u32])
.collect();
let trimesh = safe_create_trimesh(vertices, indices)?;
let mass_props = trimesh.mass_properties(1.0);
Ok(mass_props.mass() as f64)
}
pub fn compute_mesh_aabb(mesh: &Mesh) -> Result<BoundingBox> {
if mesh.vertices.is_empty() {
return Err(Error::InvalidFormat(
"Cannot compute bounding box of empty mesh".to_string(),
));
}
if mesh.triangles.is_empty() {
return Err(Error::InvalidFormat(
"Cannot compute bounding box of mesh with no triangles".to_string(),
));
}
validate_mesh_for_parry3d(mesh)?;
let vertices: Vec<ParryVector> = mesh
.vertices
.iter()
.map(|v| ParryVector::new(v.x as f32, v.y as f32, v.z as f32))
.collect();
let indices: Vec<[u32; 3]> = mesh
.triangles
.iter()
.map(|t| [t.v1 as u32, t.v2 as u32, t.v3 as u32])
.collect();
let trimesh = safe_create_trimesh(vertices, indices)?;
let aabb = trimesh.local_aabb();
Ok((
(aabb.mins.x as f64, aabb.mins.y as f64, aabb.mins.z as f64),
(aabb.maxs.x as f64, aabb.maxs.y as f64, aabb.maxs.z as f64),
))
}
pub fn apply_transform(point: Point3d, transform: &[f64; 12]) -> Point3d {
let (x, y, z) = point;
let tx = transform[0] * x + transform[1] * y + transform[2] * z + transform[3];
let ty = transform[4] * x + transform[5] * y + transform[6] * z + transform[7];
let tz = transform[8] * x + transform[9] * y + transform[10] * z + transform[11];
(tx, ty, tz)
}
pub fn compute_transformed_aabb(mesh: &Mesh, transform: Option<&[f64; 12]>) -> Result<BoundingBox> {
let (min, max) = compute_mesh_aabb(mesh)?;
let Some(transform) = transform else {
return Ok((min, max));
};
let corners = [
(min.0, min.1, min.2),
(min.0, min.1, max.2),
(min.0, max.1, min.2),
(min.0, max.1, max.2),
(max.0, min.1, min.2),
(max.0, min.1, max.2),
(max.0, max.1, min.2),
(max.0, max.1, max.2),
];
let transformed_corners: Vec<_> = corners
.iter()
.map(|&corner| apply_transform(corner, transform))
.collect();
let mut result_min = transformed_corners[0];
let mut result_max = transformed_corners[0];
for &(x, y, z) in &transformed_corners[1..] {
result_min.0 = result_min.0.min(x);
result_min.1 = result_min.1.min(y);
result_min.2 = result_min.2.min(z);
result_max.0 = result_max.0.max(x);
result_max.1 = result_max.1.max(y);
result_max.2 = result_max.2.max(z);
}
Ok((result_min, result_max))
}
pub fn compute_build_volume(model: &Model) -> Option<BoundingBox> {
let mut overall_min: Option<Point3d> = None;
let mut overall_max: Option<Point3d> = None;
for item in &model.build.items {
let Some(object) = model
.resources
.objects
.iter()
.find(|obj| obj.id == item.objectid)
else {
continue;
};
let Some(mesh) = &object.mesh else {
continue;
};
let Ok((min, max)) = compute_transformed_aabb(mesh, item.transform.as_ref()) else {
continue;
};
match (overall_min, overall_max) {
(None, None) => {
overall_min = Some(min);
overall_max = Some(max);
}
(Some(cur_min), Some(cur_max)) => {
overall_min = Some((
cur_min.0.min(min.0),
cur_min.1.min(min.1),
cur_min.2.min(min.2),
));
overall_max = Some((
cur_max.0.max(max.0),
cur_max.1.max(max.1),
cur_max.2.max(max.2),
));
}
_ => {
overall_min = Some(min);
overall_max = Some(max);
}
}
}
match (overall_min, overall_max) {
(Some(min), Some(max)) => Some((min, max)),
_ => None,
}
}
pub fn triangle_plane_intersection(
v0: &Vertex,
v1: &Vertex,
v2: &Vertex,
z: f64,
) -> Option<(Point2D, Point2D)> {
if !v0.x.is_finite()
|| !v0.y.is_finite()
|| !v0.z.is_finite()
|| !v1.x.is_finite()
|| !v1.y.is_finite()
|| !v1.z.is_finite()
|| !v2.x.is_finite()
|| !v2.y.is_finite()
|| !v2.z.is_finite()
|| !z.is_finite()
{
return None;
}
let vertices = [v0, v1, v2];
let mut intersections = Vec::with_capacity(2);
for i in 0..3 {
let va = vertices[i];
let vb = vertices[(i + 1) % 3];
let za = va.z;
let zb = vb.z;
if (za - z) * (zb - z) > 0.0 {
continue;
}
if (za - z).abs() < 1e-10 && (zb - z).abs() < 1e-10 {
intersections.push((va.x, va.y));
intersections.push((vb.x, vb.y));
break;
}
if (za - z).abs() < 1e-10 {
intersections.push((va.x, va.y));
continue;
}
if (zb - z).abs() < 1e-10 {
intersections.push((vb.x, vb.y));
continue;
}
let t = (z - za) / (zb - za);
let x = va.x + t * (vb.x - va.x);
let y = va.y + t * (vb.y - va.y);
if x.is_finite() && y.is_finite() {
intersections.push((x, y));
}
}
if intersections.len() >= 2 {
if intersections.len() > 2 {
intersections.retain(|(x, y)| x.is_finite() && y.is_finite());
if intersections.len() >= 2 {
intersections.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
});
intersections
.dedup_by(|a, b| (a.0 - b.0).abs() < 1e-10 && (a.1 - b.1).abs() < 1e-10);
}
}
if intersections.len() >= 2 {
Some((intersections[0], intersections[1]))
} else {
None
}
} else {
None
}
}
pub fn collect_intersection_segments(mesh: &Mesh, z: f64) -> Vec<(Point2D, Point2D)> {
mesh.triangles
.iter()
.filter_map(|tri| {
if tri.v1 >= mesh.vertices.len()
|| tri.v2 >= mesh.vertices.len()
|| tri.v3 >= mesh.vertices.len()
{
return None;
}
let v0 = &mesh.vertices[tri.v1];
let v1 = &mesh.vertices[tri.v2];
let v2 = &mesh.vertices[tri.v3];
triangle_plane_intersection(v0, v1, v2, z)
})
.collect()
}
pub fn assemble_contours(segments: Vec<(Point2D, Point2D)>, tolerance: f64) -> Vec<Vec<Point2D>> {
if segments.is_empty() {
return Vec::new();
}
let mut remaining_segments: Vec<(Point2D, Point2D)> = segments;
let mut contours: Vec<Vec<Point2D>> = Vec::new();
while !remaining_segments.is_empty() {
let first_segment = remaining_segments.remove(0);
let mut contour = vec![first_segment.0, first_segment.1];
let start_point = first_segment.0;
let mut current_point = first_segment.1;
let mut found_connection = true;
while found_connection && !remaining_segments.is_empty() {
found_connection = false;
for i in 0..remaining_segments.len() {
let segment = remaining_segments[i];
let dist_to_start = point_distance(current_point, segment.0);
let dist_to_end = point_distance(current_point, segment.1);
if dist_to_start <= tolerance {
current_point = segment.1;
contour.push(current_point);
remaining_segments.remove(i);
found_connection = true;
break;
} else if dist_to_end <= tolerance {
current_point = segment.0;
contour.push(current_point);
remaining_segments.remove(i);
found_connection = true;
break;
}
}
if point_distance(current_point, start_point) <= tolerance {
contour.pop();
break;
}
}
if !contour.is_empty() {
contours.push(contour);
}
}
contours
}
#[inline]
fn point_distance(p1: Point2D, p2: Point2D) -> f64 {
let dx = p1.0 - p2.0;
let dy = p1.1 - p2.1;
(dx * dx + dy * dy).sqrt()
}
#[inline]
fn cross_product(v1: (f64, f64, f64), v2: (f64, f64, f64)) -> Vector3 {
(
v1.1 * v2.2 - v1.2 * v2.1,
v1.2 * v2.0 - v1.0 * v2.2,
v1.0 * v2.1 - v1.1 * v2.0,
)
}
pub fn calculate_face_normal(v0: &Vertex, v1: &Vertex, v2: &Vertex) -> Vector3 {
let edge1 = (v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);
let edge2 = (v2.x - v0.x, v2.y - v0.y, v2.z - v0.z);
let cross = cross_product(edge1, edge2);
let magnitude = (cross.0 * cross.0 + cross.1 * cross.1 + cross.2 * cross.2).sqrt();
if magnitude > 0.0 {
(
cross.0 / magnitude,
cross.1 / magnitude,
cross.2 / magnitude,
)
} else {
(0.0, 0.0, 0.0)
}
}
pub fn calculate_vertex_normals(mesh: &Mesh) -> Vec<Vector3> {
let mut normals: Vec<(f64, f64, f64)> = vec![(0.0, 0.0, 0.0); mesh.vertices.len()];
for triangle in &mesh.triangles {
if triangle.v1 >= mesh.vertices.len()
|| triangle.v2 >= mesh.vertices.len()
|| triangle.v3 >= mesh.vertices.len()
{
continue; }
let v0 = &mesh.vertices[triangle.v1];
let v1 = &mesh.vertices[triangle.v2];
let v2 = &mesh.vertices[triangle.v3];
let edge1 = (v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);
let edge2 = (v2.x - v0.x, v2.y - v0.y, v2.z - v0.z);
let area_weighted_normal = cross_product(edge1, edge2);
let magnitude = (area_weighted_normal.0 * area_weighted_normal.0
+ area_weighted_normal.1 * area_weighted_normal.1
+ area_weighted_normal.2 * area_weighted_normal.2)
.sqrt();
if magnitude > 0.0 {
normals[triangle.v1].0 += area_weighted_normal.0;
normals[triangle.v1].1 += area_weighted_normal.1;
normals[triangle.v1].2 += area_weighted_normal.2;
normals[triangle.v2].0 += area_weighted_normal.0;
normals[triangle.v2].1 += area_weighted_normal.1;
normals[triangle.v2].2 += area_weighted_normal.2;
normals[triangle.v3].0 += area_weighted_normal.0;
normals[triangle.v3].1 += area_weighted_normal.1;
normals[triangle.v3].2 += area_weighted_normal.2;
}
}
normals
.into_iter()
.map(|(x, y, z)| {
let magnitude = (x * x + y * y + z * z).sqrt();
if magnitude > 0.0 {
(x / magnitude, y / magnitude, z / magnitude)
} else {
(0.0, 0.0, 0.0)
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Mesh, Triangle, Vertex};
#[test]
fn test_compute_mesh_volume_cube() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 0.0, 1.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 1.0)); mesh.vertices.push(Vertex::new(1.0, 1.0, 1.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 1.0));
mesh.triangles.push(Triangle::new(0, 2, 1));
mesh.triangles.push(Triangle::new(0, 3, 2));
mesh.triangles.push(Triangle::new(4, 5, 6));
mesh.triangles.push(Triangle::new(4, 6, 7));
mesh.triangles.push(Triangle::new(0, 1, 5));
mesh.triangles.push(Triangle::new(0, 5, 4));
mesh.triangles.push(Triangle::new(3, 7, 6));
mesh.triangles.push(Triangle::new(3, 6, 2));
mesh.triangles.push(Triangle::new(0, 4, 7));
mesh.triangles.push(Triangle::new(0, 7, 3));
mesh.triangles.push(Triangle::new(1, 2, 6));
mesh.triangles.push(Triangle::new(1, 6, 5));
let signed_volume = compute_mesh_signed_volume(&mesh).unwrap();
assert!(signed_volume > 0.0, "Signed volume should be positive");
assert!(
(signed_volume - 1.0).abs() < 0.01,
"Signed volume: {}",
signed_volume
);
let volume = compute_mesh_volume(&mesh).unwrap();
assert!((volume - 1.0).abs() < 0.01, "Volume: {}", volume);
}
#[test]
fn test_compute_mesh_signed_volume_inverted() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 0.0, 1.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 1.0)); mesh.vertices.push(Vertex::new(1.0, 1.0, 1.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 1.0));
mesh.triangles.push(Triangle::new(1, 2, 0)); mesh.triangles.push(Triangle::new(2, 3, 0)); mesh.triangles.push(Triangle::new(6, 5, 4)); mesh.triangles.push(Triangle::new(7, 6, 4)); mesh.triangles.push(Triangle::new(5, 1, 0)); mesh.triangles.push(Triangle::new(4, 5, 0)); mesh.triangles.push(Triangle::new(6, 7, 3)); mesh.triangles.push(Triangle::new(2, 6, 3)); mesh.triangles.push(Triangle::new(7, 4, 0)); mesh.triangles.push(Triangle::new(3, 7, 0)); mesh.triangles.push(Triangle::new(6, 2, 1)); mesh.triangles.push(Triangle::new(5, 6, 1));
let signed_volume = compute_mesh_signed_volume(&mesh).unwrap();
assert!(
signed_volume < 0.0,
"Inverted mesh should have negative signed volume, got {}",
signed_volume
);
}
#[test]
fn test_compute_mesh_aabb() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(10.0, 5.0, 3.0));
mesh.vertices.push(Vertex::new(-2.0, 8.0, 1.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
let (min, max) = compute_mesh_aabb(&mesh).unwrap();
assert_eq!(min, (-2.0, 0.0, 0.0));
assert_eq!(max, (10.0, 8.0, 3.0));
}
#[test]
fn test_apply_transform_identity() {
let point = (1.0, 2.0, 3.0);
let transform = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0];
let result = apply_transform(point, &transform);
assert_eq!(result, point);
}
#[test]
fn test_apply_transform_translation() {
let point = (1.0, 2.0, 3.0);
let transform = [1.0, 0.0, 0.0, 5.0, 0.0, 1.0, 0.0, 10.0, 0.0, 0.0, 1.0, 15.0];
let result = apply_transform(point, &transform);
assert_eq!(result, (6.0, 12.0, 18.0));
}
#[test]
fn test_compute_transformed_aabb() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(10.0, 10.0, 10.0));
mesh.triangles.push(Triangle::new(0, 1, 0));
let transform = [1.0, 0.0, 0.0, 5.0, 0.0, 1.0, 0.0, 5.0, 0.0, 0.0, 1.0, 5.0];
let (min, max) = compute_transformed_aabb(&mesh, Some(&transform)).unwrap();
assert_eq!(min, (5.0, 5.0, 5.0));
assert_eq!(max, (15.0, 15.0, 15.0));
}
#[test]
fn test_empty_mesh_volume() {
let mesh = Mesh::new();
let volume = compute_mesh_volume(&mesh).unwrap();
assert_eq!(volume, 0.0);
}
#[test]
fn test_empty_mesh_aabb() {
let mesh = Mesh::new();
let result = compute_mesh_aabb(&mesh);
assert!(result.is_err());
}
#[test]
fn test_mesh_with_no_triangles_aabb() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(10.0, 10.0, 10.0));
let result = compute_mesh_aabb(&mesh);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no triangles"));
}
#[test]
fn test_calculate_face_normal_simple() {
let v0 = Vertex::new(0.0, 0.0, 0.0);
let v1 = Vertex::new(1.0, 0.0, 0.0);
let v2 = Vertex::new(0.0, 1.0, 0.0);
let normal = calculate_face_normal(&v0, &v1, &v2);
assert!((normal.0 - 0.0).abs() < 1e-10, "X component: {}", normal.0);
assert!((normal.1 - 0.0).abs() < 1e-10, "Y component: {}", normal.1);
assert!((normal.2 - 1.0).abs() < 1e-10, "Z component: {}", normal.2);
}
#[test]
fn test_calculate_face_normal_negative_z() {
let v0 = Vertex::new(0.0, 0.0, 0.0);
let v1 = Vertex::new(0.0, 1.0, 0.0);
let v2 = Vertex::new(1.0, 0.0, 0.0);
let normal = calculate_face_normal(&v0, &v1, &v2);
assert!((normal.0 - 0.0).abs() < 1e-10);
assert!((normal.1 - 0.0).abs() < 1e-10);
assert!((normal.2 - (-1.0)).abs() < 1e-10);
}
#[test]
fn test_calculate_face_normal_arbitrary() {
let v0 = Vertex::new(1.0, 0.0, 0.0);
let v1 = Vertex::new(0.0, 1.0, 0.0);
let v2 = Vertex::new(0.0, 0.0, 1.0);
let normal = calculate_face_normal(&v0, &v1, &v2);
let magnitude = (normal.0 * normal.0 + normal.1 * normal.1 + normal.2 * normal.2).sqrt();
assert!((magnitude - 1.0).abs() < 1e-10, "Magnitude: {}", magnitude);
assert!(
(normal.0 - normal.1).abs() < 1e-10,
"X: {}, Y: {}",
normal.0,
normal.1
);
assert!(
(normal.1 - normal.2).abs() < 1e-10,
"Y: {}, Z: {}",
normal.1,
normal.2
);
}
#[test]
fn test_calculate_face_normal_degenerate() {
let v0 = Vertex::new(0.0, 0.0, 0.0);
let v1 = Vertex::new(1.0, 0.0, 0.0);
let v2 = Vertex::new(2.0, 0.0, 0.0);
let normal = calculate_face_normal(&v0, &v1, &v2);
assert_eq!(normal, (0.0, 0.0, 0.0));
}
#[test]
fn test_calculate_face_normal_zero_area() {
let v0 = Vertex::new(1.0, 2.0, 3.0);
let v1 = Vertex::new(1.0, 2.0, 3.0);
let v2 = Vertex::new(4.0, 5.0, 6.0);
let normal = calculate_face_normal(&v0, &v1, &v2);
assert_eq!(normal, (0.0, 0.0, 0.0));
}
#[test]
fn test_calculate_vertex_normals_single_triangle() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
let normals = calculate_vertex_normals(&mesh);
assert_eq!(normals.len(), 3);
for normal in &normals {
assert!((normal.0 - 0.0).abs() < 1e-10, "X: {}", normal.0);
assert!((normal.1 - 0.0).abs() < 1e-10, "Y: {}", normal.1);
assert!((normal.2 - 1.0).abs() < 1e-10, "Z: {}", normal.2);
}
}
#[test]
fn test_calculate_vertex_normals_cube() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 0.0, 1.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 1.0)); mesh.vertices.push(Vertex::new(1.0, 1.0, 1.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 1.0));
mesh.triangles.push(Triangle::new(0, 2, 1));
mesh.triangles.push(Triangle::new(0, 3, 2));
mesh.triangles.push(Triangle::new(4, 5, 6));
mesh.triangles.push(Triangle::new(4, 6, 7));
mesh.triangles.push(Triangle::new(0, 1, 5));
mesh.triangles.push(Triangle::new(0, 5, 4));
mesh.triangles.push(Triangle::new(3, 7, 6));
mesh.triangles.push(Triangle::new(3, 6, 2));
mesh.triangles.push(Triangle::new(0, 4, 7));
mesh.triangles.push(Triangle::new(0, 7, 3));
mesh.triangles.push(Triangle::new(1, 2, 6));
mesh.triangles.push(Triangle::new(1, 6, 5));
let normals = calculate_vertex_normals(&mesh);
assert_eq!(normals.len(), 8);
let expected = 1.0 / (3.0_f64).sqrt();
assert!(
(normals[0].0 - (-expected)).abs() < 1e-10,
"V0 X: {}",
normals[0].0
);
assert!(
(normals[0].1 - (-expected)).abs() < 1e-10,
"V0 Y: {}",
normals[0].1
);
assert!(
(normals[0].2 - (-expected)).abs() < 1e-10,
"V0 Z: {}",
normals[0].2
);
for (i, normal) in normals.iter().enumerate() {
let magnitude =
(normal.0 * normal.0 + normal.1 * normal.1 + normal.2 * normal.2).sqrt();
assert!(
(magnitude - 1.0).abs() < 1e-10,
"Vertex {} normal magnitude: {}",
i,
magnitude
);
}
}
#[test]
fn test_calculate_vertex_normals_empty_mesh() {
let mesh = Mesh::new();
let normals = calculate_vertex_normals(&mesh);
assert_eq!(normals.len(), 0);
}
#[test]
fn test_calculate_vertex_normals_with_degenerate_triangles() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
mesh.vertices.push(Vertex::new(2.0, 0.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
mesh.triangles.push(Triangle::new(0, 1, 3));
let normals = calculate_vertex_normals(&mesh);
assert_eq!(normals.len(), 4);
assert!((normals[0].2 - 1.0).abs() < 1e-10);
assert!((normals[1].2 - 1.0).abs() < 1e-10);
assert!((normals[2].2 - 1.0).abs() < 1e-10);
assert_eq!(normals[3], (0.0, 0.0, 0.0));
}
#[test]
fn test_calculate_vertex_normals_with_invalid_indices() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
mesh.triangles.push(Triangle::new(0, 1, 10));
let normals = calculate_vertex_normals(&mesh);
assert_eq!(normals.len(), 3);
for normal in &normals {
assert!((normal.2 - 1.0).abs() < 1e-10);
}
}
#[test]
fn test_calculate_vertex_normals_area_weighting() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(2.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 2.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
mesh.triangles.push(Triangle::new(0, 3, 4));
let normals = calculate_vertex_normals(&mesh);
assert!((normals[0].0 - 0.0).abs() < 1e-10);
assert!((normals[0].1 - 0.0).abs() < 1e-10);
assert!((normals[0].2 - 1.0).abs() < 1e-10);
let magnitude = (normals[0].0 * normals[0].0
+ normals[0].1 * normals[0].1
+ normals[0].2 * normals[0].2)
.sqrt();
assert!((magnitude - 1.0).abs() < 1e-10);
}
#[test]
fn test_triangle_indices_exceed_u32_max() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
let mut triangle = Triangle::new(0, 1, 2);
triangle.v1 = (u32::MAX as usize) + 1;
mesh.triangles.push(triangle);
let volume_result = compute_mesh_volume(&mesh);
assert!(volume_result.is_err());
let err_msg = volume_result.unwrap_err().to_string();
assert!(
err_msg.contains("exceed") || err_msg.contains("invalid"),
"Error message was: {}",
err_msg
);
let aabb_result = compute_mesh_aabb(&mesh);
assert!(aabb_result.is_err());
let err_msg = aabb_result.unwrap_err().to_string();
assert!(
err_msg.contains("exceed") || err_msg.contains("invalid"),
"Error message was: {}",
err_msg
);
}
#[test]
fn test_parry3d_bvh_panic_handling() {
let mut mesh = Mesh::new();
let large_val = f32::MAX as f64 * 0.5;
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(large_val, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.0, large_val, 0.0));
mesh.vertices.push(Vertex::new(0.0, 0.0, large_val));
mesh.triangles.push(Triangle::new(0, 1, 2));
mesh.triangles.push(Triangle::new(0, 2, 3));
mesh.triangles.push(Triangle::new(0, 3, 1));
mesh.triangles.push(Triangle::new(1, 3, 2));
let volume_result = compute_mesh_volume(&mesh);
assert!(
volume_result.is_ok(),
"Volume computation should succeed with large valid coordinates: {:?}",
volume_result
);
let aabb_result = compute_mesh_aabb(&mesh);
assert!(
aabb_result.is_ok(),
"AABB computation should succeed with large valid coordinates: {:?}",
aabb_result
);
let mut small_mesh = Mesh::new();
small_mesh.vertices.push(Vertex::new(1e-35, 0.0, 0.0)); small_mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
small_mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
small_mesh.triangles.push(Triangle::new(0, 1, 2));
let small_volume_result = compute_mesh_volume(&small_mesh);
assert!(
small_volume_result.is_err(),
"Volume computation should reject extremely small coordinates"
);
let err_msg = small_volume_result.unwrap_err().to_string();
assert!(
err_msg.contains("extremely small") || err_msg.contains("numerical instability"),
"Error should mention numerical issues: {}",
err_msg
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SubdivisionMethod {
Midpoint,
Loop,
CatmullClark,
}
#[derive(Clone, Debug)]
pub struct SubdivisionOptions {
pub method: SubdivisionMethod,
pub levels: u32,
pub preserve_boundaries: bool,
pub interpolate_uvs: bool,
}
impl Default for SubdivisionOptions {
fn default() -> Self {
Self {
method: SubdivisionMethod::Midpoint,
levels: 1,
preserve_boundaries: true,
interpolate_uvs: true,
}
}
}
pub fn subdivide(mesh: &Mesh, options: &SubdivisionOptions) -> Mesh {
const MAX_SUBDIVISION_LEVELS: u32 = 10;
let capped_levels = options.levels.min(MAX_SUBDIVISION_LEVELS);
let mut result = mesh.clone();
for _ in 0..capped_levels {
result = match options.method {
SubdivisionMethod::Midpoint => subdivide_midpoint(&result),
SubdivisionMethod::Loop => subdivide_loop(&result),
SubdivisionMethod::CatmullClark => {
subdivide_midpoint(&result)
}
};
}
result
}
pub fn subdivide_simple(mesh: &Mesh, levels: u32) -> Mesh {
let options = SubdivisionOptions {
method: SubdivisionMethod::Midpoint,
levels,
..Default::default()
};
subdivide(mesh, &options)
}
pub fn subdivide_midpoint(mesh: &Mesh) -> Mesh {
if mesh.triangles.is_empty() {
return mesh.clone();
}
let num_vertices = mesh.vertices.len();
let estimated_new_vertices = mesh
.vertices
.len()
.saturating_add(mesh.triangles.len().saturating_mul(3) / 2);
let new_triangle_count = mesh.triangles.len().saturating_mul(4);
let mut result = Mesh::with_capacity(estimated_new_vertices, new_triangle_count);
result.vertices.extend_from_slice(&mesh.vertices);
let mut edge_midpoints: HashMap<(usize, usize), usize> = HashMap::new();
for triangle in &mesh.triangles {
if triangle.v1 >= num_vertices || triangle.v2 >= num_vertices || triangle.v3 >= num_vertices
{
continue;
}
let m0 = get_or_create_midpoint(
&mut result.vertices,
&mut edge_midpoints,
&mesh.vertices,
triangle.v1,
triangle.v2,
);
let m1 = get_or_create_midpoint(
&mut result.vertices,
&mut edge_midpoints,
&mesh.vertices,
triangle.v2,
triangle.v3,
);
let m2 = get_or_create_midpoint(
&mut result.vertices,
&mut edge_midpoints,
&mesh.vertices,
triangle.v3,
triangle.v1,
);
let mut t0 = Triangle::new(triangle.v1, m0, m2);
let mut t1 = Triangle::new(m0, triangle.v2, m1);
let mut t2 = Triangle::new(m2, m1, triangle.v3);
let mut t3 = Triangle::new(m0, m1, m2);
for t in [&mut t0, &mut t1, &mut t2, &mut t3] {
t.pid = triangle.pid;
t.pindex = triangle.pindex;
t.p1 = triangle.p1;
t.p2 = triangle.p2;
t.p3 = triangle.p3;
}
result.triangles.push(t0);
result.triangles.push(t1);
result.triangles.push(t2);
result.triangles.push(t3);
}
result.beamset = mesh.beamset.clone();
result
}
fn get_or_create_midpoint(
vertices: &mut Vec<Vertex>,
edge_midpoints: &mut HashMap<(usize, usize), usize>,
original_vertices: &[Vertex],
v1: usize,
v2: usize,
) -> usize {
let edge_key = if v1 < v2 { (v1, v2) } else { (v2, v1) };
if let Some(&midpoint_idx) = edge_midpoints.get(&edge_key) {
return midpoint_idx;
}
let vert1 = &original_vertices[v1];
let vert2 = &original_vertices[v2];
let midpoint = Vertex::new(
(vert1.x + vert2.x) / 2.0,
(vert1.y + vert2.y) / 2.0,
(vert1.z + vert2.z) / 2.0,
);
let midpoint_idx = vertices.len();
vertices.push(midpoint);
edge_midpoints.insert(edge_key, midpoint_idx);
midpoint_idx
}
pub fn subdivide_loop(mesh: &Mesh) -> Mesh {
if mesh.triangles.is_empty() {
return mesh.clone();
}
subdivide_midpoint(mesh)
}
#[cfg(test)]
mod subdivision_tests {
use super::*;
#[test]
fn test_subdivide_simple_single_triangle() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
let subdivided = subdivide_simple(&mesh, 1);
assert_eq!(subdivided.triangles.len(), 4);
assert_eq!(subdivided.vertices.len(), 6);
}
#[test]
fn test_subdivide_midpoint_vertex_count() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
let subdivided = subdivide_midpoint(&mesh);
let m0 = &subdivided.vertices[3];
assert!((m0.x - 0.5).abs() < 1e-10);
assert!((m0.y - 0.0).abs() < 1e-10);
assert!((m0.z - 0.0).abs() < 1e-10);
}
#[test]
fn test_subdivide_multiple_levels() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
let subdivided = subdivide_simple(&mesh, 2);
assert_eq!(subdivided.triangles.len(), 16);
let subdivided = subdivide_simple(&mesh, 3);
assert_eq!(subdivided.triangles.len(), 64);
}
#[test]
fn test_subdivide_preserves_properties() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
let mut triangle = Triangle::new(0, 1, 2);
triangle.pid = Some(5);
triangle.p1 = Some(1);
triangle.p2 = Some(2);
triangle.p3 = Some(3);
mesh.triangles.push(triangle);
let subdivided = subdivide_midpoint(&mesh);
for tri in &subdivided.triangles {
assert_eq!(tri.pid, Some(5));
assert_eq!(tri.p1, Some(1));
assert_eq!(tri.p2, Some(2));
assert_eq!(tri.p3, Some(3));
}
}
#[test]
fn test_subdivide_empty_mesh() {
let mesh = Mesh::new();
let subdivided = subdivide_simple(&mesh, 1);
assert_eq!(subdivided.vertices.len(), 0);
assert_eq!(subdivided.triangles.len(), 0);
}
#[test]
fn test_subdivide_two_triangles_shared_edge() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.5, -1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
mesh.triangles.push(Triangle::new(0, 3, 1));
let subdivided = subdivide_midpoint(&mesh);
assert_eq!(subdivided.triangles.len(), 8);
assert_eq!(subdivided.vertices.len(), 9);
}
#[test]
fn test_subdivide_winding_order() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0)); mesh.triangles.push(Triangle::new(0, 1, 2));
let subdivided = subdivide_midpoint(&mesh);
for tri in &subdivided.triangles {
let v0 = &subdivided.vertices[tri.v1];
let v1 = &subdivided.vertices[tri.v2];
let v2 = &subdivided.vertices[tri.v3];
let area = (v1.x - v0.x) * (v2.y - v0.y) - (v2.x - v0.x) * (v1.y - v0.y);
assert!(area > 0.0, "Triangle winding order not preserved");
}
}
#[test]
fn test_subdivision_options() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
let options = SubdivisionOptions {
method: SubdivisionMethod::Midpoint,
levels: 2,
preserve_boundaries: true,
interpolate_uvs: true,
};
let subdivided = subdivide(&mesh, &options);
assert_eq!(subdivided.triangles.len(), 16);
}
#[test]
fn test_subdivide_loop_basic() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
mesh.triangles.push(Triangle::new(0, 1, 2));
let options = SubdivisionOptions {
method: SubdivisionMethod::Loop,
levels: 1,
..Default::default()
};
let subdivided = subdivide(&mesh, &options);
assert_eq!(subdivided.triangles.len(), 4);
}
#[test]
fn test_triangle_plane_intersection_simple() {
let v0 = Vertex::new(0.0, 0.0, 0.0);
let v1 = Vertex::new(10.0, 0.0, 10.0);
let v2 = Vertex::new(5.0, 10.0, 0.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_some());
let (p1, p2) = result.unwrap();
assert!(p1.0 >= 0.0 && p1.0 <= 10.0);
assert!(p1.1 >= 0.0 && p1.1 <= 10.0);
assert!(p2.0 >= 0.0 && p2.0 <= 10.0);
assert!(p2.1 >= 0.0 && p2.1 <= 10.0);
}
#[test]
fn test_triangle_plane_intersection_no_intersection() {
let v0 = Vertex::new(0.0, 0.0, 10.0);
let v1 = Vertex::new(10.0, 0.0, 15.0);
let v2 = Vertex::new(5.0, 10.0, 12.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_none());
let v0 = Vertex::new(0.0, 0.0, 0.0);
let v1 = Vertex::new(10.0, 0.0, 2.0);
let v2 = Vertex::new(5.0, 10.0, 1.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_none());
}
#[test]
fn test_triangle_plane_intersection_vertex_on_plane() {
let v0 = Vertex::new(0.0, 0.0, 5.0); let v1 = Vertex::new(10.0, 0.0, 0.0); let v2 = Vertex::new(5.0, 10.0, 10.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_some());
}
#[test]
fn test_triangle_plane_intersection_nan_handling() {
let v0 = Vertex::new(f64::NAN, 0.0, 0.0);
let v1 = Vertex::new(10.0, 0.0, 10.0);
let v2 = Vertex::new(5.0, 10.0, 0.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_none());
let v0 = Vertex::new(0.0, f64::NAN, 0.0);
let v1 = Vertex::new(10.0, 0.0, 10.0);
let v2 = Vertex::new(5.0, 10.0, 0.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_none());
let v0 = Vertex::new(0.0, 0.0, f64::NAN);
let v1 = Vertex::new(10.0, 0.0, 10.0);
let v2 = Vertex::new(5.0, 10.0, 0.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_none());
let v0 = Vertex::new(f64::INFINITY, 0.0, 0.0);
let v1 = Vertex::new(10.0, 0.0, 10.0);
let v2 = Vertex::new(5.0, 10.0, 0.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, 5.0);
assert!(result.is_none());
let v0 = Vertex::new(0.0, 0.0, 0.0);
let v1 = Vertex::new(10.0, 0.0, 10.0);
let v2 = Vertex::new(5.0, 10.0, 0.0);
let result = triangle_plane_intersection(&v0, &v1, &v2, f64::NAN);
assert!(result.is_none());
}
#[test]
fn test_collect_intersection_segments() {
let mut mesh = Mesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(10.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(10.0, 10.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 10.0, 0.0)); mesh.vertices.push(Vertex::new(5.0, 5.0, 10.0));
mesh.triangles.push(Triangle::new(0, 2, 1));
mesh.triangles.push(Triangle::new(0, 3, 2));
mesh.triangles.push(Triangle::new(0, 1, 4));
mesh.triangles.push(Triangle::new(1, 2, 4));
mesh.triangles.push(Triangle::new(2, 3, 4));
mesh.triangles.push(Triangle::new(3, 0, 4));
let segments = collect_intersection_segments(&mesh, 5.0);
assert_eq!(segments.len(), 4);
}
#[test]
fn test_assemble_contours_simple_square() {
let segments = vec![
((0.0, 0.0), (1.0, 0.0)),
((1.0, 0.0), (1.0, 1.0)),
((1.0, 1.0), (0.0, 1.0)),
((0.0, 1.0), (0.0, 0.0)),
];
let contours = assemble_contours(segments, 1e-6);
assert_eq!(contours.len(), 1);
assert_eq!(contours[0].len(), 4);
}
#[test]
fn test_assemble_contours_unordered_segments() {
let segments = vec![
((1.0, 1.0), (0.0, 1.0)),
((0.0, 0.0), (1.0, 0.0)),
((0.0, 1.0), (0.0, 0.0)),
((1.0, 0.0), (1.0, 1.0)),
];
let contours = assemble_contours(segments, 1e-6);
assert_eq!(contours.len(), 1);
assert_eq!(contours[0].len(), 4);
}
#[test]
fn test_assemble_contours_multiple_loops() {
let segments = vec![
((0.0, 0.0), (1.0, 0.0)),
((1.0, 0.0), (1.0, 1.0)),
((1.0, 1.0), (0.0, 1.0)),
((0.0, 1.0), (0.0, 0.0)),
((5.0, 5.0), (6.0, 5.0)),
((6.0, 5.0), (6.0, 6.0)),
((6.0, 6.0), (5.0, 6.0)),
((5.0, 6.0), (5.0, 5.0)),
];
let contours = assemble_contours(segments, 1e-6);
assert_eq!(contours.len(), 2);
for contour in &contours {
assert_eq!(contour.len(), 4);
}
}
#[test]
fn test_point_distance() {
let p1 = (0.0, 0.0);
let p2 = (3.0, 4.0);
let dist = point_distance(p1, p2);
assert!((dist - 5.0).abs() < 1e-10); }
}