Skip to content

Commit 1c052bc

Browse files
committed
geom(timespan): make fat_aabb conservative for rotations by unioning start/mid/end AABBs; add rotation-offset coverage test; update plan + decision log
1 parent d649e9f commit 1c052bc

File tree

4 files changed

+99
-4
lines changed

4 files changed

+99
-4
lines changed

crates/rmg-geom/src/temporal/timespan.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,49 @@ impl Timespan {
3838

3939
/// Computes a conservative fat AABB for a collider with local-space `shape` AABB.
4040
///
41-
/// The fat box is defined as the union of the shape’s AABBs at the start and
42-
/// end transforms. This is conservative for linear motion and suffices for
43-
/// broad-phase pairing and CCD triggering.
41+
/// Policy (deterministic): unions the AABBs at three sample poses — start (t=0),
42+
/// midpoint (t=0.5), and end (t=1). This strictly contains pure translations
43+
/// and captures protrusions that can occur at `t≈0.5` during rotations about
44+
/// an off‑centre pivot, which a start/end‑only union can miss.
45+
///
46+
/// Sampling count is fixed (3) for determinism; future work may make the
47+
/// sampling policy configurable while keeping results identical across peers.
4448
#[must_use]
4549
pub fn fat_aabb(&self, shape: &Aabb) -> Aabb {
4650
let a0 = shape.transformed(&self.start.to_mat4());
4751
let a1 = shape.transformed(&self.end.to_mat4());
48-
a0.union(&a1)
52+
53+
// Midpoint transform via linear interp of translation/scale and
54+
// normalized-linear blend of rotation (nlerp), then convert to Mat4.
55+
let t0 = self.start.translation().to_array();
56+
let t1 = self.end.translation().to_array();
57+
let tm = rmg_core::math::Vec3::new(
58+
0.5 * (t0[0] + t1[0]),
59+
0.5 * (t0[1] + t1[1]),
60+
0.5 * (t0[2] + t1[2]),
61+
);
62+
63+
let s0 = self.start.scale().to_array();
64+
let s1 = self.end.scale().to_array();
65+
let sm = rmg_core::math::Vec3::new(
66+
0.5 * (s0[0] + s1[0]),
67+
0.5 * (s0[1] + s1[1]),
68+
0.5 * (s0[2] + s1[2]),
69+
);
70+
71+
let q0 = self.start.rotation().to_array();
72+
let q1 = self.end.rotation().to_array();
73+
let qm = rmg_core::math::Quat::new(
74+
0.5 * (q0[0] + q1[0]),
75+
0.5 * (q0[1] + q1[1]),
76+
0.5 * (q0[2] + q1[2]),
77+
0.5 * (q0[3] + q1[3]),
78+
)
79+
.normalize();
80+
81+
let mid_tf = crate::types::transform::Transform::new(tm, qm, sm);
82+
let am = shape.transformed(&mid_tf.to_mat4());
83+
84+
a0.union(&a1).union(&am)
4985
}
5086
}

crates/rmg-geom/tests/geom_broad_tests.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,50 @@ fn broad_phase_pair_order_is_deterministic() {
4343
// Expected canonical order: (0,1), (0,3), (1,3)
4444
assert_eq!(pairs, vec![(0, 1), (0, 3), (1, 3)]);
4545
}
46+
47+
#[test]
48+
fn fat_aabb_covers_mid_rotation_with_offset() {
49+
use core::f32::consts::FRAC_PI_2;
50+
// Local shape: rod from x=0..2 (center at (1,0,0)) with small thickness
51+
let local =
52+
Aabb::from_center_half_extents(rmg_core::math::Vec3::new(1.0, 0.0, 0.0), 1.0, 0.1, 0.1);
53+
54+
let t0 = Transform::new(
55+
rmg_core::math::Vec3::new(0.0, 0.0, 0.0),
56+
rmg_core::math::Quat::identity(),
57+
rmg_core::math::Vec3::new(1.0, 1.0, 1.0),
58+
);
59+
let t1 = Transform::new(
60+
rmg_core::math::Vec3::new(0.0, 0.0, 0.0),
61+
rmg_core::math::Quat::from_axis_angle(rmg_core::math::Vec3::new(0.0, 0.0, 1.0), FRAC_PI_2),
62+
rmg_core::math::Vec3::new(1.0, 1.0, 1.0),
63+
);
64+
let span = Timespan::new(t0, t1);
65+
66+
// Compute mid pose explicitly (45°); this can protrude beyond both endpoints.
67+
let mid_rot = rmg_core::math::Quat::from_axis_angle(
68+
rmg_core::math::Vec3::new(0.0, 0.0, 1.0),
69+
FRAC_PI_2 * 0.5,
70+
);
71+
let mid = Transform::new(
72+
rmg_core::math::Vec3::new(0.0, 0.0, 0.0),
73+
mid_rot,
74+
rmg_core::math::Vec3::new(1.0, 1.0, 1.0),
75+
);
76+
let mid_aabb = local.transformed(&mid.to_mat4());
77+
78+
let fat = span.fat_aabb(&local);
79+
let fmin = fat.min().to_array();
80+
let fmax = fat.max().to_array();
81+
let mmin = mid_aabb.min().to_array();
82+
let mmax = mid_aabb.max().to_array();
83+
84+
assert!(
85+
fmin[0] <= mmin[0] && fmin[1] <= mmin[1] && fmin[2] <= mmin[2],
86+
"fat min must enclose mid min: fat={fmin:?} mid={mmin:?}"
87+
);
88+
assert!(
89+
fmax[0] >= mmax[0] && fmax[1] >= mmax[1] && fmax[2] >= mmax[2],
90+
"fat max must enclose mid max: fat={fmax:?} mid={mmax:?}"
91+
);
92+
}

docs/decision-log.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,10 @@
8484
- Decision: Pre-commit runs `cargo fmt --all -- --check` whenever staged Rust files are detected. Retain the PRNG coupling guard but remove the unconditional early exit so formatting still runs when the PRNG file isn’t staged.
8585
- EditorConfig: normalize line endings (LF), ensure final newline, trim trailing whitespace, set 2-space indent for JS/TS/JSON and 4-space for Rust.
8686
- Consequence: Developers get immediate feedback on formatting; cleaner diffs and fewer CI round-trips.
87+
88+
## 2025-10-29 — Geom fat AABB bounds mid-rotation
89+
90+
- Context: Broad-phase must not miss overlaps when a shape rotates about an off‑centre pivot; union of endpoint AABBs can under‑approximate mid‑tick extents.
91+
- Decision: `Timespan::fat_aabb` now unions AABBs at start, mid (t=0.5 via nlerp for rotation, lerp for translation/scale), and end. Sampling count is fixed (3) for determinism.
92+
- Change: Implement midpoint sampling in `crates/rmg-geom/src/temporal/timespan.rs`; add test `fat_aabb_covers_mid_rotation_with_offset` to ensure mid‑pose is enclosed.
93+
- Consequence: Deterministic and more conservative broad‑phase bounds for typical rotation cases without introducing policy/config surface yet; future work may expose a configurable sampling policy.

docs/execution-plan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s
3333

3434
## Today’s Intent
3535

36+
> 2025-10-29 — Geom fat AABB midpoint sampling (merge-train)
37+
38+
- Update `rmg-geom::temporal::Timespan::fat_aabb` to union AABBs at start, mid (t=0.5), and end to conservatively bound rotations about off‑centre pivots.
39+
- Add test `fat_aabb_covers_mid_rotation_with_offset` to verify the fat box encloses the mid‑pose AABB.
40+
3641
> 2025-10-29 — Hooks formatting gate (PR #12)
3742
3843
- Pre-commit: add rustfmt check for staged Rust files (`cargo fmt --all -- --check`).

0 commit comments

Comments
 (0)