Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propagate errors from mesh creation #262

Merged
merged 6 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/parry2d/examples/project_point2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
4 changes: 2 additions & 2 deletions crates/parry2d/tests/query/point_composite_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion crates/parry3d/benches/common/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ pub fn generate_trimesh_around_origin<R: Rng>(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()
}
2 changes: 1 addition & 1 deletion crates/parry3d/examples/mesh3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion crates/parry3d/examples/plane_intersection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion crates/parry3d/examples/project_point3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion crates/parry3d/tests/geometry/trimesh_intersection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn build_diamond(position: &Isometry<Real>) -> TriMesh {
[1, 4, 3],
];

TriMesh::new(points, indices)
TriMesh::new(points, indices).unwrap()
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/parry3d/tests/geometry/trimesh_trimesh_toi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Real> {
Expand Down
31 changes: 16 additions & 15 deletions src/query/split/split_trimesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -612,7 +612,7 @@ impl TriMesh {
aabb: &Aabb,
flip_cuboid: bool,
epsilon: Real,
) -> Option<Self> {
) -> Result<Option<Self>, MeshIntersectionError> {
let cuboid = Cuboid::new(aabb.half_extents());
let cuboid_pos = Isometry::from(aabb.center());
self.intersection_with_cuboid(
Expand All @@ -634,7 +634,7 @@ impl TriMesh {
cuboid_position: &Isometry<Real>,
flip_cuboid: bool,
epsilon: Real,
) -> Option<Self> {
) -> Result<Option<Self>, MeshIntersectionError> {
self.intersection_with_local_cuboid(
flip_mesh,
cuboid,
Expand All @@ -652,25 +652,26 @@ impl TriMesh {
cuboid_position: &Isometry<Real>,
flip_cuboid: bool,
_epsilon: Real,
) -> Option<Self> {
) -> Result<Option<Self>, 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);
Expand All @@ -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
Expand Down Expand Up @@ -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
}
})
}
}
15 changes: 11 additions & 4 deletions src/shape/shared_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Shape>);
Expand Down Expand Up @@ -194,8 +196,11 @@ impl SharedShape {
}

/// Initializes a triangle mesh shape defined by its vertex and index buffers.
pub fn trimesh(vertices: Vec<Point<Real>>, indices: Vec<[u32; 3]>) -> Self {
SharedShape(Arc::new(TriMesh::new(vertices, indices)))
pub fn trimesh(
vertices: Vec<Point<Real>>,
indices: Vec<[u32; 3]>,
) -> Result<Self, TriMeshBuilderError> {
Ok(SharedShape(Arc::new(TriMesh::new(vertices, indices)?)))
}

/// Initializes a triangle mesh shape defined by its vertex and index buffers and
Expand All @@ -204,8 +209,10 @@ impl SharedShape {
vertices: Vec<Point<Real>>,
indices: Vec<[u32; 3]>,
flags: TriMeshFlags,
) -> Self {
SharedShape(Arc::new(TriMesh::with_flags(vertices, indices, flags)))
) -> Result<Self, TriMeshBuilderError> {
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
Expand Down
65 changes: 38 additions & 27 deletions src/shape/trimesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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<Point<Real>>, indices: Vec<[u32; 3]>) -> Self {
pub fn new(
vertices: Vec<Point<Real>>,
indices: Vec<[u32; 3]>,
) -> Result<Self, TriMeshBuilderError> {
Self::with_flags(vertices, indices, TriMeshFlags::empty())
}

Expand All @@ -264,11 +263,10 @@ impl TriMesh {
vertices: Vec<Point<Real>>,
indices: Vec<[u32; 3]>,
flags: TriMeshFlags,
) -> Self {
assert!(
!indices.is_empty(),
"A triangle mesh must contain at least one triangle."
);
) -> Result<Self, TriMeshBuilderError> {
if indices.is_empty() {
return Err(TriMeshBuilderError::EmptyIndices);
}

let mut result = Self {
qbvh: Qbvh::new(),
Expand All @@ -288,7 +286,7 @@ impl TriMesh {
result.rebuild_qbvh();
}

result
Ok(result)
}

/// Sets the flags of this triangle mesh, controlling its optional associated data.
Expand Down Expand Up @@ -415,15 +413,15 @@ 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.
///
/// 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<Point<Real>>) -> Option<Self> {
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.
Expand Down Expand Up @@ -970,15 +968,15 @@ impl TriMesh {
impl From<crate::shape::HeightField> for TriMesh {
fn from(heightfield: crate::shape::HeightField) -> Self {
let (vtx, idx) = heightfield.to_trimesh();
TriMesh::new(vtx, idx)
TriMesh::new(vtx, idx).unwrap()
}
}

#[cfg(feature = "dim3")]
impl From<Cuboid> for TriMesh {
fn from(cuboid: Cuboid) -> Self {
let (vtx, idx) = cuboid.to_trimesh();
TriMesh::new(vtx, idx)
TriMesh::new(vtx, idx).unwrap()
}
}

Expand Down Expand Up @@ -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."
);
}
}
2 changes: 1 addition & 1 deletion src/transformation/mesh_intersection/mesh_intersection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading