1use crate::error::PhysicsError;
14use crate::math::Fix128;
15
16#[cfg(not(feature = "std"))]
17use alloc::vec::Vec;
18
19pub type MaterialId = u16;
21
22pub const DEFAULT_MATERIAL: MaterialId = 0;
24
25pub(crate) const MATERIAL_ID_CAPACITY: usize = MaterialId::MAX as usize + 1;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
30pub enum CombineRule {
31 #[default]
33 Average,
34 Min,
36 Max,
38 Multiply,
40}
41
42impl CombineRule {
43 #[inline]
45 #[must_use]
46 pub fn apply(&self, a: Fix128, b: Fix128) -> Fix128 {
47 match self {
48 Self::Average => (a + b).half(),
49 Self::Min => {
50 if a < b {
51 a
52 } else {
53 b
54 }
55 }
56 Self::Max => {
57 if a > b {
58 a
59 } else {
60 b
61 }
62 }
63 Self::Multiply => a * b,
64 }
65 }
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct PhysicsMaterial {
71 pub id: MaterialId,
73 pub static_friction: Fix128,
75 pub dynamic_friction: Fix128,
77 pub restitution: Fix128,
79 pub friction_combine: CombineRule,
81 pub restitution_combine: CombineRule,
83}
84
85impl PhysicsMaterial {
86 #[must_use]
88 pub const fn new(id: MaterialId, friction: Fix128, restitution: Fix128) -> Self {
89 Self {
90 id,
91 static_friction: friction,
92 dynamic_friction: friction,
93 restitution,
94 friction_combine: CombineRule::Average,
95 restitution_combine: CombineRule::Average,
96 }
97 }
98
99 #[must_use]
101 pub const fn with_combine_rules(
102 mut self,
103 friction: CombineRule,
104 restitution: CombineRule,
105 ) -> Self {
106 self.friction_combine = friction;
107 self.restitution_combine = restitution;
108 self
109 }
110
111 #[must_use]
113 pub const fn with_static_friction(mut self, static_friction: Fix128) -> Self {
114 self.static_friction = static_friction;
115 self
116 }
117}
118
119impl Default for PhysicsMaterial {
120 fn default() -> Self {
121 Self::new(
122 DEFAULT_MATERIAL,
123 Fix128::from_ratio(5, 10),
124 Fix128::from_ratio(3, 10),
125 )
126 }
127}
128
129#[derive(Clone, Copy, Debug, PartialEq)]
131pub(crate) struct PairOverride {
132 pub(crate) mat_a: MaterialId,
133 pub(crate) mat_b: MaterialId,
134 pub(crate) friction: Fix128,
135 pub(crate) restitution: Fix128,
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub struct CombinedMaterial {
141 pub friction: Fix128,
143 pub restitution: Fix128,
145}
146
147pub struct MaterialTable {
149 pub(crate) materials: Vec<PhysicsMaterial>,
151 pub(crate) pair_overrides: Vec<PairOverride>,
153 pub default_friction_combine: CombineRule,
155 pub default_restitution_combine: CombineRule,
157}
158
159impl MaterialTable {
160 #[must_use]
162 pub fn new() -> Self {
163 let mut table = Self {
164 materials: Vec::new(),
165 pair_overrides: Vec::new(),
166 default_friction_combine: CombineRule::Average,
167 default_restitution_combine: CombineRule::Average,
168 };
169 table.register(PhysicsMaterial::default());
171 table
172 }
173
174 pub fn register(&mut self, material: PhysicsMaterial) -> MaterialId {
186 match self.try_register(material) {
187 Ok(id) => id,
188 Err(e) => panic!("MaterialTable::register: {e}; every MaterialId is in use"),
189 }
190 }
191
192 pub fn try_register(&mut self, material: PhysicsMaterial) -> Result<MaterialId, PhysicsError> {
205 let len = self.materials.len();
206 if len >= MATERIAL_ID_CAPACITY {
207 return Err(PhysicsError::CapacityExceeded {
208 resource: "materials",
209 limit: MATERIAL_ID_CAPACITY,
210 });
211 }
212 let id = len as MaterialId;
214 let mut mat = material;
215 mat.id = id;
216 self.materials.push(mat);
217 Ok(id)
218 }
219
220 #[must_use]
222 pub fn get(&self, id: MaterialId) -> &PhysicsMaterial {
223 self.materials
224 .get(id as usize)
225 .unwrap_or(&self.materials[0])
226 }
227
228 pub fn set_pair_override(
230 &mut self,
231 mat_a: MaterialId,
232 mat_b: MaterialId,
233 friction: Fix128,
234 restitution: Fix128,
235 ) {
236 let (a, b) = if mat_a <= mat_b {
237 (mat_a, mat_b)
238 } else {
239 (mat_b, mat_a)
240 };
241
242 if let Some(p) = self
244 .pair_overrides
245 .iter_mut()
246 .find(|p| p.mat_a == a && p.mat_b == b)
247 {
248 p.friction = friction;
249 p.restitution = restitution;
250 } else {
251 self.pair_overrides.push(PairOverride {
252 mat_a: a,
253 mat_b: b,
254 friction,
255 restitution,
256 });
257 }
258 }
259
260 #[must_use]
262 pub fn combine(&self, mat_a: MaterialId, mat_b: MaterialId) -> CombinedMaterial {
263 self.combine_with_surface(mat_a, None, mat_b, None)
264 }
265
266 #[must_use]
279 pub(crate) fn combine_with_surface(
280 &self,
281 mat_a: MaterialId,
282 surface_a: Option<(Fix128, Fix128)>,
283 mat_b: MaterialId,
284 surface_b: Option<(Fix128, Fix128)>,
285 ) -> CombinedMaterial {
286 let (a, b, surface_a, surface_b) = if mat_a <= mat_b {
287 (mat_a, mat_b, surface_a, surface_b)
288 } else {
289 (mat_b, mat_a, surface_b, surface_a)
290 };
291
292 if let Some(p) = self
294 .pair_overrides
295 .iter()
296 .find(|p| p.mat_a == a && p.mat_b == b)
297 {
298 return CombinedMaterial {
299 friction: p.friction,
300 restitution: p.restitution,
301 };
302 }
303
304 let mat_a = self.get(a);
306 let mat_b = self.get(b);
307
308 let friction_rule = combine_rule_priority(mat_a.friction_combine, mat_b.friction_combine);
310 let restitution_rule =
311 combine_rule_priority(mat_a.restitution_combine, mat_b.restitution_combine);
312
313 let (fa, ra) = surface_a.unwrap_or((mat_a.dynamic_friction, mat_a.restitution));
314 let (fb, rb) = surface_b.unwrap_or((mat_b.dynamic_friction, mat_b.restitution));
315 CombinedMaterial {
316 friction: friction_rule.apply(fa, fb),
317 restitution: restitution_rule.apply(ra, rb),
318 }
319 }
320
321 #[inline]
323 #[must_use]
324 pub fn len(&self) -> usize {
325 self.materials.len()
326 }
327
328 #[inline]
330 #[must_use]
331 pub fn is_empty(&self) -> bool {
332 self.materials.is_empty()
333 }
334
335 pub fn register_metal(&mut self) -> MaterialId {
339 self.register(PhysicsMaterial::new(
340 0,
341 Fix128::from_ratio(4, 10),
342 Fix128::from_ratio(1, 10),
343 ))
344 }
345
346 pub fn register_wood(&mut self) -> MaterialId {
348 self.register(PhysicsMaterial::new(
349 0,
350 Fix128::from_ratio(5, 10),
351 Fix128::from_ratio(3, 10),
352 ))
353 }
354
355 pub fn register_rubber(&mut self) -> MaterialId {
357 self.register(
358 PhysicsMaterial::new(0, Fix128::from_ratio(8, 10), Fix128::from_ratio(8, 10))
359 .with_combine_rules(CombineRule::Max, CombineRule::Max),
360 )
361 }
362
363 pub fn register_ice(&mut self) -> MaterialId {
365 self.register(
366 PhysicsMaterial::new(0, Fix128::from_ratio(5, 100), Fix128::from_ratio(1, 10))
367 .with_combine_rules(CombineRule::Min, CombineRule::Min),
368 )
369 }
370
371 pub fn register_concrete(&mut self) -> MaterialId {
373 self.register(PhysicsMaterial::new(
374 0,
375 Fix128::from_ratio(6, 10),
376 Fix128::from_ratio(2, 10),
377 ))
378 }
379}
380
381impl Default for MaterialTable {
382 fn default() -> Self {
383 Self::new()
384 }
385}
386
387const fn combine_rule_priority(a: CombineRule, b: CombineRule) -> CombineRule {
389 const fn priority(r: CombineRule) -> u8 {
390 match r {
391 CombineRule::Min => 0,
392 CombineRule::Average => 1,
393 CombineRule::Multiply => 2,
394 CombineRule::Max => 3,
395 }
396 }
397
398 if priority(a) >= priority(b) {
399 a
400 } else {
401 b
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_combine_rules() {
411 let a = Fix128::from_ratio(4, 10);
412 let b = Fix128::from_ratio(8, 10);
413
414 let avg = CombineRule::Average.apply(a, b);
415 assert_eq!(avg.hi, 0);
416 let min = CombineRule::Min.apply(a, b);
419 assert!(min <= a);
420
421 let max = CombineRule::Max.apply(a, b);
422 assert!(max >= b);
423 }
424
425 #[test]
426 fn test_material_table() {
427 let mut table = MaterialTable::new();
428 let metal = table.register_metal();
429 let rubber = table.register_rubber();
430
431 let combined = table.combine(metal, rubber);
432 assert!(combined.friction > Fix128::ZERO);
434 }
435
436 #[test]
437 fn test_pair_override() {
438 let mut table = MaterialTable::new();
439 let a = table.register(PhysicsMaterial::new(
440 0,
441 Fix128::from_ratio(5, 10),
442 Fix128::from_ratio(5, 10),
443 ));
444 let b = table.register(PhysicsMaterial::new(
445 0,
446 Fix128::from_ratio(5, 10),
447 Fix128::from_ratio(5, 10),
448 ));
449
450 table.set_pair_override(a, b, Fix128::from_ratio(1, 100), Fix128::ZERO);
452
453 let combined = table.combine(a, b);
454 assert!(combined.friction < Fix128::from_ratio(1, 10));
455 }
456
457 #[test]
458 fn test_default_material() {
459 let table = MaterialTable::new();
460 let mat = table.get(DEFAULT_MATERIAL);
461 assert!(mat.dynamic_friction > Fix128::ZERO);
462 assert!(mat.restitution > Fix128::ZERO);
463 }
464
465 #[test]
466 fn test_combine_rule_priority() {
467 let result = combine_rule_priority(CombineRule::Min, CombineRule::Max);
468 assert_eq!(result, CombineRule::Max);
469
470 let result = combine_rule_priority(CombineRule::Average, CombineRule::Multiply);
471 assert_eq!(result, CombineRule::Multiply);
472 }
473
474 fn near(a: Fix128, b: Fix128) -> bool {
476 (a - b).abs() < Fix128 { hi: 0, lo: 1 << 8 }
477 }
478
479 #[test]
480 fn register_wood_ice_concrete_store_documented_coefficients_and_rules() {
481 let mut table = MaterialTable::new();
482 let wood = table.register_wood();
483 let ice = table.register_ice();
484 let concrete = table.register_concrete();
485 assert_eq!((wood, ice, concrete), (1, 2, 3));
487 assert_eq!(table.len(), 4);
488 assert_eq!(table.get(wood).id, wood);
489 assert_eq!(table.get(ice).id, ice);
490 assert_eq!(table.get(concrete).id, concrete);
491
492 let w = table.get(wood);
493 assert_eq!(w.dynamic_friction, Fix128::from_ratio(5, 10));
494 assert_eq!(w.static_friction, Fix128::from_ratio(5, 10));
495 assert_eq!(w.restitution, Fix128::from_ratio(3, 10));
496 assert_eq!(w.friction_combine, CombineRule::Average);
497 assert_eq!(w.restitution_combine, CombineRule::Average);
498
499 let i = table.get(ice);
500 assert_eq!(i.dynamic_friction, Fix128::from_ratio(5, 100));
501 assert_eq!(i.restitution, Fix128::from_ratio(1, 10));
502 assert_eq!(i.friction_combine, CombineRule::Min);
503 assert_eq!(i.restitution_combine, CombineRule::Min);
504
505 let c = table.get(concrete);
506 assert_eq!(c.dynamic_friction, Fix128::from_ratio(6, 10));
507 assert_eq!(c.restitution, Fix128::from_ratio(2, 10));
508 assert_eq!(c.friction_combine, CombineRule::Average);
509 assert_eq!(c.restitution_combine, CombineRule::Average);
510
511 let wc = table.combine(wood, concrete);
513 assert!(near(wc.friction, Fix128::from_ratio(55, 100)), "{wc:?}");
514 assert!(near(wc.restitution, Fix128::from_ratio(25, 100)), "{wc:?}");
515 let ic = table.combine(ice, concrete);
517 assert!(near(ic.friction, Fix128::from_ratio(325, 1000)), "{ic:?}");
518 assert!(near(ic.restitution, Fix128::from_ratio(15, 100)), "{ic:?}");
519 let ii = table.combine(ice, ice);
521 assert_eq!(ii.friction, Fix128::from_ratio(5, 100));
522 assert_eq!(ii.restitution, Fix128::from_ratio(1, 10));
523 assert_eq!(table.combine(concrete, ice), ic);
525 }
526
527 #[test]
528 fn with_static_friction_only_changes_static_coefficient() {
529 let base = PhysicsMaterial::new(7, Fix128::from_ratio(3, 10), Fix128::from_ratio(2, 10));
530 let m = base.with_static_friction(Fix128::from_ratio(9, 10));
531 assert_eq!(m.static_friction, Fix128::from_ratio(9, 10));
532 assert_eq!(m.dynamic_friction, Fix128::from_ratio(3, 10));
533 assert_eq!(m.restitution, Fix128::from_ratio(2, 10));
534 assert_eq!(m.id, 7);
535 assert_eq!(m.friction_combine, base.friction_combine);
536 assert_eq!(m.restitution_combine, base.restitution_combine);
537 let mut expected = base;
539 expected.static_friction = Fix128::from_ratio(9, 10);
540 assert_eq!(m, expected);
541
542 let mut table = MaterialTable::new();
544 let plain = table.register(base);
545 let sticky = table.register(m);
546 let plain_pair = table.combine(plain, DEFAULT_MATERIAL);
547 let sticky_pair = table.combine(sticky, DEFAULT_MATERIAL);
548 assert_eq!(plain_pair, sticky_pair);
549 assert!(
551 near(sticky_pair.friction, Fix128::from_ratio(4, 10)),
552 "{sticky_pair:?}"
553 );
554 }
555}