gizmo_physics_core/body.rs
1//! Opaque body handle for the physics world.
2
3/// An opaque identifier for a body inside a physics world.
4///
5/// `BodyHandle` is the single identity the physics public API speaks in —
6/// collision/trigger/fracture events, raycast hits, joints and broadphase
7/// queries all carry `BodyHandle`s rather than the engine's ECS
8/// [`Entity`](gizmo_core::entity::Entity). The ECS↔physics bridge converts at
9/// the boundary (`BodyHandle::from_id(entity.id())` on the way in,
10/// `Entity::new(handle.id(), 0)` on the way out), so a non-ECS embedding can
11/// drive the simulation with its own handles and the internal representation
12/// can evolve post-1.0 without breaking the physics API.
13///
14/// It is a transparent newtype over `u32`, so where it appears in serialized
15/// data (e.g. scene-file joint endpoints) it round-trips as that single id.
16///
17/// # Determinism
18/// A handle wraps the body's stable `u32` id — the sole identity physics keys
19/// on (the deterministic state hash sorts and mixes bodies by [`id`](Self::id)).
20/// Carrying the same id the old `Entity` did keeps the state hash bit-identical,
21/// preserving cross-process/rollback determinism.
22#[derive(
23 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
24)]
25pub struct BodyHandle(u32);
26
27impl BodyHandle {
28 /// Sentinel value for "no body"; usable in place of `Option<BodyHandle>`.
29 pub const INVALID: Self = Self(u32::MAX);
30
31 /// Builds a handle from a raw body id.
32 #[inline]
33 pub const fn from_id(id: u32) -> Self {
34 Self(id)
35 }
36
37 /// The body's stable id (the value the deterministic state hash keys on).
38 #[inline]
39 pub const fn id(self) -> u32 {
40 self.0
41 }
42
43 /// `true` unless this is [`INVALID`](Self::INVALID).
44 #[inline]
45 pub fn is_valid(self) -> bool {
46 self != Self::INVALID
47 }
48}
49
50impl std::fmt::Display for BodyHandle {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 if *self == Self::INVALID {
53 write!(f, "BodyHandle(INVALID)")
54 } else {
55 write!(f, "BodyHandle({})", self.0)
56 }
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn from_id_roundtrips() {
66 let h = BodyHandle::from_id(42);
67 assert_eq!(h.id(), 42);
68 assert!(h.is_valid());
69 }
70
71 #[test]
72 fn invalid_sentinel() {
73 assert!(!BodyHandle::INVALID.is_valid());
74 assert_eq!(BodyHandle::INVALID.id(), u32::MAX);
75 }
76
77 #[test]
78 fn ordering_and_hash_by_id() {
79 use std::collections::HashSet;
80 assert!(BodyHandle::from_id(5) < BodyHandle::from_id(10));
81 let mut set = HashSet::new();
82 set.insert(BodyHandle::from_id(7));
83 assert!(set.contains(&BodyHandle::from_id(7)));
84 assert!(!set.contains(&BodyHandle::from_id(8)));
85 }
86}