use crate::math::{Fix128, Vec3Fix};
use crate::solver::RigidBody;
use crate::world_participant::{
ObservationSink, Participant, ParticipantFault, ParticipantKind, StateError, SubstepCtx,
};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub const DRAG_MEDIUM_KIND: ParticipantKind = ParticipantKind::new(u32::from_be_bytes(*b"MEDM"));
pub const MEDIUM_OBS_VELOCITY: u32 = 0;
pub const MEDIUM_OBS_MOMENTUM: u32 = 3;
const MEDIUM_STATE_VERSION: u32 = 1;
const MEDIUM_STATE_LEN: usize = 4 + 8 + 3 * 16;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum DragMediumError {
NonPositiveMass,
NegativeCoefficient {
body: usize,
},
DuplicateBody {
body: usize,
},
MomentumOutOfRange,
}
impl core::fmt::Display for DragMediumError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NonPositiveMass => write!(f, "medium mass must be positive"),
Self::NegativeCoefficient { body } => {
write!(f, "drag coefficient of body {body} is negative")
}
Self::DuplicateBody { body } => write!(f, "body {body} is already coupled"),
Self::MomentumOutOfRange => write!(f, "medium momentum out of range"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DragMediumError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DragCoupling {
pub body: usize,
pub coefficient: Fix128,
}
#[derive(Clone, Debug)]
pub struct DragMedium {
mass: Fix128,
momentum: Vec3Fix,
couplings: Vec<DragCoupling>,
digest: u64,
}
fn wide(f: Fix128) -> i128 {
(i128::from(f.hi) << 64) | i128::from(f.lo)
}
fn narrow(raw: i128) -> Fix128 {
Fix128 {
hi: (raw >> 64) as i64,
lo: raw as u64,
}
}
fn checked_add(a: Fix128, b: Fix128) -> Option<Fix128> {
wide(a).checked_add(wide(b)).map(narrow)
}
fn checked_sub(a: Fix128, b: Fix128) -> Option<Fix128> {
wide(a).checked_sub(wide(b)).map(narrow)
}
fn checked_add_vec(a: Vec3Fix, b: Vec3Fix) -> Option<Vec3Fix> {
Some(Vec3Fix::new(
checked_add(a.x, b.x)?,
checked_add(a.y, b.y)?,
checked_add(a.z, b.z)?,
))
}
fn checked_sub_vec(a: Vec3Fix, b: Vec3Fix) -> Option<Vec3Fix> {
Some(Vec3Fix::new(
checked_sub(a.x, b.x)?,
checked_sub(a.y, b.y)?,
checked_sub(a.z, b.z)?,
))
}
fn checked_quotient(a: Fix128, b: Fix128) -> Option<Fix128> {
if b.is_zero() {
return None;
}
let q = a / b;
let back = q.checked_mul(b)?;
let miss = wide(back).checked_sub(wide(a))?.unsigned_abs();
let allowed = (wide(b).unsigned_abs() >> 64) + 2;
(miss <= allowed).then_some(q)
}
fn checked_quotient_vec(a: Vec3Fix, b: Fix128) -> Option<Vec3Fix> {
Some(Vec3Fix::new(
checked_quotient(a.x, b)?,
checked_quotient(a.y, b)?,
checked_quotient(a.z, b)?,
))
}
fn fnv(mut h: u64, bytes: &[u8]) -> u64 {
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
fn fnv_fix(h: u64, f: Fix128) -> u64 {
fnv(fnv(h, &f.hi.to_le_bytes()), &f.lo.to_le_bytes())
}
fn digest(mass: Fix128, couplings: &[DragCoupling]) -> u64 {
let mut h = fnv_fix(0xcbf2_9ce4_8422_2325, mass);
for c in couplings {
h = fnv(h, &(c.body as u64).to_le_bytes());
h = fnv_fix(h, c.coefficient);
}
h
}
fn put_fix(out: &mut Vec<u8>, f: Fix128) {
out.extend_from_slice(&f.hi.to_le_bytes());
out.extend_from_slice(&f.lo.to_le_bytes());
}
fn get_fix(bytes: &[u8], at: usize) -> Fix128 {
let mut hi = [0u8; 8];
let mut lo = [0u8; 8];
hi.copy_from_slice(&bytes[at..at + 8]);
lo.copy_from_slice(&bytes[at + 8..at + 16]);
Fix128 {
hi: i64::from_le_bytes(hi),
lo: u64::from_le_bytes(lo),
}
}
impl DragMedium {
pub fn new(mass: Fix128, velocity: Vec3Fix) -> Result<Self, DragMediumError> {
if mass <= Fix128::ZERO {
return Err(DragMediumError::NonPositiveMass);
}
let momentum = velocity
.checked_scale(mass)
.ok_or(DragMediumError::MomentumOutOfRange)?;
Ok(Self {
mass,
momentum,
couplings: Vec::new(),
digest: digest(mass, &[]),
})
}
pub fn couple(&mut self, body: usize, coefficient: Fix128) -> Result<(), DragMediumError> {
if coefficient < Fix128::ZERO {
return Err(DragMediumError::NegativeCoefficient { body });
}
if self.couplings.iter().any(|c| c.body == body) {
return Err(DragMediumError::DuplicateBody { body });
}
self.couplings.push(DragCoupling { body, coefficient });
self.digest = digest(self.mass, &self.couplings);
Ok(())
}
#[must_use]
pub const fn mass(&self) -> Fix128 {
self.mass
}
#[must_use]
pub const fn momentum(&self) -> Vec3Fix {
self.momentum
}
#[must_use]
pub fn velocity(&self) -> Option<Vec3Fix> {
checked_quotient_vec(self.momentum, self.mass)
}
#[must_use]
pub fn couplings(&self) -> &[DragCoupling] {
&self.couplings
}
#[must_use]
pub fn total_momentum(&self, bodies: &[RigidBody]) -> Option<Vec3Fix> {
let mut total = self.momentum;
for c in &self.couplings {
let body = bodies.get(c.body)?;
if !body.is_dynamic() || body.inv_mass.is_zero() {
continue;
}
total = checked_add_vec(total, checked_quotient_vec(body.velocity, body.inv_mass)?)?;
}
Some(total)
}
fn exchange(
&self,
bodies: &[RigidBody],
h: Fix128,
mut stage: impl FnMut(usize, Vec3Fix) -> Result<(), ParticipantFault>,
) -> Result<Vec3Fix, ParticipantFault> {
let oor = ParticipantFault::OutOfRange;
let u = checked_quotient_vec(self.momentum, self.mass).ok_or(oor)?;
let mut momentum = self.momentum;
let mut staged: Vec<(usize, Vec3Fix)> = Vec::with_capacity(self.couplings.len());
for c in &self.couplings {
let body = bodies.get(c.body).ok_or(ParticipantFault::InvalidState)?;
if !body.is_dynamic() || body.inv_mass.is_zero() {
continue;
}
let force = checked_sub_vec(u, body.velocity)
.and_then(|w| w.checked_scale(c.coefficient))
.ok_or(oor)?;
let dv = force
.checked_scale(body.inv_mass)
.and_then(|a| a.checked_scale(h))
.ok_or(oor)?;
checked_add_vec(body.velocity, dv).ok_or(oor)?;
let given = checked_quotient_vec(dv, body.inv_mass).ok_or(oor)?;
momentum = checked_sub_vec(momentum, given).ok_or(oor)?;
staged.push((c.body, force));
}
for (body, force) in staged {
stage(body, force)?;
}
Ok(momentum)
}
}
impl Participant for DragMedium {
fn kind(&self) -> ParticipantKind {
DRAG_MEDIUM_KIND
}
fn substep(&mut self, ctx: &mut SubstepCtx<'_>, h: Fix128) -> Result<(), ParticipantFault> {
let bodies = ctx.bodies().to_vec();
let momentum = self.exchange(&bodies, h, |body, force| {
ctx.add_force(body, force)
.map_err(|_| ParticipantFault::InvalidState)
})?;
self.momentum = momentum;
Ok(())
}
fn observe(&self, out: &mut ObservationSink) {
if let Some(u) = self.velocity() {
out.push(MEDIUM_OBS_VELOCITY, u.x);
out.push(MEDIUM_OBS_VELOCITY + 1, u.y);
out.push(MEDIUM_OBS_VELOCITY + 2, u.z);
}
out.push(MEDIUM_OBS_MOMENTUM, self.momentum.x);
out.push(MEDIUM_OBS_MOMENTUM + 1, self.momentum.y);
out.push(MEDIUM_OBS_MOMENTUM + 2, self.momentum.z);
}
fn write_state(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&MEDIUM_STATE_VERSION.to_le_bytes());
out.extend_from_slice(&self.digest.to_le_bytes());
put_fix(out, self.momentum.x);
put_fix(out, self.momentum.y);
put_fix(out, self.momentum.z);
}
fn check_state(&self, bytes: &[u8]) -> Result<(), StateError> {
if bytes.len() != MEDIUM_STATE_LEN {
return Err(StateError::Length {
expected: MEDIUM_STATE_LEN,
found: bytes.len(),
});
}
let mut version = [0u8; 4];
version.copy_from_slice(&bytes[0..4]);
let mut d = [0u8; 8];
d.copy_from_slice(&bytes[4..12]);
if u32::from_le_bytes(version) != MEDIUM_STATE_VERSION
|| u64::from_le_bytes(d) != self.digest
{
return Err(StateError::InvalidValue);
}
Ok(())
}
fn read_state(&mut self, bytes: &[u8]) {
self.momentum = Vec3Fix::new(get_fix(bytes, 12), get_fix(bytes, 28), get_fix(bytes, 44));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::{string::ToString, vec};
#[test]
fn quotient_refuses_zero_and_wrapped_results() {
assert_eq!(checked_quotient(Fix128::ONE, Fix128::ZERO), None);
let tiny = Fix128::from_raw(0, 1);
assert_eq!(checked_quotient(Fix128::from_int(1 << 40), tiny), None);
assert_eq!(
checked_quotient(Fix128::from_int(6), Fix128::from_int(2)),
Some(Fix128::from_int(3))
);
assert_eq!(
checked_quotient(Fix128::from_int(-6), Fix128::from_ratio(1, 4)),
Some(Fix128::from_int(-24))
);
}
#[test]
fn digest_depends_on_mass_and_couplings() {
let mut a = DragMedium::new(Fix128::ONE, Vec3Fix::ZERO).expect("medium");
let b = DragMedium::new(Fix128::from_int(2), Vec3Fix::ZERO).expect("medium");
assert_ne!(a.digest, b.digest);
let before = a.digest;
a.couple(0, Fix128::ONE).expect("couple");
assert_ne!(a.digest, before);
}
fn x(v: Fix128) -> Vec3Fix {
Vec3Fix::new(v, Fix128::ZERO, Fix128::ZERO)
}
#[test]
fn construction_errors_and_messages() {
assert_eq!(
DragMedium::new(Fix128::ZERO, Vec3Fix::ZERO).err(),
Some(DragMediumError::NonPositiveMass)
);
assert_eq!(
DragMedium::new(Fix128::from_int(1 << 40), x(Fix128::from_int(1 << 40))).err(),
Some(DragMediumError::MomentumOutOfRange)
);
let mut m = DragMedium::new(Fix128::ONE, Vec3Fix::ZERO).expect("medium");
assert_eq!(
m.couple(3, -Fix128::ONE),
Err(DragMediumError::NegativeCoefficient { body: 3 })
);
m.couple(3, Fix128::ONE).expect("couple");
assert_eq!(
m.couple(3, Fix128::ONE),
Err(DragMediumError::DuplicateBody { body: 3 })
);
for (e, text) in [
(
DragMediumError::NonPositiveMass,
"medium mass must be positive",
),
(
DragMediumError::NegativeCoefficient { body: 3 },
"drag coefficient of body 3 is negative",
),
(
DragMediumError::DuplicateBody { body: 3 },
"body 3 is already coupled",
),
(
DragMediumError::MomentumOutOfRange,
"medium momentum out of range",
),
] {
assert_eq!(e.to_string(), text);
}
}
#[test]
fn accessors_and_total_momentum() {
let mut m =
DragMedium::new(Fix128::from_int(2), Vec3Fix::from_int(1, 2, 3)).expect("medium");
assert_eq!(m.mass(), Fix128::from_int(2));
assert_eq!(m.momentum(), Vec3Fix::from_int(2, 4, 6));
assert_eq!(m.velocity(), Some(Vec3Fix::from_int(1, 2, 3)));
let mut body = RigidBody::new(Vec3Fix::ZERO, Fix128::from_int(4));
body.velocity = Vec3Fix::from_int(1, 0, 0);
let bodies = [body, RigidBody::new_static(Vec3Fix::ZERO)];
m.couple(0, Fix128::ONE).expect("couple");
m.couple(1, Fix128::ONE).expect("couple");
assert_eq!(m.couplings().len(), 2);
assert_eq!(m.total_momentum(&bodies), Some(Vec3Fix::from_int(6, 4, 6)));
m.couple(5, Fix128::ONE).expect("couple");
assert_eq!(m.total_momentum(&bodies), None);
}
#[test]
fn exchange_closed_form() {
let mut m = DragMedium::new(Fix128::from_int(2), x(Fix128::ONE)).expect("medium");
m.couple(0, Fix128::from_int(2)).expect("couple");
let bodies = [RigidBody::new(Vec3Fix::ZERO, Fix128::ONE)];
let mut staged = Vec::new();
let momentum = m
.exchange(&bodies, Fix128::from_ratio(1, 4), |b, f| {
staged.push((b, f));
Ok(())
})
.expect("exchange");
assert_eq!(momentum, x(Fix128::from_ratio(3, 2)));
assert_eq!(staged, vec![(0, x(Fix128::from_int(2)))]);
m.couple(7, Fix128::ONE).expect("couple");
assert_eq!(
m.exchange(&bodies, Fix128::ONE, |_, _| Ok(())),
Err(ParticipantFault::InvalidState)
);
}
#[cfg(feature = "std")]
#[test]
fn participant_in_a_world_and_state() {
let mut world = crate::solver::PhysicsWorld::new(crate::solver::SolverConfig {
substeps: 1,
gravity: Vec3Fix::ZERO,
damping: Fix128::ONE,
..Default::default()
});
let body = world.add_body(RigidBody::new(Vec3Fix::ZERO, Fix128::ONE));
let mut m = DragMedium::new(Fix128::from_int(2), x(Fix128::ONE)).expect("medium");
m.couple(body, Fix128::from_int(2)).expect("couple");
assert_eq!(Participant::kind(&m), ParticipantKind::new(0x4d45_444d));
world
.add_participant(Box::new(m.clone()))
.expect("register");
world.step(Fix128::from_ratio(1, 4));
assert_eq!(world.bodies[body].velocity, x(Fix128::from_ratio(1, 2)));
let Some(crate::world_participant::Observed::Exact(sink)) = world.observe_participant(0)
else {
panic!("observation");
};
let z = Fix128::ZERO;
assert_eq!(
sink.values(),
&[
(MEDIUM_OBS_VELOCITY, Fix128::from_ratio(3, 4)),
(MEDIUM_OBS_VELOCITY + 1, z),
(MEDIUM_OBS_VELOCITY + 2, z),
(MEDIUM_OBS_MOMENTUM, Fix128::from_ratio(3, 2)),
(MEDIUM_OBS_MOMENTUM + 1, z),
(MEDIUM_OBS_MOMENTUM + 2, z),
]
);
let bytes = world.participant_state(0).expect("state");
assert_eq!(bytes.len(), 60);
assert_eq!(bytes[..4], 1_u32.to_le_bytes());
assert_eq!(m.check_state(&bytes), Ok(()));
m.read_state(&bytes);
assert_eq!(m.momentum(), x(Fix128::from_ratio(3, 2)));
assert_eq!(
m.check_state(&bytes[..59]),
Err(StateError::Length {
expected: 60,
found: 59
})
);
let mut version = bytes.clone();
version[0] = 9;
assert_eq!(m.check_state(&version), Err(StateError::InvalidValue));
let mut digest = bytes;
digest[4] ^= 1;
assert_eq!(m.check_state(&digest), Err(StateError::InvalidValue));
}
}