Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
00aed58
fix: clamp bin indices in the binned BVH builder
sebcrozet Aug 7, 2026
5706e96
fix: handle degenerate arcs in round shape outlines
sebcrozet Aug 7, 2026
b3ee249
fix: prune HeightField::project_local_point instead of iterating on e…
sebcrozet Aug 7, 2026
0f70b0c
test: check ball-vs-polyline endpoint contacts have a non-zero normal
sebcrozet Aug 7, 2026
7f8912f
docs: clarify the frame conventions of ShapeCastHit fields
sebcrozet Aug 7, 2026
6eb9edc
fix: refine the shape-cast TOI lower bound at the GJK stagnation exit…
sebcrozet Aug 7, 2026
171e99b
fix: don't report disjoint shapes when the GJK simplex stalls without…
sebcrozet Aug 7, 2026
380dc0c
test: capsule/ball-vs-large-cuboid intersection sweep has no false ne…
sebcrozet Aug 7, 2026
79833ff
fix: scale the GJK ray-cast full-simplex rejection tolerance with the…
sebcrozet Aug 7, 2026
febcea4
fix: only report PenetratingOrWithinTargetDist for actual penetration…
sebcrozet Aug 7, 2026
a9ec9f0
fix: handle uncertified GJK stalls in exact-distance and shape-cast q…
sebcrozet Aug 7, 2026
1f02003
test: ShapeCastOptions::target_distance reports hits at the threshold…
sebcrozet Aug 7, 2026
341b792
test: frustum-vs-frustum contact and intersection queries succeed
sebcrozet Aug 7, 2026
8c6fc9e
chore: rustfmt the new GJK helper and regression tests
sebcrozet Aug 7, 2026
24aa82b
fix: compute Triangle::area from the cross product instead of Kahan's…
sebcrozet Aug 7, 2026
789c644
fix: project points on the longest edge of degenerate triangles
sebcrozet Aug 7, 2026
f9c6bb9
fix: classify exact segment endpoints as OnVertex in segments_interse…
sebcrozet Aug 7, 2026
9af7d76
fix: don't panic on composite-shape point projection with non-finite …
sebcrozet Aug 7, 2026
05efda2
test: check distance to a 1e-10-scaled cube TriMesh doesn't panic
sebcrozet Aug 7, 2026
66f7db5
test: check cylinder-vs-trimesh closest_points beyond max_dist return…
sebcrozet Aug 7, 2026
2a4817c
test: check plane intersection with a flat two-triangle quad terminates
sebcrozet Aug 7, 2026
139d924
feat: implement an informative Debug for TriMesh
sebcrozet Aug 7, 2026
5091d87
test: check the full trimesh inertia tensor against the analytic cubo…
sebcrozet Aug 7, 2026
302c744
fix: return voxels shape-cast witness1 in the voxels shape's local frame
sebcrozet Aug 7, 2026
654df5d
docs: voxel iterators only yield non-empty voxels
sebcrozet Aug 7, 2026
22cbc87
test: voxels contact manifolds at the domain minimum don't overflow t…
sebcrozet Aug 7, 2026
45b7115
fix: loosen the voxels-vs-shape manifold AABB by the prediction distance
sebcrozet Aug 7, 2026
5a836cf
test: composite-vs-shape contact agrees with the flipped shape order
sebcrozet Aug 7, 2026
c4bdcf8
test: in-place Bvh rebuild tick-loop with a reused workspace
sebcrozet Aug 7, 2026
556fa25
fix: require spade 2.15, the minimum version providing try_bulk_load_cdt
sebcrozet Aug 7, 2026
c78269a
docs: floating-point caveat on BoundingVolume merge/merged containment
sebcrozet Aug 7, 2026
b006a22
fix: use O(1) swap_remove in hertel_mehlhorn instead of O(n) remove
sebcrozet Aug 7, 2026
917ac43
test: check cuboid-cuboid distance doesn't depend on the arguments order
sebcrozet Aug 7, 2026
762e161
fix: return real witness points/normal for exactly-touching contacts …
sebcrozet Aug 7, 2026
7ddc2ed
fix: support heightfields in the contact() query
sebcrozet Aug 7, 2026
5e35b29
fix: orient the cylinder/cone cap contact-feature approximation towar…
sebcrozet Aug 7, 2026
ae736c9
chore: rustfmt the triage regression tests
sebcrozet Aug 7, 2026
8a2c1ff
chore: update changelog
sebcrozet Aug 7, 2026
e89ffb4
chore: CI fixes
sebcrozet Aug 7, 2026
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 .github/workflows/parry-ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: sudo apt-get install -y cmake libxcb-composite0-dev
# WORKAROUND: zune-core 0.5.2 made its `warn!` macro expand to nothing, which breaks
# zune-jpeg (all 0.5.x), where it is called in expression position. Reached through the
# kiss3d -> gltf/image dev-dependency. Remove once upstream is fixed.
- name: Pin zune-core (0.5.2 breaks zune-jpeg)
run: cargo update -p zune-core@0.5 --precise 0.5.1
- name: Run tests
run: cargo test --features wavefront
- name: Run tests (parallel)
Expand Down Expand Up @@ -107,5 +112,8 @@ jobs:
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
# See the `tests` job: zune-core 0.5.2 breaks zune-jpeg. Remove once upstream is fixed.
- name: Pin zune-core (0.5.2 breaks zune-jpeg)
run: cargo update -p zune-core@0.5 --precise 0.5.1
- name: check benchmarks
run: cargo +nightly check --benches
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,60 @@
## Unreleased

### Added

- `Debug` for `TriMesh` now prints a summary instead of just the type name
([#177](https://github.com/dimforge/parry/issues/177)).
- `query::contact` now supports heightfields, instead of returning `Err(Unsupported)`
([#274](https://github.com/dimforge/parry/issues/274)).

### Fixed

- Fix GJK reporting overlapping axisymmetric shapes as disjoint when the simplex stalls on a
degenerate support direction ([#396](https://github.com/dimforge/parry/issues/396)).
- Fix wrong shape-cast normals and distances for stalled GJK simplices, now classified as touching
or penetrating from the solver's own distance bound ([#193](https://github.com/dimforge/parry/issues/193)).
- Fix shape-casts missing grazing hits at large coordinates, by scaling the GJK ray-cast tolerance
with the support magnitude ([#180](https://github.com/dimforge/parry/issues/180)).
- Fix shape-cast time of impact being short by an amount scaling with the shape's extent
([#429](https://github.com/dimforge/parry/issues/429)).
- Fix `ShapeCastStatus::PenetratingOrWithinTargetDist` being reported for merely touching support-map
casts ([#106](https://github.com/dimforge/parry/issues/106)).
- Fix `contact` between exactly-touching shapes returning witness points at the shapes' centers
([#315](https://github.com/dimforge/parry/issues/315)).
- Fix `Triangle::area` returning wildly wrong values for degenerate triangles
([#111](https://github.com/dimforge/parry/issues/111)).
- Fix point projection on degenerate triangles reporting the point as inside
([#76](https://github.com/dimforge/parry/issues/76)).
- Fix `segments_intersection2d` classifying exact segment endpoints as `OnEdge` instead of `OnVertex`
([#109](https://github.com/dimforge/parry/issues/109)).
- Fix composite-shape point projection panicking on non-finite points
([#395](https://github.com/dimforge/parry/issues/395)).
- Fix `HeightField::project_local_point` iterating on every element, making it O(rows * cols)
([rapier#332](https://github.com/dimforge/rapier/issues/332)).
- Fix single-point contact manifolds on cylinder and cone caps, by orienting the cap's polygonal
approximation toward the contact ([rapier#810](https://github.com/dimforge/rapier/issues/810)).
- Fix missing speculative contacts between a `Voxels` shape and a shape within the prediction
distance ([#404](https://github.com/dimforge/parry/issues/404)).
- Fix `Voxels` shape-cast `witness1` not being expressed in the voxels shape's local frame
([#373](https://github.com/dimforge/parry/issues/373)).
- Fix an out-of-bounds panic in the binned BVH builder on degenerate leaf AABBs
([rapier#961](https://github.com/dimforge/rapier/issues/961)).
- Fix out-of-bounds indices in `to_outline` for round shapes with a zero border radius
([rapier#969](https://github.com/dimforge/rapier/issues/969)).
- Fix the quadratic polygon removal in `hertel_mehlhorn`
([#408](https://github.com/dimforge/parry/issues/408)).
- Require `spade` 2.15, the minimum version providing `try_bulk_load_cdt`
([#428](https://github.com/dimforge/parry/issues/428)).

### Modified

- Document the frame conventions of the `ShapeCastHit` fields
([rapier#933](https://github.com/dimforge/rapier/issues/933)).
- Document that the `Voxels` iterators only yield non-empty voxels
([#382](https://github.com/dimforge/parry/issues/382)).
- Document that `BoundingVolume::merge`/`merged` don't guarantee strict containment of their inputs
([#260](https://github.com/dimforge/parry/issues/260)).

## 0.30.1

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ indexmap = { version = "2", features = ["serde"] }
hashbrown = { version = "0.17", default-features = false, features = [
"default-hasher",
] }
spade = { version = "2.9", default-features = false }
spade = { version = "2.15", default-features = false }
rayon = "1"
bytemuck = { version = "1", features = ["derive"] }
log = "0.4"
Expand Down
94 changes: 94 additions & 0 deletions crates/parry2d/tests/issue_109_segments_intersection_endpoints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Regression test for https://github.com/dimforge/parry/issues/109
//
// `segments_intersection2d` used to classify exact segment endpoints as `OnEdge`
// instead of `OnVertex`, in both the collinear and the non-parallel branches.

use parry2d::math::Vector;
use parry2d::shape::SegmentPointLocation;
use parry2d::utils::{segments_intersection2d, SegmentsIntersection};

#[test]
fn identical_segments_intersect_on_vertices() {
// Exact case from the issue.
let a = Vector::new(10.0, 0.0);
let b = Vector::new(10.0, 10.0);

let intersection = segments_intersection2d(a, b, a, b, 0.0).unwrap();

let SegmentsIntersection::Segment {
first_loc1,
first_loc2,
second_loc1,
second_loc2,
} = intersection
else {
panic!("the intersection should be a Segment intersection");
};

assert_eq!(first_loc1, SegmentPointLocation::OnVertex(0));
assert_eq!(first_loc2, SegmentPointLocation::OnVertex(0));
assert_eq!(second_loc1, SegmentPointLocation::OnVertex(1));
assert_eq!(second_loc2, SegmentPointLocation::OnVertex(1));
}

#[test]
fn identical_horizontal_segments_intersect_on_vertices() {
// Same check through the non-vertical code path of `between()`.
let a = Vector::new(-3.0, 2.0);
let b = Vector::new(5.0, 2.0);

let intersection = segments_intersection2d(a, b, a, b, 0.0).unwrap();

let SegmentsIntersection::Segment {
first_loc1,
first_loc2,
second_loc1,
second_loc2,
} = intersection
else {
panic!("the intersection should be a Segment intersection");
};

assert_eq!(first_loc1, SegmentPointLocation::OnVertex(0));
assert_eq!(first_loc2, SegmentPointLocation::OnVertex(0));
assert_eq!(second_loc1, SegmentPointLocation::OnVertex(1));
assert_eq!(second_loc2, SegmentPointLocation::OnVertex(1));
}

#[test]
fn crossing_at_endpoint_is_on_vertex() {
// Two non-parallel segments touching at seg1's vertex 1 == seg2's vertex 0.
// Exercises the `s == 1.0` fix (previously `s == denom`, so `OnVertex(1)`
// was unreachable).
let a = Vector::new(0.0, 0.0);
let b = Vector::new(1.0, 1.0);
let c = Vector::new(1.0, 1.0);
let d = Vector::new(2.0, 0.0);

let intersection = segments_intersection2d(a, b, c, d, 0.0).unwrap();

let SegmentsIntersection::Point { loc1, loc2 } = intersection else {
panic!("the intersection should be a Point intersection");
};

assert_eq!(loc1, SegmentPointLocation::OnVertex(1));
assert_eq!(loc2, SegmentPointLocation::OnVertex(0));
}

#[test]
fn crossing_at_interior_point_is_on_edge() {
// Proper crossing in both interiors: still `OnEdge`.
let a = Vector::new(-1.0, 0.0);
let b = Vector::new(1.0, 0.0);
let c = Vector::new(0.0, -1.0);
let d = Vector::new(0.0, 1.0);

let intersection = segments_intersection2d(a, b, c, d, 0.0).unwrap();

let SegmentsIntersection::Point { loc1, loc2 } = intersection else {
panic!("the intersection should be a Point intersection");
};

assert_eq!(loc1, SegmentPointLocation::OnEdge([0.5, 0.5]));
assert_eq!(loc2, SegmentPointLocation::OnEdge([0.5, 0.5]));
}
81 changes: 81 additions & 0 deletions crates/parry2d/tests/issue_111_triangle_area_degenerate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Regression test for https://github.com/dimforge/parry/issues/111 (2D version).
//
// `Triangle::area` now uses half the perp-product magnitude in 2D, which is
// exact (0.0) when the vertices are bitwise collinear.

use parry2d::math::{Real, Vector};
use parry2d::shape::Triangle;

/// The previous implementation (Kahan's formula on side lengths).
fn kahan_area(tri: &Triangle) -> Real {
let mut s = [
tri.b.distance(tri.a),
tri.c.distance(tri.b),
tri.a.distance(tri.c),
];
s.sort_by(|x, y| x.partial_cmp(y).unwrap());
let (c, b, a) = (s[0], s[1], s[2]); // a >= b >= c

let sqr = (a + (b + c)) * (c - (a - b)) * (c + (a - b)) * (a + (b - c));
sqr.max(0.0).sqrt() * 0.25
}

#[test]
fn degenerate_triangle_area_is_exactly_zero() {
// The issue's collinear triangle, projected to 2D (all points share x, so
// use the (y, z) coordinates).
let tri = Triangle::new(
Vector::new(-2.871, 17.464),
Vector::new(1.629, 17.464),
Vector::new(-1.521, 17.464),
);
assert_eq!(tri.area(), 0.0);

// Two identical vertices.
let tri = Triangle::new(
Vector::new(2.277, -7.9),
Vector::new(-0.57, -8.1),
Vector::new(-0.57, -8.1),
);
assert_eq!(tri.area(), 0.0);
}

#[test]
fn area_matches_kahan_for_well_conditioned_triangles() {
let tris = [
Triangle::new(
Vector::new(0.0, 0.0),
Vector::new(1.0, 0.0),
Vector::new(0.0, 1.0),
),
Triangle::new(
Vector::new(1.0, 2.0),
Vector::new(4.0, 0.0),
Vector::new(2.0, 5.0),
),
Triangle::new(
Vector::new(-3.0, 1.0),
Vector::new(0.0, 4.0),
Vector::new(5.0, -1.0),
),
];

for tri in &tris {
let expected = kahan_area(tri);
let area = tri.area();
assert!(
(area - expected).abs() <= expected * 1.0e-6,
"area {area} != kahan area {expected}"
);
}

assert_eq!(
Triangle::new(
Vector::new(0.0, 0.0),
Vector::new(1.0, 0.0),
Vector::new(0.0, 1.0),
)
.area(),
0.5
);
}
84 changes: 84 additions & 0 deletions crates/parry2d/tests/issue_168_ball_polyline_endpoint_normal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Regression test for https://github.com/dimforge/rapier/issues/168
//
// A ball contacting a polyline exactly at (or beyond) its endpoint vertex used
// to produce a zero contact normal. The ball-vs-convex manifold computation now
// has a degenerate fallback so the normal is always finite and non-zero.

use parry2d::math::{Pose, Real, Vector};
use parry2d::query::{ContactManifold, DefaultQueryDispatcher, PersistentQueryDispatcher};
use parry2d::shape::{Ball, Polyline};

fn check_manifold_normals(pos12: Pose, prediction: Real) -> usize {
let polyline = Polyline::new(vec![Vector::new(-10.0, 0.0), Vector::new(10.0, 0.0)], None);
let ball = Ball::new(1.0);

let dispatcher = DefaultQueryDispatcher;
let mut manifolds: Vec<ContactManifold<(), ()>> = Vec::new();
let mut workspace = None;

dispatcher
.contact_manifolds(
&pos12,
&polyline,
&ball,
prediction,
&mut manifolds,
&mut workspace,
)
.unwrap();

let mut num_contacts = 0;

for manifold in &manifolds {
if manifold.points.is_empty() {
continue;
}

num_contacts += manifold.points.len();

for local_n in [manifold.local_n1, manifold.local_n2] {
assert!(
local_n.is_finite(),
"non-finite contact normal: {local_n:?} (pos12: {pos12:?})"
);
assert!(
local_n.length() > 0.9,
"degenerate contact normal: {local_n:?} (pos12: {pos12:?})"
);
}
}

num_contacts
}

#[test]
fn ball_beyond_polyline_endpoint() {
// The geometry from the issue: the polyline ends at x = 10.0, the ball
// (radius 1) is at x = 11.0, y = 1.0, so it barely misses the endpoint but
// is within the contact prediction distance.
let num_contacts = check_manifold_normals(Pose::translation(11.0, 1.0), 1.0);
assert!(num_contacts > 0);
}

#[test]
fn ball_aligned_with_polyline_beyond_endpoint() {
// The `______o` case from the issue: ball right beside the line, aligned
// with it but not touching it.
let num_contacts = check_manifold_normals(Pose::translation(11.5, 0.0), 1.0);
assert!(num_contacts > 0);
}

#[test]
fn ball_centered_on_polyline_endpoint() {
// Fully degenerate case: the ball center lies exactly on the endpoint
// vertex, so the projection distance is zero.
let num_contacts = check_manifold_normals(Pose::translation(10.0, 0.0), 0.1);
assert!(num_contacts > 0);
}

#[test]
fn ball_overlapping_polyline_endpoint() {
// Overlap case without prediction: the ball overlaps the endpoint vertex.
let num_contacts = check_manifold_normals(Pose::translation(10.5, 0.5), 0.0);
assert!(num_contacts > 0);
}
Loading
Loading