Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ Cargo.lock
Makefile
.vscode
.idea
.DS_store
.DS_store
.claude
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
## Unreleased

### Modified

- Query results now identify the sub-shape they came from, through the new `SubShapeId` type alias:
`RayIntersection` and `PointProjection` gained a `subshape` field, and `Contact` and `ShapeCastHit`
gained `subshape1`/`subshape2`. A shape with no sub-shapes reports `0`.
- `QueryDispatcher::distance` and `QueryDispatcher::intersection_test`, and the `distance` and
`intersection_test` free functions, now return `ShapeDistance` and `ShapeIntersection` instead
of a bare `Real` and `bool`, so they can report the sub-shapes too. Read `.distance` or
`.intersecting` to recover the previous value.
- Query results now report the feature of the sub-shape they hit rather than one encoding that
sub-shape's index: a `TriMesh` ray-cast reports the triangle's own face, and the triangle itself
is the result's `subshape`. `TriMesh::triangle_normal` and `HeightField::convert_triangle_feature_id`
take that sub-shape index directly, and `TriMesh::is_backface` tests the triangle's own face.

### Added

- `CompoundFlags::FIX_INTERNAL_EDGES` makes a `Compound` treat the edges (2D) or faces (3D) its parts share as
Expand Down
9 changes: 6 additions & 3 deletions crates/parry2d/examples/distance_query2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ fn main() {
let ball_pos_intersecting = Pose::translation(0.0, 1.0);
let ball_pos_disjoint = Pose::translation(0.0, 3.0);

let dist_intersecting =
query::distance(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid).unwrap();
let dist_disjoint = query::distance(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid).unwrap();
let dist_intersecting = query::distance(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid)
.unwrap()
.distance;
let dist_disjoint = query::distance(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid)
.unwrap()
.distance;

assert_eq!(dist_intersecting, 0.0);
assert!(relative_eq!(dist_disjoint, 1.0, epsilon = 1.0e-7));
Expand Down
12 changes: 10 additions & 2 deletions crates/parry2d/examples/proximity_query2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ fn main() {
let ball_pos_intersecting = Pose::translation(1.0, 1.0);
let ball_pos_disjoint = Pose::translation(3.0, 3.0);

assert!(query::intersection_test(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid).unwrap());
assert!(!query::intersection_test(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid).unwrap());
assert!(
query::intersection_test(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid)
.unwrap()
.intersecting
);
assert!(
!query::intersection_test(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid)
.unwrap()
.intersecting
);
}
8 changes: 4 additions & 4 deletions crates/parry2d/tests/issue_431_cuboid_distance_asymmetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use parry2d::query::{self, ClosestPoints};
use parry2d::shape::Cuboid;

fn check_symmetric_and_exact(p1: &Pose, c1: &Cuboid, p2: &Pose, c2: &Cuboid, expected: Real) {
let d12 = query::distance(p1, c1, p2, c2).unwrap();
let d21 = query::distance(p2, c2, p1, c1).unwrap();
let d12 = query::distance(p1, c1, p2, c2).unwrap().distance;
let d21 = query::distance(p2, c2, p1, c1).unwrap().distance;

// Cross-check against the exact GJK closest points.
let gjk_dist = match query::closest_points(p1, c1, p2, c2, Real::MAX).unwrap() {
Expand Down Expand Up @@ -91,8 +91,8 @@ fn touching_and_overlapping() {

for x in [2.0, 1.5] {
let p2 = Pose::new(Vector::new(x, 0.0), 0.0);
let d12 = query::distance(&p1, &c1, &p2, &c2).unwrap();
let d21 = query::distance(&p2, &c2, &p1, &c1).unwrap();
let d12 = query::distance(&p1, &c1, &p2, &c2).unwrap().distance;
let d21 = query::distance(&p2, &c2, &p1, &c1).unwrap().distance;
assert!(d12.abs() < 1.0e-6, "expected zero distance, got {d12}");
assert!(d21.abs() < 1.0e-6, "expected zero distance, got {d21}");
}
Expand Down
12 changes: 9 additions & 3 deletions crates/parry2d/tests/query/closest_points_cuboid_cuboid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ fn closest_points_cuboid_cuboid_axis_aligned_diagonal() {
// The same failure is reachable through the public API: `query::distance`
// dispatches cuboid-cuboid pairs to `distance_cuboid_cuboid`, which is
// implemented on top of `closest_points_cuboid_cuboid`.
let dist = query::distance(&Pose::IDENTITY, &cuboid, &pos12, &cuboid).unwrap();
let dist = query::distance(&Pose::IDENTITY, &cuboid, &pos12, &cuboid)
.unwrap()
.distance;
assert_relative_eq!(dist, true_dist, epsilon = 1e-5);
}
}
Expand All @@ -60,7 +62,9 @@ fn closest_points_cuboid_cuboid_axis_aligned_corner_touching() {
other => panic!("expected WithinMargin at distance {gap}, got {other:?}"),
}

let dist = query::distance(&Pose::IDENTITY, &cuboid, &pos12, &cuboid).unwrap();
let dist = query::distance(&Pose::IDENTITY, &cuboid, &pos12, &cuboid)
.unwrap()
.distance;
assert_relative_eq!(dist, gap, epsilon = 1e-5);
}

Expand Down Expand Up @@ -89,6 +93,8 @@ fn closest_points_cuboid_cuboid_axis_aligned_subulp_gap() {
ClosestPoints::Disjoint => panic!("expected WithinMargin, got Disjoint"),
}

let dist = query::distance(&Pose::IDENTITY, &c1, &pos12, &c2).unwrap();
let dist = query::distance(&Pose::IDENTITY, &c1, &pos12, &c2)
.unwrap()
.distance;
assert!(dist < 1.0e-5, "distance = {dist}");
}
12 changes: 6 additions & 6 deletions crates/parry2d/tests/query/point_composite_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn project_local_point_and_get_feature_gets_the_enclosing_triangle() {
let mesh = TriMesh::new(vertices, vec![[0, 1, 2], [3, 0, 2]]).unwrap();
let query_pt = Vector::new(0.6, 0.6); // Inside the top-right triangle (index 1)

let (proj, feat) = mesh.project_local_point_and_get_feature(query_pt);
let (proj, _feat) = mesh.project_local_point_and_get_feature(query_pt);

let correct_tri_idx = 1;
let correct_tri = mesh.triangle(correct_tri_idx);
Expand All @@ -22,7 +22,7 @@ fn project_local_point_and_get_feature_gets_the_enclosing_triangle() {

assert!(is_inside_correct);
assert_eq!(proj.is_inside, is_inside_correct);
assert_eq!(feat.unwrap_face(), correct_tri_idx);
assert_eq!(proj.subshape, correct_tri_idx);
}

#[test]
Expand All @@ -39,7 +39,7 @@ fn project_local_point_and_get_feature_projects_correctly_from_outside() {
{
let query_pt = Vector::new(-1.0, 0.0); // Left from the bottom-left triangle (index 0)

let (proj, feat) = mesh.project_local_point_and_get_feature(query_pt);
let (proj, _feat) = mesh.project_local_point_and_get_feature(query_pt);

let correct_tri_idx = 0;
let correct_tri = mesh.triangle(correct_tri_idx);
Expand All @@ -49,12 +49,12 @@ fn project_local_point_and_get_feature_projects_correctly_from_outside() {
assert_eq!(is_inside_correct, false);
assert_eq!(proj.is_inside, is_inside_correct);
assert_eq!(proj.point, Vector::ZERO);
assert_eq!(feat.unwrap_face(), correct_tri_idx);
assert_eq!(proj.subshape, correct_tri_idx);
}
{
let query_pt = Vector::new(0.5, 2.0); // Above the top-right triangle (index 1)

let (proj, feat) = mesh.project_local_point_and_get_feature(query_pt);
let (proj, _feat) = mesh.project_local_point_and_get_feature(query_pt);

let correct_tri_idx = 1;
let correct_tri = mesh.triangle(correct_tri_idx);
Expand All @@ -64,6 +64,6 @@ fn project_local_point_and_get_feature_projects_correctly_from_outside() {
assert_eq!(is_inside_correct, false);
assert_eq!(proj.is_inside, is_inside_correct);
assert_eq!(proj.point, Vector::new(0.5, 1.0));
assert_eq!(feat.unwrap_face(), correct_tri_idx);
assert_eq!(proj.subshape, correct_tri_idx);
}
}
160 changes: 160 additions & 0 deletions crates/parry2d/tests/sub_shape_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use parry2d::math::{Pose, Real, Vector};
use parry2d::query::{self, PointQuery, Ray, RayCast};
use parry2d::shape::{Ball, Compound, Cuboid, Polyline, SharedShape};

/// Three unit boxes in a row along x, centered at x = 0, 4 and 8.
fn three_boxes() -> Compound {
Compound::new(
(0..3)
.map(|i| {
(
Pose::from_translation(Vector::new(i as Real * 4.0, 0.0)),
SharedShape::new(Cuboid::new(Vector::splat(0.5))),
)
})
.collect(),
)
}

#[test]
fn queries_against_a_compound_report_the_part() {
let compound = three_boxes();
let probe = Ball::new(0.25);

for part in 0..3u32 {
let x = part as Real * 4.0;

let ray = Ray::new(Vector::new(x, 5.0), Vector::new(0.0, -1.0));
assert_eq!(
compound
.cast_local_ray_and_get_normal(&ray, Real::MAX, true)
.expect("hits a box")
.subshape,
part
);

assert_eq!(
compound
.project_local_point(Vector::new(x, 3.0), false)
.subshape,
part
);

let pose12 = Pose::from_translation(Vector::new(x, 0.7));
let contact = query::contact(&Pose::IDENTITY, &compound, &pose12, &probe, 1.0)
.unwrap()
.expect("within prediction");
assert_eq!((contact.subshape1, contact.subshape2), (part, 0));

let dist = query::distance(&Pose::IDENTITY, &compound, &pose12, &probe).unwrap();
assert_eq!((dist.subshape1, dist.subshape2), (part, 0));

let overlapping = Pose::from_translation(Vector::new(x, 0.0));
let test =
query::intersection_test(&Pose::IDENTITY, &compound, &overlapping, &probe).unwrap();
assert!(test.intersecting);
assert_eq!((test.subshape1, test.subshape2), (part, 0));
}
}

#[test]
fn a_polyline_reports_the_segment() {
// A staircase: segment 0 spans x in [0,1], segment 1 [1,2], segment 2 [2,3].
let polyline = Polyline::new(
vec![
Vector::new(0.0, 0.0),
Vector::new(1.0, 0.0),
Vector::new(2.0, 0.0),
Vector::new(3.0, 0.0),
],
None,
);

for segment in 0..3u32 {
let x = segment as Real + 0.5;
assert_eq!(
polyline
.project_local_point(Vector::new(x, 2.0), false)
.subshape,
segment
);

let ray = Ray::new(Vector::new(x, 2.0), Vector::new(0.0, -1.0));
assert_eq!(
polyline
.cast_local_ray_and_get_normal(&ray, Real::MAX, true)
.expect("hits the polyline")
.subshape,
segment
);
}
}

/// The conversion resolves a segment endpoint to the polyline vertex it indexes, so the two
/// segments meeting at a corner name the same vertex.
#[test]
fn segment_features_convert_to_polyline_features() {
use parry2d::shape::FeatureId;

// Segment 0 spans vertices 0-1, segment 1 spans 1-2: they share vertex 1.
let polyline = Polyline::new(
vec![
Vector::new(0.0, 0.0),
Vector::new(1.0, 0.0),
Vector::new(2.0, 0.0),
],
None,
);

// Endpoint 1 of segment 0 and endpoint 0 of segment 1 are the same polyline vertex.
assert_eq!(
polyline.segment_feature_to_polyline_feature(0, FeatureId::Vertex(1)),
FeatureId::Vertex(1)
);
assert_eq!(
polyline.segment_feature_to_polyline_feature(1, FeatureId::Vertex(0)),
FeatureId::Vertex(1)
);
assert_eq!(
polyline.segment_feature_to_polyline_feature(1, FeatureId::Vertex(1)),
FeatureId::Vertex(2)
);

// Each segment has a side facing each way, and every (segment, side) pair is distinct.
let sides: Vec<_> = (0..2u32)
.flat_map(|segment| (0..2u32).map(move |side| (segment, side)))
.map(|(segment, side)| {
polyline.segment_feature_to_polyline_feature(segment, FeatureId::Face(side))
})
.collect();
for (i, face) in sides.iter().enumerate() {
assert!(!sides[..i].contains(face), "{face:?} reused");
}
}

#[test]
fn shapes_without_sub_shapes_report_zero() {
let ball = Ball::new(1.0);
let ray = Ray::new(Vector::new(0.0, 5.0), Vector::new(0.0, -1.0));

assert_eq!(
ball.cast_local_ray_and_get_normal(&ray, Real::MAX, true)
.unwrap()
.subshape,
0
);
assert_eq!(
ball.project_local_point(Vector::new(3.0, 0.0), false)
.subshape,
0
);

let dist = query::distance(
&Pose::IDENTITY,
&ball,
&Pose::from_translation(Vector::new(5.0, 0.0)),
&Ball::new(1.0),
)
.unwrap();
assert_eq!((dist.subshape1, dist.subshape2), (0, 0));
}
9 changes: 6 additions & 3 deletions crates/parry3d/examples/distance_query3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ fn main() {
let ball_pos_intersecting = Pose::translation(0.0, 1.0, 0.0);
let ball_pos_disjoint = Pose::translation(0.0, 3.0, 0.0);

let dist_intersecting =
query::distance(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid).unwrap();
let dist_disjoint = query::distance(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid).unwrap();
let dist_intersecting = query::distance(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid)
.unwrap()
.distance;
let dist_disjoint = query::distance(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid)
.unwrap()
.distance;

assert_eq!(dist_intersecting, 0.0);
assert!(relative_eq!(dist_disjoint, 1.0, epsilon = 1.0e-7));
Expand Down
8 changes: 6 additions & 2 deletions crates/parry3d/examples/proximity_query3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ fn main() {
let ball_pos_disjoint = Pose::translation(3.0, 3.0, 3.0);

let intersecting =
query::intersection_test(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid).unwrap();
query::intersection_test(&ball_pos_intersecting, &ball, &cuboid_pos, &cuboid)
.unwrap()
.intersecting;
let not_intersecting =
!query::intersection_test(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid).unwrap();
!query::intersection_test(&ball_pos_disjoint, &ball, &cuboid_pos, &cuboid)
.unwrap()
.intersecting;

assert!(intersecting);
assert!(not_intersecting);
Expand Down
2 changes: 1 addition & 1 deletion crates/parry3d/tests/issue_157_frustum_contact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,5 @@ fn convex_polyhedra_intersection() {
.unwrap();

assert!(num_contained_points == 4);
assert!(intersects);
assert!(intersects.intersecting);
}
Loading
Loading