use crate::math::Fix128;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
pub type MaterialId = u16;
pub const DEFAULT_MATERIAL: MaterialId = 0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum CombineRule {
#[default]
Average,
Min,
Max,
Multiply,
}
impl CombineRule {
#[inline]
#[must_use]
pub fn apply(&self, a: Fix128, b: Fix128) -> Fix128 {
match self {
Self::Average => (a + b).half(),
Self::Min => {
if a < b {
a
} else {
b
}
}
Self::Max => {
if a > b {
a
} else {
b
}
}
Self::Multiply => a * b,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PhysicsMaterial {
pub id: MaterialId,
pub static_friction: Fix128,
pub dynamic_friction: Fix128,
pub restitution: Fix128,
pub friction_combine: CombineRule,
pub restitution_combine: CombineRule,
}
impl PhysicsMaterial {
#[must_use]
pub const fn new(id: MaterialId, friction: Fix128, restitution: Fix128) -> Self {
Self {
id,
static_friction: friction,
dynamic_friction: friction,
restitution,
friction_combine: CombineRule::Average,
restitution_combine: CombineRule::Average,
}
}
#[must_use]
pub const fn with_combine_rules(
mut self,
friction: CombineRule,
restitution: CombineRule,
) -> Self {
self.friction_combine = friction;
self.restitution_combine = restitution;
self
}
#[must_use]
pub const fn with_static_friction(mut self, static_friction: Fix128) -> Self {
self.static_friction = static_friction;
self
}
}
impl Default for PhysicsMaterial {
fn default() -> Self {
Self::new(
DEFAULT_MATERIAL,
Fix128::from_ratio(5, 10),
Fix128::from_ratio(3, 10),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct PairOverride {
mat_a: MaterialId,
mat_b: MaterialId,
friction: Fix128,
restitution: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CombinedMaterial {
pub friction: Fix128,
pub restitution: Fix128,
}
pub struct MaterialTable {
materials: Vec<PhysicsMaterial>,
pair_overrides: Vec<PairOverride>,
pub default_friction_combine: CombineRule,
pub default_restitution_combine: CombineRule,
}
impl MaterialTable {
#[must_use]
pub fn new() -> Self {
let mut table = Self {
materials: Vec::new(),
pair_overrides: Vec::new(),
default_friction_combine: CombineRule::Average,
default_restitution_combine: CombineRule::Average,
};
table.register(PhysicsMaterial::default());
table
}
pub fn register(&mut self, material: PhysicsMaterial) -> MaterialId {
let id = self.materials.len() as MaterialId;
let mut mat = material;
mat.id = id;
self.materials.push(mat);
id
}
#[must_use]
pub fn get(&self, id: MaterialId) -> &PhysicsMaterial {
self.materials
.get(id as usize)
.unwrap_or(&self.materials[0])
}
pub fn set_pair_override(
&mut self,
mat_a: MaterialId,
mat_b: MaterialId,
friction: Fix128,
restitution: Fix128,
) {
let (a, b) = if mat_a <= mat_b {
(mat_a, mat_b)
} else {
(mat_b, mat_a)
};
if let Some(p) = self
.pair_overrides
.iter_mut()
.find(|p| p.mat_a == a && p.mat_b == b)
{
p.friction = friction;
p.restitution = restitution;
} else {
self.pair_overrides.push(PairOverride {
mat_a: a,
mat_b: b,
friction,
restitution,
});
}
}
#[must_use]
pub fn combine(&self, mat_a: MaterialId, mat_b: MaterialId) -> CombinedMaterial {
let (a, b) = if mat_a <= mat_b {
(mat_a, mat_b)
} else {
(mat_b, mat_a)
};
if let Some(p) = self
.pair_overrides
.iter()
.find(|p| p.mat_a == a && p.mat_b == b)
{
return CombinedMaterial {
friction: p.friction,
restitution: p.restitution,
};
}
let mat_a = self.get(a);
let mat_b = self.get(b);
let friction_rule = combine_rule_priority(mat_a.friction_combine, mat_b.friction_combine);
let restitution_rule =
combine_rule_priority(mat_a.restitution_combine, mat_b.restitution_combine);
CombinedMaterial {
friction: friction_rule.apply(mat_a.dynamic_friction, mat_b.dynamic_friction),
restitution: restitution_rule.apply(mat_a.restitution, mat_b.restitution),
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.materials.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.materials.is_empty()
}
pub fn register_metal(&mut self) -> MaterialId {
self.register(PhysicsMaterial::new(
0,
Fix128::from_ratio(4, 10),
Fix128::from_ratio(1, 10),
))
}
pub fn register_wood(&mut self) -> MaterialId {
self.register(PhysicsMaterial::new(
0,
Fix128::from_ratio(5, 10),
Fix128::from_ratio(3, 10),
))
}
pub fn register_rubber(&mut self) -> MaterialId {
self.register(
PhysicsMaterial::new(0, Fix128::from_ratio(8, 10), Fix128::from_ratio(8, 10))
.with_combine_rules(CombineRule::Max, CombineRule::Max),
)
}
pub fn register_ice(&mut self) -> MaterialId {
self.register(
PhysicsMaterial::new(0, Fix128::from_ratio(5, 100), Fix128::from_ratio(1, 10))
.with_combine_rules(CombineRule::Min, CombineRule::Min),
)
}
pub fn register_concrete(&mut self) -> MaterialId {
self.register(PhysicsMaterial::new(
0,
Fix128::from_ratio(6, 10),
Fix128::from_ratio(2, 10),
))
}
}
impl Default for MaterialTable {
fn default() -> Self {
Self::new()
}
}
const fn combine_rule_priority(a: CombineRule, b: CombineRule) -> CombineRule {
const fn priority(r: CombineRule) -> u8 {
match r {
CombineRule::Min => 0,
CombineRule::Average => 1,
CombineRule::Multiply => 2,
CombineRule::Max => 3,
}
}
if priority(a) >= priority(b) {
a
} else {
b
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_combine_rules() {
let a = Fix128::from_ratio(4, 10);
let b = Fix128::from_ratio(8, 10);
let avg = CombineRule::Average.apply(a, b);
assert_eq!(avg.hi, 0);
let min = CombineRule::Min.apply(a, b);
assert!(min <= a);
let max = CombineRule::Max.apply(a, b);
assert!(max >= b);
}
#[test]
fn test_material_table() {
let mut table = MaterialTable::new();
let metal = table.register_metal();
let rubber = table.register_rubber();
let combined = table.combine(metal, rubber);
assert!(combined.friction > Fix128::ZERO);
}
#[test]
fn test_pair_override() {
let mut table = MaterialTable::new();
let a = table.register(PhysicsMaterial::new(
0,
Fix128::from_ratio(5, 10),
Fix128::from_ratio(5, 10),
));
let b = table.register(PhysicsMaterial::new(
0,
Fix128::from_ratio(5, 10),
Fix128::from_ratio(5, 10),
));
table.set_pair_override(a, b, Fix128::from_ratio(1, 100), Fix128::ZERO);
let combined = table.combine(a, b);
assert!(combined.friction < Fix128::from_ratio(1, 10));
}
#[test]
fn test_default_material() {
let table = MaterialTable::new();
let mat = table.get(DEFAULT_MATERIAL);
assert!(mat.dynamic_friction > Fix128::ZERO);
assert!(mat.restitution > Fix128::ZERO);
}
#[test]
fn test_combine_rule_priority() {
let result = combine_rule_priority(CombineRule::Min, CombineRule::Max);
assert_eq!(result, CombineRule::Max);
let result = combine_rule_priority(CombineRule::Average, CombineRule::Multiply);
assert_eq!(result, CombineRule::Multiply);
}
}