diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c1cf5c4..df312927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ - Implement `::to_trimesh` in 2d for `Cuboid` and `Aabb`. - Implement `Shape::feature_normal_at_point` for `TriMesh` to retrieve the normal of a face, when passing a `FeatureId::Face`. +### Modified + +- Propagate error information while creating a mesh and using functions making use of it (See #262): + - `TriMesh::new` + - `TriMesh::intersection_with_aabb` + - `SharedShape::trimesh` + - `SharedShape::trimesh_with_flags` + ## v0.17.1 ### Modified diff --git a/crates/parry2d/examples/project_point2d.rs b/crates/parry2d/examples/project_point2d.rs index fc2a9884..f65a9518 100644 --- a/crates/parry2d/examples/project_point2d.rs +++ b/crates/parry2d/examples/project_point2d.rs @@ -22,7 +22,7 @@ async fn main() { let scale = 200f32; let (points, indices) = Cuboid::new(Vector2::new(0.2 * scale, 0.5 * scale)).to_trimesh(); - let trimesh = TriMesh::with_flags(points, indices, TriMeshFlags::ORIENTED); + let trimesh = TriMesh::with_flags(points, indices, TriMeshFlags::ORIENTED).unwrap(); for _i in 1.. { clear_background(BLACK); diff --git a/crates/parry2d/tests/query/point_composite_shape.rs b/crates/parry2d/tests/query/point_composite_shape.rs index 0f50f140..dd113714 100644 --- a/crates/parry2d/tests/query/point_composite_shape.rs +++ b/crates/parry2d/tests/query/point_composite_shape.rs @@ -10,7 +10,7 @@ fn project_local_point_and_get_feature_gets_the_enclosing_triangle() { Point2::new(1.0, 1.0), ]; - let mesh = TriMesh::new(vertices, vec![[0, 1, 2], [3, 0, 2]]); + let mesh = TriMesh::new(vertices, vec![[0, 1, 2], [3, 0, 2]]).unwrap(); let query_pt = Point2::new(0.6, 0.6); // Inside the top-right triangle (index 1) let (proj, feat) = mesh.project_local_point_and_get_feature(&query_pt); @@ -34,7 +34,7 @@ fn project_local_point_and_get_feature_projects_correctly_from_outside() { Point2::new(1.0, 1.0), ]; - let mesh = TriMesh::new(vertices, vec![[0, 1, 2], [3, 0, 2]]); + let mesh = TriMesh::new(vertices, vec![[0, 1, 2], [3, 0, 2]]).unwrap(); { let query_pt = Point2::new(-1.0, 0.0); // Left from the bottom-left triangle (index 0) diff --git a/crates/parry3d/benches/common/generators.rs b/crates/parry3d/benches/common/generators.rs index a1a5f61c..4ff7dc43 100644 --- a/crates/parry3d/benches/common/generators.rs +++ b/crates/parry3d/benches/common/generators.rs @@ -9,5 +9,5 @@ pub fn generate_trimesh_around_origin(rng: &mut R) -> TriMesh { .map(|i| Point3::new(i * 3, i * 3 + 1, i * 3 + 2)) .collect(); - TriMesh::new(pts, indices, Some(uvs)) + TriMesh::new(pts, indices, Some(uvs)).unwrap() } diff --git a/crates/parry3d/examples/mesh3d.rs b/crates/parry3d/examples/mesh3d.rs index e32e1424..aeb26eb0 100644 --- a/crates/parry3d/examples/mesh3d.rs +++ b/crates/parry3d/examples/mesh3d.rs @@ -14,7 +14,7 @@ fn main() { let indices = vec![[0u32, 1, 2], [0, 2, 3], [0, 3, 1]]; // Build the mesh. - let mesh = TriMesh::new(points, indices); + let mesh = TriMesh::new(points, indices).unwrap(); assert!(mesh.vertices().len() == 4); } diff --git a/crates/parry3d/examples/plane_intersection.rs b/crates/parry3d/examples/plane_intersection.rs index 47f45e69..2d7f284f 100644 --- a/crates/parry3d/examples/plane_intersection.rs +++ b/crates/parry3d/examples/plane_intersection.rs @@ -14,7 +14,7 @@ async fn main() { let camera_pos = Vec3::new(-1.5f32, 2.5f32, -3f32); let mesh = mquad_mesh_from_points(&trimesh, light_pos, DARKGRAY); - let trimesh = TriMesh::new(trimesh.0, trimesh.1); + let trimesh = TriMesh::new(trimesh.0, trimesh.1).unwrap(); for _ in 1.. { clear_background(BLACK); diff --git a/crates/parry3d/examples/project_point3d.rs b/crates/parry3d/examples/project_point3d.rs index 31e745a6..ac3025af 100644 --- a/crates/parry3d/examples/project_point3d.rs +++ b/crates/parry3d/examples/project_point3d.rs @@ -16,7 +16,7 @@ async fn main() { Color::from_rgba(200, 200, 200, 150), ); let (points, indices) = trimesh; - let trimesh = TriMesh::with_flags(points, indices, TriMeshFlags::ORIENTED); + let trimesh = TriMesh::with_flags(points, indices, TriMeshFlags::ORIENTED).unwrap(); for _i in 1.. { clear_background(BLACK); diff --git a/crates/parry3d/tests/geometry/trimesh_connected_components.rs b/crates/parry3d/tests/geometry/trimesh_connected_components.rs index bfaf5789..03216020 100644 --- a/crates/parry3d/tests/geometry/trimesh_connected_components.rs +++ b/crates/parry3d/tests/geometry/trimesh_connected_components.rs @@ -15,7 +15,7 @@ fn mesh_connected_components_grouped_faces() { Point::new(15.82, 6.4, 0.0), ]; - let mut roof = TriMesh::new(verts, vec![[0, 1, 2], [3, 4, 5]]); + let mut roof = TriMesh::new(verts, vec![[0, 1, 2], [3, 4, 5]]).unwrap(); if let Err(e) = roof.set_flags(TriMeshFlags::MERGE_DUPLICATE_VERTICES | TriMeshFlags::CONNECTED_COMPONENTS) diff --git a/crates/parry3d/tests/geometry/trimesh_intersection.rs b/crates/parry3d/tests/geometry/trimesh_intersection.rs index 78cf096a..94df835c 100644 --- a/crates/parry3d/tests/geometry/trimesh_intersection.rs +++ b/crates/parry3d/tests/geometry/trimesh_intersection.rs @@ -22,7 +22,7 @@ fn build_diamond(position: &Isometry) -> TriMesh { [1, 4, 3], ]; - TriMesh::new(points, indices) + TriMesh::new(points, indices).unwrap() } #[test] diff --git a/crates/parry3d/tests/geometry/trimesh_trimesh_toi.rs b/crates/parry3d/tests/geometry/trimesh_trimesh_toi.rs index 22493edb..ae08c636 100644 --- a/crates/parry3d/tests/geometry/trimesh_trimesh_toi.rs +++ b/crates/parry3d/tests/geometry/trimesh_trimesh_toi.rs @@ -15,7 +15,7 @@ fn build_pyramid() -> TriMesh { let indices = vec![[0u32, 1, 2], [0, 2, 3], [0, 3, 1]]; - TriMesh::new(points, indices) + TriMesh::new(points, indices).unwrap() } fn do_toi_test() -> Option { diff --git a/src/query/split/split_trimesh.rs b/src/query/split/split_trimesh.rs index b4c3b166..fa0e96df 100644 --- a/src/query/split/split_trimesh.rs +++ b/src/query/split/split_trimesh.rs @@ -3,7 +3,7 @@ use crate::math::{Isometry, Point, Real, UnitVector, Vector}; use crate::query::visitors::BoundingVolumeIntersectionsVisitor; use crate::query::{IntersectResult, PointQuery, SplitResult}; use crate::shape::{Cuboid, FeatureId, Polyline, Segment, Shape, TriMesh, TriMeshFlags, Triangle}; -use crate::transformation; +use crate::transformation::{intersect_meshes, MeshIntersectionError}; use crate::utils::{hashmap::HashMap, SortedPair, WBasis}; use spade::{handles::FixedVertexHandle, ConstrainedDelaunayTriangulation, Triangulation as _}; use std::cmp::Ordering; @@ -339,8 +339,8 @@ impl TriMesh { } else if indices_lhs.is_empty() { SplitResult::Positive } else { - let mesh_lhs = TriMesh::new(vertices_lhs, indices_lhs); - let mesh_rhs = TriMesh::new(vertices_rhs, indices_rhs); + let mesh_lhs = TriMesh::new(vertices_lhs, indices_lhs).unwrap(); + let mesh_rhs = TriMesh::new(vertices_rhs, indices_rhs).unwrap(); SplitResult::Pair(mesh_lhs, mesh_rhs) } } @@ -612,7 +612,7 @@ impl TriMesh { aabb: &Aabb, flip_cuboid: bool, epsilon: Real, - ) -> Option { + ) -> Result, MeshIntersectionError> { let cuboid = Cuboid::new(aabb.half_extents()); let cuboid_pos = Isometry::from(aabb.center()); self.intersection_with_cuboid( @@ -634,7 +634,7 @@ impl TriMesh { cuboid_position: &Isometry, flip_cuboid: bool, epsilon: Real, - ) -> Option { + ) -> Result, MeshIntersectionError> { self.intersection_with_local_cuboid( flip_mesh, cuboid, @@ -652,25 +652,26 @@ impl TriMesh { cuboid_position: &Isometry, flip_cuboid: bool, _epsilon: Real, - ) -> Option { + ) -> Result, MeshIntersectionError> { if self.topology().is_some() && self.pseudo_normals().is_some() { let (cuboid_vtx, cuboid_idx) = cuboid.to_trimesh(); let cuboid_trimesh = TriMesh::with_flags( cuboid_vtx, cuboid_idx, TriMeshFlags::HALF_EDGE_TOPOLOGY | TriMeshFlags::ORIENTED, - ); + ) + // `TriMesh::with_flags` can't fail for `cuboid.to_trimesh()`. + .unwrap(); - return transformation::intersect_meshes( + let intersect_meshes = intersect_meshes( &Isometry::identity(), self, flip_mesh, cuboid_position, &cuboid_trimesh, flip_cuboid, - ) - .ok() - .flatten(); + ); + return intersect_meshes; } let cuboid_aabb = cuboid.compute_aabb(cuboid_position); @@ -682,7 +683,7 @@ impl TriMesh { let _ = self.qbvh().traverse_depth_first(&mut visitor); if intersecting_tris.is_empty() { - return None; + return Ok(None); } // First, very naive version that outputs a triangle soup without @@ -730,10 +731,10 @@ impl TriMesh { *pt = cuboid_position * *pt; } - if new_vertices.len() >= 3 { - Some(TriMesh::new(new_vertices, new_indices)) + Ok(if new_vertices.len() >= 3 { + Some(TriMesh::new(new_vertices, new_indices).unwrap()) } else { None - } + }) } } diff --git a/src/shape/shared_shape.rs b/src/shape/shared_shape.rs index 7add13b7..eb2beeff 100644 --- a/src/shape/shared_shape.rs +++ b/src/shape/shared_shape.rs @@ -17,6 +17,8 @@ use std::fmt; use std::ops::Deref; use std::sync::Arc; +use super::TriMeshBuilderError; + /// The shape of a collider. #[derive(Clone)] pub struct SharedShape(pub Arc); @@ -194,8 +196,11 @@ impl SharedShape { } /// Initializes a triangle mesh shape defined by its vertex and index buffers. - pub fn trimesh(vertices: Vec>, indices: Vec<[u32; 3]>) -> Self { - SharedShape(Arc::new(TriMesh::new(vertices, indices))) + pub fn trimesh( + vertices: Vec>, + indices: Vec<[u32; 3]>, + ) -> Result { + Ok(SharedShape(Arc::new(TriMesh::new(vertices, indices)?))) } /// Initializes a triangle mesh shape defined by its vertex and index buffers and @@ -204,8 +209,10 @@ impl SharedShape { vertices: Vec>, indices: Vec<[u32; 3]>, flags: TriMeshFlags, - ) -> Self { - SharedShape(Arc::new(TriMesh::with_flags(vertices, indices, flags))) + ) -> Result { + Ok(SharedShape(Arc::new(TriMesh::with_flags( + vertices, indices, flags, + )?))) } /// Initializes a compound shape obtained from the decomposition of the given trimesh (in 3D) or diff --git a/src/shape/trimesh.rs b/src/shape/trimesh.rs index 140ecf2f..32569a28 100644 --- a/src/shape/trimesh.rs +++ b/src/shape/trimesh.rs @@ -22,11 +22,13 @@ use crate::query::details::NormalConstraints; use rkyv::{bytecheck, CheckBytes}; /// Indicated an inconsistency in the topology of a triangle mesh. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(thiserror::Error, Copy, Clone, Debug, PartialEq, Eq)] pub enum TopologyError { /// Found a triangle with two or three identical vertices. + #[error("the triangle {0} has at least two identical vertices.")] BadTriangle(u32), /// At least two adjacent triangles have opposite orientations. + #[error("the triangles {triangle1} and {triangle2} sharing the edge {edge:?} have opposite orientations.")] BadAdjacentTrianglesOrientation { /// The first triangle, with an orientation opposite to the second triangle. triangle1: u32, @@ -37,23 +39,17 @@ pub enum TopologyError { }, } -impl fmt::Display for TopologyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::BadTriangle(fid) => { - f.pad(&format!("the triangle {fid} has at least two identical vertices.")) - } - Self::BadAdjacentTrianglesOrientation { - triangle1, - triangle2, - edge, - } => f.pad(&format!("the triangles {triangle1} and {triangle2} sharing the edge {:?} have opposite orientations.", edge)), - } - } +/// Indicated an inconsistency while building a triangle mesh. +#[derive(thiserror::Error, Copy, Clone, Debug, PartialEq, Eq)] +pub enum TriMeshBuilderError { + /// A triangle mesh must contain at least one triangle. + #[error("A triangle mesh must contain at least one triangle.")] + EmptyIndices, + /// Indicated an inconsistency in the topology of a triangle mesh. + #[error("Topology Error: {0}")] + TopologyError(TopologyError), } -impl std::error::Error for TopologyError {} - /// The set of pseudo-normals of a triangle mesh. /// /// These pseudo-normals are used for the inside-outside test of a @@ -255,7 +251,10 @@ impl fmt::Debug for TriMesh { impl TriMesh { /// Creates a new triangle mesh from a vertex buffer and an index buffer. - pub fn new(vertices: Vec>, indices: Vec<[u32; 3]>) -> Self { + pub fn new( + vertices: Vec>, + indices: Vec<[u32; 3]>, + ) -> Result { Self::with_flags(vertices, indices, TriMeshFlags::empty()) } @@ -264,11 +263,10 @@ impl TriMesh { vertices: Vec>, indices: Vec<[u32; 3]>, flags: TriMeshFlags, - ) -> Self { - assert!( - !indices.is_empty(), - "A triangle mesh must contain at least one triangle." - ); + ) -> Result { + if indices.is_empty() { + return Err(TriMeshBuilderError::EmptyIndices); + } let mut result = Self { qbvh: Qbvh::new(), @@ -288,7 +286,7 @@ impl TriMesh { result.rebuild_qbvh(); } - result + Ok(result) } /// Sets the flags of this triangle mesh, controlling its optional associated data. @@ -415,7 +413,7 @@ impl TriMesh { let vertices = std::mem::take(&mut self.vertices); let indices = std::mem::take(&mut self.indices); - *self = TriMesh::with_flags(vertices, indices, self.flags); + *self = TriMesh::with_flags(vertices, indices, self.flags).unwrap(); } /// Create a `TriMesh` from a set of points assumed to describe a counter-clockwise non-convex polygon. @@ -423,7 +421,7 @@ impl TriMesh { /// This operation may fail if the input polygon is invalid, e.g. it is non-simple or has zero surface area. #[cfg(feature = "dim2")] pub fn from_polygon(vertices: Vec>) -> Option { - triangulate_ear_clipping(&vertices).map(|indices| Self::new(vertices, indices)) + triangulate_ear_clipping(&vertices).map(|indices| Self::new(vertices, indices).unwrap()) } /// A flat view of the index buffer of this mesh. @@ -970,7 +968,7 @@ impl TriMesh { impl From for TriMesh { fn from(heightfield: crate::shape::HeightField) -> Self { let (vtx, idx) = heightfield.to_trimesh(); - TriMesh::new(vtx, idx) + TriMesh::new(vtx, idx).unwrap() } } @@ -978,7 +976,7 @@ impl From for TriMesh { impl From for TriMesh { fn from(cuboid: Cuboid) -> Self { let (vtx, idx) = cuboid.to_trimesh(); - TriMesh::new(vtx, idx) + TriMesh::new(vtx, idx).unwrap() } } @@ -1041,3 +1039,16 @@ impl TypedSimdCompositeShape for TriMesh { &self.qbvh } } + +#[cfg(test)] +mod test { + use crate::shape::{TriMesh, TriMeshFlags}; + + #[test] + fn trimesh_error_empty_indices() { + assert!( + TriMesh::with_flags(vec![], vec![], TriMeshFlags::empty()).is_err(), + "A triangle mesh with no triangles is invalid." + ); + } +} diff --git a/src/transformation/mesh_intersection/mesh_intersection.rs b/src/transformation/mesh_intersection/mesh_intersection.rs index ae016f0b..bf6505d6 100644 --- a/src/transformation/mesh_intersection/mesh_intersection.rs +++ b/src/transformation/mesh_intersection/mesh_intersection.rs @@ -244,7 +244,7 @@ pub fn intersect_meshes_with_tolerances( let vertices: Vec<_> = vertices.iter().map(|p| Point3::from(p.point)).collect(); if !topology_indices.is_empty() { - Ok(Some(TriMesh::new(vertices, topology_indices))) + Ok(Some(TriMesh::new(vertices, topology_indices)?)) } else { Ok(None) } diff --git a/src/transformation/mesh_intersection/mesh_intersection_error.rs b/src/transformation/mesh_intersection/mesh_intersection_error.rs index 9132824e..13904c3b 100644 --- a/src/transformation/mesh_intersection/mesh_intersection_error.rs +++ b/src/transformation/mesh_intersection/mesh_intersection_error.rs @@ -1,30 +1,30 @@ -use core::fmt; +use crate::shape::TriMeshBuilderError; /// Error indicating that a query is not supported between certain shapes -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq)] pub enum MeshIntersectionError { + /// At least one of the meshes is missing its topology information. Call `mesh.compute_topology` on the mesh + #[error("at least one of the meshes is missing its topology information. Call `mesh.compute_topology` on the mesh")] MissingTopology, + /// At least one of the meshes is missing its pseudo-normals. Call `mesh.compute_pseudo_normals` on the mesh + #[error("at least one of the meshes is missing its pseudo-normals. Call `mesh.compute_pseudo_normals` on the mesh")] MissingPseudoNormals, + /// Internal failure while intersecting two triangles + #[error("internal failure while intersecting two triangles")] TriTriError, + /// Internal failure while merging faces resulting from intersections + #[error("internal failure while merging faces resulting from intersections")] DuplicateVertices, + /// Internal failure while triangulating an intersection face + #[error("internal failure while triangulating an intersection face")] TriangulationError, + /// See [`TriMeshBuilderError`] + #[error("TriMeshBuilderError: {0}")] + TriMeshBuilderError(TriMeshBuilderError), } -impl fmt::Display for MeshIntersectionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::MissingTopology => { - f.pad("at least one of the meshes is missing its topology information. Call `mesh.compute_topology` on the mesh") - } - Self::MissingPseudoNormals => { - f.pad("at least one of the meshes is missing its pseudo-normals. Call `mesh.compute_pseudo_normals` on the mesh") - } - Self::TriTriError => f.pad("internal failure while intersecting two triangles"), - Self::DuplicateVertices => f.pad("internal failure while merging faces resulting from intersections"), - Self::TriangulationError => f.pad("internal failure while triangulating an intersection face"), - } +impl From for MeshIntersectionError { + fn from(value: TriMeshBuilderError) -> Self { + MeshIntersectionError::TriMeshBuilderError(value) } } - -#[cfg(feature = "std")] -impl std::error::Error for MeshIntersectionError {} diff --git a/src/transformation/mod.rs b/src/transformation/mod.rs index 7b29e427..653c0c6d 100644 --- a/src/transformation/mod.rs +++ b/src/transformation/mod.rs @@ -7,7 +7,7 @@ pub use self::convex_hull2::{convex_hull2 as convex_hull, convex_hull2_idx as co #[cfg(feature = "dim3")] pub use self::convex_hull3::{check_convex_hull, convex_hull, try_convex_hull, ConvexHullError}; #[cfg(feature = "dim3")] -pub use self::mesh_intersection::intersect_meshes; +pub use self::mesh_intersection::{intersect_meshes, MeshIntersectionError}; pub use self::polygon_intersection::{ convex_polygons_intersection, convex_polygons_intersection_points, polygons_intersection, polygons_intersection_points,