Skip to main content

loam_runtime/
physics.rs

1use std::any::Any;
2use std::ops::Sub;
3
4use loam_math::Bivector;
5use loam_physics::collision::VectorOps;
6use loam_physics::{
7    BodyDef, BodyId, Collider, EditError, Narrowphase, PhysicsSpace, RigidBody, World, WorldState,
8};
9
10use crate::command::{Outcome, Rejection};
11use crate::domain::{
12    ChartCommand, ChartPose, DomainBuilder, DomainError, Facility, Homogeneous, Pose, TypedDomain,
13};
14use crate::entity::{Entity, EntityKey, SceneId};
15use crate::phase::Step;
16use crate::session::RestoreError;
17use crate::store::{Owner, SchemaId, Store};
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
20pub enum GrabHold {
21    /// Pulls the body's center, so carrying adds no spin.
22    #[default]
23    Center,
24    /// Pulls the grabbed point, so the body swings from it.
25    Anchor,
26}
27
28#[derive(Clone, Copy)]
29pub struct GrabConfig<S: PhysicsSpace> {
30    stiffness: f32,
31    max_carry_speed: f32,
32    max_acceleration: f32,
33    hold: GrabHold,
34    anchor_point: fn(S::Point, S::Point) -> S::Point,
35    constrain_target: fn(S::Point, S::Point) -> S::Point,
36}
37
38impl<S: PhysicsSpace> GrabConfig<S> {
39    pub fn new(stiffness: f32, max_carry_speed: f32, max_acceleration: f32) -> Self {
40        Self {
41            stiffness,
42            max_carry_speed,
43            max_acceleration,
44            hold: GrabHold::Center,
45            anchor_point: |_, point| point,
46            constrain_target: |_, point| point,
47        }
48    }
49
50    pub fn hold(mut self, hold: GrabHold) -> Self {
51        self.hold = hold;
52        self
53    }
54
55    pub fn anchor_point(mut self, anchor_point: fn(S::Point, S::Point) -> S::Point) -> Self {
56        self.anchor_point = anchor_point;
57        self
58    }
59
60    pub fn constrain_target(
61        mut self,
62        constrain_target: fn(S::Point, S::Point) -> S::Point,
63    ) -> Self {
64        self.constrain_target = constrain_target;
65        self
66    }
67}
68
69pub struct PhysicsConfig<S: PhysicsSpace> {
70    gravity: Option<S::Vector>,
71    register: fn(&mut Narrowphase<S>),
72    min_substeps: u32,
73    max_substeps: u32,
74    grab: Option<GrabConfig<S>>,
75    max_step_travel: Option<f32>,
76    motion_speed: fn(&RigidBody<S>) -> f32,
77}
78
79fn linear_speed<S>(body: &RigidBody<S>) -> f32
80where
81    S: PhysicsSpace,
82    S::Vector: VectorOps,
83{
84    body.velocity.length()
85}
86
87impl<S> PhysicsConfig<S>
88where
89    S: PhysicsSpace,
90    S::Vector: VectorOps,
91{
92    pub fn new(register: fn(&mut Narrowphase<S>)) -> Self {
93        Self {
94            gravity: None,
95            register,
96            min_substeps: 1,
97            max_substeps: 1,
98            grab: None,
99            max_step_travel: None,
100            motion_speed: linear_speed::<S>,
101        }
102    }
103
104    pub fn gravity(mut self, gravity: S::Vector) -> Self {
105        self.gravity = Some(gravity);
106        self
107    }
108
109    pub fn substeps(mut self, substeps: u32) -> Self {
110        self.min_substeps = substeps.max(1);
111        self.max_substeps = self.min_substeps;
112        self
113    }
114
115    pub fn adaptive_substeps(
116        mut self,
117        minimum: u32,
118        maximum: u32,
119        max_step_travel: f32,
120        motion_speed: fn(&RigidBody<S>) -> f32,
121    ) -> Self {
122        self.min_substeps = minimum.max(1);
123        self.max_substeps = maximum.max(self.min_substeps);
124        self.max_step_travel = Some(max_step_travel);
125        self.motion_speed = motion_speed;
126        self
127    }
128
129    pub fn grab(mut self, grab: GrabConfig<S>) -> Self {
130        self.grab = Some(grab);
131        self
132    }
133
134    fn validate(&self, space: &S) -> Result<(), EditError> {
135        if self
136            .gravity
137            .is_some_and(|gravity| !space.valid_vector(gravity))
138        {
139            return Err(EditError::InvalidGravity);
140        }
141        if let Some(grab) = &self.grab {
142            if !grab.stiffness.is_finite()
143                || grab.stiffness < 0.0
144                || grab.max_carry_speed.is_nan()
145                || grab.max_carry_speed < 0.0
146                || grab.max_acceleration.is_nan()
147                || grab.max_acceleration < 0.0
148            {
149                return Err(EditError::InvalidGrabConfig);
150            }
151        }
152        if self
153            .max_step_travel
154            .is_some_and(|travel| travel.is_nan() || travel <= 0.0)
155        {
156            return Err(EditError::InvalidStepTravel);
157        }
158        Ok(())
159    }
160}
161
162struct Held<S: PhysicsSpace> {
163    body: BodyId,
164    target: S::Point,
165    anchor: S::Vector,
166    offset: S::Vector,
167}
168
169struct Released<S: PhysicsSpace> {
170    body: BodyId,
171    anchor: S::Vector,
172}
173
174impl<S: PhysicsSpace> Clone for Held<S> {
175    fn clone(&self) -> Self {
176        *self
177    }
178}
179
180impl<S: PhysicsSpace> Copy for Held<S> {}
181
182impl<S: PhysicsSpace> Clone for Released<S> {
183    fn clone(&self) -> Self {
184        *self
185    }
186}
187
188impl<S: PhysicsSpace> Copy for Released<S> {}
189
190pub struct Physics<S: PhysicsSpace> {
191    scene: SceneId,
192    world: World<S>,
193    to_body: Vec<Option<(u32, BodyId)>>,
194    to_entity: Vec<Option<(u32, EntityKey)>>,
195    held: Option<Held<S>>,
196    released: Option<Released<S>>,
197    min_substeps: u32,
198    max_substeps: u32,
199    grab: Option<GrabConfig<S>>,
200    max_step_travel: Option<f32>,
201    motion_speed: fn(&RigidBody<S>) -> f32,
202}
203
204struct PhysicsSnapshot<S: PhysicsSpace> {
205    world: WorldState<S>,
206    to_entity: Vec<Option<(u32, EntityKey)>>,
207}
208
209fn body_at(to_body: &[Option<(u32, BodyId)>], key: EntityKey) -> Option<BodyId> {
210    match to_body.get(key.slot() as usize)? {
211        Some((generation, body)) if *generation == key.generation() => Some(*body),
212        _ => None,
213    }
214}
215
216fn entity_at(to_entity: &[Option<(u32, EntityKey)>], body: BodyId) -> Option<EntityKey> {
217    match to_entity.get(body.slot() as usize)? {
218        Some((generation, key)) if *generation == body.generation() => Some(*key),
219        _ => None,
220    }
221}
222
223fn put<T: Copy>(slots: &mut Vec<Option<T>>, index: usize, value: Option<T>) {
224    if index >= slots.len() {
225        if value.is_none() {
226            return;
227        }
228        slots.resize(index + 1, None);
229    }
230    slots[index] = value;
231}
232
233fn clamp_length<V: VectorOps>(vector: V, maximum: f32) -> V {
234    let length = vector.length();
235    if length > maximum {
236        vector * (maximum / length)
237    } else {
238        vector
239    }
240}
241
242fn domain_edit(entity: Entity, error: EditError) -> DomainError {
243    match error {
244        EditError::StaleHandle => DomainError::Stale(entity),
245        error => error.into(),
246    }
247}
248
249fn domain_rejection(entity: Entity, rejection: Rejection) -> DomainError {
250    match rejection {
251        Rejection::Domain(error) => error,
252        Rejection::Edit(error) => domain_edit(entity, error),
253        _ => DomainError::Unsupported("physics pose edit"),
254    }
255}
256
257impl<S> Physics<S>
258where
259    S: Homogeneous + PhysicsSpace + Copy,
260    S::Vector: VectorOps + Default,
261    S::Point: Sub<Output = S::Vector>,
262{
263    pub fn world(&self) -> &World<S> {
264        &self.world
265    }
266
267    pub fn narrowphase_mut(&mut self) -> &mut Narrowphase<S> {
268        &mut self.world.narrowphase
269    }
270
271    pub fn set_solver_iterations(&mut self, iterations: usize) {
272        self.world.set_solver_iterations(iterations);
273    }
274
275    /// Has no effect on a domain built without a grab.
276    pub fn set_grab_hold(&mut self, hold: GrabHold) {
277        if let Some(grab) = &mut self.grab {
278            grab.hold = hold;
279        }
280    }
281
282    pub fn set_gravity(&mut self, gravity: Option<S::Vector>) -> Result<(), EditError> {
283        self.world.set_gravity(gravity)
284    }
285
286    pub fn set_solver_tolerance(&mut self, tolerance: f32) -> Result<(), EditError> {
287        self.world.set_solver_tolerance(tolerance)
288    }
289
290    pub fn body(&self, entity: Entity) -> Option<BodyId> {
291        if entity.scene() != self.scene {
292            return None;
293        }
294        body_at(&self.to_body, entity.key())
295    }
296
297    pub fn entity(&self, body: BodyId) -> Option<Entity> {
298        entity_at(&self.to_entity, body).map(|key| Entity::new(self.scene, key))
299    }
300
301    pub fn set_velocity(
302        &mut self,
303        entity: Entity,
304        velocity: S::Vector,
305        angular_velocity: S::AngVel,
306    ) -> Result<(), EditError> {
307        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
308        self.world.set_velocity(id, velocity, angular_velocity)
309    }
310
311    pub fn set_restitution(&mut self, entity: Entity, restitution: f32) -> Result<(), EditError> {
312        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
313        self.world.set_restitution(id, restitution)
314    }
315
316    pub fn set_collision_filter(
317        &mut self,
318        entity: Entity,
319        group: u32,
320        mask: u32,
321    ) -> Result<(), EditError> {
322        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
323        self.world.set_collision_filter(id, group, mask)
324    }
325
326    pub fn set_mass_properties(
327        &mut self,
328        entity: Entity,
329        mass: f32,
330        inertia: S::Inertia,
331    ) -> Result<(), EditError> {
332        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
333        self.world.set_mass_properties(id, mass, inertia)
334    }
335
336    pub fn set_collider(
337        &mut self,
338        entity: Entity,
339        collider: Collider,
340        inertia: S::Inertia,
341    ) -> Result<(), EditError> {
342        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
343        self.world.set_collider(id, collider, inertia)
344    }
345
346    pub fn sleep_body(&mut self, entity: Entity) -> Result<(), EditError> {
347        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
348        self.world.sleep_body(id)
349    }
350
351    pub fn wake_body(&mut self, entity: Entity) -> Result<(), EditError> {
352        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
353        self.world.wake_body(id)
354    }
355
356    pub fn is_held(&self, entity: Entity) -> bool {
357        self.body(entity)
358            .is_some_and(|body| self.held.is_some_and(|held| held.body == body))
359    }
360
361    pub fn held_anchor(&self, entity: Entity) -> Option<S::Point> {
362        let body = self.body(entity)?;
363        let held = self.held.filter(|held| held.body == body)?;
364        let row = self.world.body(body)?;
365        let lever = self
366            .world
367            .space()
368            .iso_transport(row.orientation, row.position, held.anchor);
369        let point = self.world.space().exp(row.position, lever);
370        self.world.space().valid_point(point).then_some(point)
371    }
372
373    pub fn throw(&mut self, entity: Entity, velocity: S::Vector) -> Result<(), EditError> {
374        let id = self.body(entity).ok_or(EditError::StaleHandle)?;
375        let angular = self
376            .world
377            .body(id)
378            .ok_or(EditError::StaleHandle)?
379            .angular_velocity;
380        self.world.set_velocity(id, velocity, angular)?;
381        if self.released.is_some_and(|released| released.body == id) {
382            self.released = None;
383        }
384        Ok(())
385    }
386
387    /// The grab point of the body released last, moved with the body since.
388    pub fn released_anchor(&self, entity: Entity) -> Option<S::Point> {
389        let body = self.body(entity)?;
390        let released = self.released.filter(|released| released.body == body)?;
391        let row = self.world.body(body)?;
392        let lever =
393            self.world
394                .space()
395                .iso_transport(row.orientation, row.position, released.anchor);
396        let point = self.world.space().exp(row.position, lever);
397        self.world.space().valid_point(point).then_some(point)
398    }
399
400    pub(crate) fn spawn(&mut self, entity: Entity, body: BodyDef<S>) -> BodyId {
401        let _ = self.despawn(entity);
402        let id = self.world.push_body(body);
403        let key = entity.key();
404        put(
405            &mut self.to_body,
406            key.slot() as usize,
407            Some((key.generation(), id)),
408        );
409        put(
410            &mut self.to_entity,
411            id.slot() as usize,
412            Some((id.generation(), key)),
413        );
414        id
415    }
416
417    pub fn despawn(&mut self, entity: Entity) -> Option<BodyId> {
418        let id = self.body(entity)?;
419        if self.held.is_some_and(|held| held.body == id) {
420            self.held = None;
421        }
422        if self.released.is_some_and(|released| released.body == id) {
423            self.released = None;
424        }
425        let _ = self.world.despawn_body(id);
426        put(&mut self.to_body, entity.key().slot() as usize, None);
427        put(&mut self.to_entity, id.slot() as usize, None);
428        Some(id)
429    }
430
431    fn mirror_entity(&self, id: BodyId, poses: &Store<Pose<S>>) -> Result<Entity, EditError> {
432        let entity = self.entity(id).ok_or(EditError::StaleHandle)?;
433        if entity.scene() != poses.scene() {
434            return Err(EditError::StaleHandle);
435        }
436        poses.get(entity).ok_or(EditError::StaleHandle)?;
437        Ok(entity)
438    }
439
440    fn mirror_pose(&self, id: BodyId, poses: &mut Store<Pose<S>>) -> Result<(), EditError> {
441        let entity = self.mirror_entity(id, poses)?;
442        let row = self.world.body(id).ok_or(EditError::StaleHandle)?;
443        let pose = poses.get_mut(entity).ok_or(EditError::StaleHandle)?;
444        *pose = self.world.space().pose_of(self.world.space().iso_compose(
445            self.world.space().transvection(row.position),
446            row.orientation,
447        ));
448        Ok(())
449    }
450
451    pub(crate) fn check_pose(&self, pose: Pose<S>) -> Result<(), EditError> {
452        let (position, orientation) = self.physics_pose(pose);
453        if !self.world.space().valid_point(position)
454            || !self.world.space().valid_orientation(orientation)
455        {
456            return Err(EditError::NotFinite);
457        }
458        Ok(())
459    }
460
461    fn physics_pose(&self, pose: Pose<S>) -> (S::Point, S::Iso) {
462        let space = *self.world.space();
463        let orientation = space.iso_compose(
464            space.iso_inverse(space.transvection(pose.point)),
465            space.iso_of(&pose),
466        );
467        (pose.point, orientation)
468    }
469
470    pub(crate) fn replace_pose(
471        &mut self,
472        id: BodyId,
473        pose: Pose<S>,
474        poses: &mut Store<Pose<S>>,
475    ) -> Result<(), EditError> {
476        self.mirror_entity(id, poses)?;
477        let (position, orientation) = self.physics_pose(pose);
478        self.world.set_pose(id, position, orientation)?;
479        self.mirror_pose(id, poses)
480    }
481
482    fn place(
483        &mut self,
484        id: BodyId,
485        pose: &ChartPose,
486        poses: &mut Store<Pose<S>>,
487    ) -> Result<Outcome, Rejection> {
488        let pose = self.world.space().pose_from_chart(pose)?;
489        self.replace_pose(id, pose, poses)
490            .map_err(Rejection::Edit)?;
491        Ok(Outcome::Done)
492    }
493
494    fn move_to(&mut self, id: BodyId, position: S::Point) -> Result<bool, Rejection> {
495        let orientation = self
496            .world
497            .body(id)
498            .ok_or(Rejection::Edit(EditError::StaleHandle))?
499            .orientation;
500        if !self.world.space().valid_point(position) {
501            return Err(Rejection::Edit(EditError::NotFinite));
502        }
503        if let Some(held) = &mut self.held {
504            if held.body == id {
505                let target = self.grab.map_or(position, |grab| {
506                    (grab.constrain_target)(held.target, position)
507                });
508                if !self.world.space().valid_point(target) {
509                    return Err(Rejection::Edit(EditError::NotFinite));
510                }
511                held.target = target;
512                return Ok(false);
513            }
514        }
515        self.world
516            .set_pose(id, position, orientation)
517            .map_err(Rejection::Edit)?;
518        Ok(true)
519    }
520
521    fn move_to_and_mirror(
522        &mut self,
523        id: BodyId,
524        point: S::Point,
525        poses: &mut Store<Pose<S>>,
526    ) -> Result<(), Rejection> {
527        self.mirror_entity(id, poses).map_err(Rejection::Edit)?;
528        if self.move_to(id, point)? {
529            self.mirror_pose(id, poses).map_err(Rejection::Edit)?;
530        }
531        Ok(())
532    }
533
534    fn grab(&mut self, id: BodyId, point: S::Point) -> Result<Outcome, Rejection> {
535        let Some(grab) = self.grab else {
536            return Ok(Outcome::Done);
537        };
538        let row = self
539            .world
540            .body(id)
541            .ok_or(Rejection::Edit(EditError::StaleHandle))?;
542        let target = row.position;
543        let anchor = (grab.anchor_point)(target, point);
544        if !self.world.space().valid_point(anchor) {
545            return Err(Rejection::Edit(EditError::NotFinite));
546        }
547        let lever = self.world.space().log(target, anchor);
548        let local_anchor = self.world.space().iso_transport(
549            self.world.space().iso_inverse(row.orientation),
550            target,
551            lever,
552        );
553        if !self.world.space().valid_vector(local_anchor) {
554            return Err(Rejection::Edit(EditError::NotFinite));
555        }
556        self.world
557            .set_velocity(id, S::Vector::default(), S::AngVel::zero())
558            .map_err(Rejection::Edit)?;
559        self.released = None;
560        self.held = Some(Held {
561            body: id,
562            target,
563            anchor: local_anchor,
564            offset: lever,
565        });
566        Ok(Outcome::Done)
567    }
568
569    fn release_grab(&mut self, id: BodyId) -> Result<Outcome, Rejection> {
570        if let Some(held) = self.held.filter(|held| held.body == id) {
571            self.held = None;
572            self.released = Some(Released {
573                body: held.body,
574                anchor: held.anchor,
575            });
576        }
577        Ok(Outcome::Done)
578    }
579
580    fn drive_held(&mut self, dt: f32) -> Result<(), DomainError> {
581        let Some(held) = self.held else {
582            return Ok(());
583        };
584        let Some(grab) = self.grab else {
585            return Ok(());
586        };
587        let space = *self.world.space();
588        let row = *self.world.body(held.body).ok_or(EditError::StaleHandle)?;
589        if grab.hold == GrabHold::Center {
590            let desired = clamp_length(
591                space.log(row.position, held.target) * grab.stiffness,
592                grab.max_carry_speed,
593            );
594            let acceleration = clamp_length(desired - row.velocity, grab.max_acceleration * dt);
595            return self
596                .world
597                .set_velocity(held.body, row.velocity + acceleration, row.angular_velocity)
598                .map_err(DomainError::from);
599        }
600        let lever = space.iso_transport(row.orientation, row.position, held.anchor);
601        let point = space.exp(row.position, lever);
602        let goal = space.exp(held.target, held.offset);
603        let moving = space.velocity_at_point(&row, point);
604        let desired = clamp_length(
605            space.log(point, goal) * grab.stiffness,
606            grab.max_carry_speed,
607        );
608        let change = clamp_length(desired - moving, grab.max_acceleration * dt);
609        let size = change.length();
610        if size <= 0.0 {
611            return Ok(());
612        }
613        let direction = change * (1.0 / size);
614        let mut probe = row;
615        probe.apply_impulse_at_point(&space, direction, point);
616        let response = (space.velocity_at_point(&probe, point) - moving).dot(direction);
617        if response <= 0.0 {
618            return Ok(());
619        }
620        self.world
621            .apply_impulse_at_point(held.body, direction * (size / response), point)
622            .map_err(DomainError::from)
623    }
624
625    fn step_count(&self, dt: f32) -> u32 {
626        if self.min_substeps == self.max_substeps {
627            return self.min_substeps;
628        }
629        let Some(max_step_travel) = self.max_step_travel else {
630            return self.min_substeps;
631        };
632        let fastest = self
633            .world
634            .bodies()
635            .iter()
636            .map(|body| (self.motion_speed)(body))
637            .fold(0.0f32, f32::max);
638        let needed = (fastest * dt / max_step_travel).ceil() as u32;
639        needed.clamp(self.min_substeps, self.max_substeps)
640    }
641
642    fn sync_poses(&mut self, poses: &mut Store<Pose<S>>, rewrite: bool) {
643        let space = *self.world.space();
644        let scene = poses.scene();
645        let to_entity = &self.to_entity;
646        for (id, row) in self.world.drain_dirty() {
647            let Some(key) = entity_at(to_entity, id) else {
648                continue;
649            };
650            let entity = Entity::new(scene, key);
651            let next =
652                space.pose_of(space.iso_compose(space.transvection(row.position), row.orientation));
653            if !rewrite
654                && poses
655                    .get(entity)
656                    .is_some_and(|pose| space.chart_pose(pose) == space.chart_pose(&next))
657            {
658                continue;
659            }
660            let Some(pose) = poses.get_mut(entity) else {
661                continue;
662            };
663            *pose = next;
664        }
665    }
666}
667
668impl<S> Facility<S> for Physics<S>
669where
670    S: Homogeneous + PhysicsSpace + Copy,
671    S::Vector: VectorOps + Default,
672    S::AngVel: PartialEq,
673    S::Point: Sub<Output = S::Vector>,
674{
675    fn name(&self) -> &'static str {
676        "physics"
677    }
678
679    fn bind(&mut self, scene: SceneId, _owner: Owner) {
680        self.scene = scene;
681    }
682
683    fn step(
684        &mut self,
685        poses: &mut Store<Pose<S>>,
686        step: Step,
687        _owner: Owner,
688    ) -> Result<(), DomainError> {
689        if !step.dt.is_finite() || step.dt < 0.0 {
690            return Err(EditError::InvalidTimeStep.into());
691        }
692        self.drive_held(step.dt)?;
693        let substeps = self.step_count(step.dt);
694        let dt = step.dt / substeps as f32;
695        for _ in 0..substeps {
696            self.world.step(dt)?;
697        }
698        self.sync_poses(poses, true);
699        self.released = None;
700        Ok(())
701    }
702
703    fn synchronize(&mut self, poses: &mut Store<Pose<S>>, _owner: Owner) {
704        self.sync_poses(poses, false);
705    }
706
707    fn set_pose(
708        &mut self,
709        entity: Entity,
710        pose: Pose<S>,
711        poses: &mut Store<Pose<S>>,
712        _owner: Owner,
713    ) -> Option<Result<(), DomainError>> {
714        let id = self.body(entity)?;
715        Some(
716            self.replace_pose(id, pose, poses)
717                .map_err(|error| domain_edit(entity, error)),
718        )
719    }
720
721    fn move_to(
722        &mut self,
723        entity: Entity,
724        point: S::Point,
725        poses: &mut Store<Pose<S>>,
726        _owner: Owner,
727    ) -> Option<Result<(), DomainError>> {
728        let id = self.body(entity)?;
729        Some(
730            self.move_to_and_mirror(id, point, poses)
731                .map_err(|error| domain_rejection(entity, error)),
732        )
733    }
734
735    fn snapshot(&self, _owner: Owner) -> Box<dyn Any + Send> {
736        Box::new(PhysicsSnapshot::<S> {
737            world: self.world.snapshot(),
738            to_entity: self.to_entity.clone(),
739        })
740    }
741
742    fn check_restore(&self, from: &(dyn Any + Send), _owner: Owner) -> Result<(), RestoreError> {
743        let from = from
744            .downcast_ref::<PhysicsSnapshot<S>>()
745            .ok_or_else(|| RestoreError::Schema(SchemaId::of::<PhysicsSnapshot<S>>()))?;
746        self.world
747            .check_restore(&from.world)
748            .map_err(RestoreError::Edit)
749    }
750
751    fn restore(&mut self, from: &(dyn Any + Send), _owner: Owner) -> Result<(), RestoreError> {
752        let from = from
753            .downcast_ref::<PhysicsSnapshot<S>>()
754            .ok_or_else(|| RestoreError::Schema(SchemaId::of::<PhysicsSnapshot<S>>()))?;
755        self.world
756            .restore(&from.world)
757            .map_err(RestoreError::Edit)?;
758        self.to_entity.clear();
759        self.to_entity.extend_from_slice(&from.to_entity);
760        self.held = None;
761        self.released = None;
762        for slot in &mut self.to_body {
763            *slot = None;
764        }
765        for dense in 0..self.world.bodies().len() {
766            let id = self.world.bodies().id_at(dense);
767            let Some(key) = entity_at(&self.to_entity, id) else {
768                continue;
769            };
770            put(
771                &mut self.to_body,
772                key.slot() as usize,
773                Some((key.generation(), id)),
774            );
775        }
776        Ok(())
777    }
778
779    fn release(&mut self, entity: Entity, _owner: Owner) {
780        let _ = self.despawn(entity);
781    }
782
783    fn apply(
784        &mut self,
785        command: &ChartCommand,
786        poses: &mut Store<Pose<S>>,
787        _owner: Owner,
788    ) -> Option<Result<Outcome, Rejection>> {
789        match command {
790            ChartCommand::Place { entity, pose } => {
791                let id = self.body(*entity)?;
792                Some(self.place(id, pose, poses))
793            }
794            ChartCommand::Grab { entity, point } => {
795                let id = self.body(*entity)?;
796                let point = match self.world.space().point_from_chart(point) {
797                    Ok(point) => point,
798                    Err(error) => return Some(Err(error.into())),
799                };
800                Some(self.grab(id, point))
801            }
802            ChartCommand::Release { entity } => {
803                let id = self.body(*entity)?;
804                Some(self.release_grab(id))
805            }
806            _ => None,
807        }
808    }
809}
810
811impl<S> DomainBuilder<S>
812where
813    S: Homogeneous + PhysicsSpace + Copy,
814    S::Vector: VectorOps + Default,
815    S::AngVel: PartialEq,
816    S::Point: Sub<Output = S::Vector>,
817{
818    pub fn physics(self, config: PhysicsConfig<S>) -> Result<Self, EditError> {
819        config.validate(&self.space)?;
820        let mut world = World::new(self.space);
821        world.set_gravity(config.gravity)?;
822        (config.register)(&mut world.narrowphase);
823        Ok(self.facility(Physics {
824            scene: SceneId::UNBOUND,
825            world,
826            to_body: Vec::new(),
827            to_entity: Vec::new(),
828            held: None,
829            released: None,
830            min_substeps: config.min_substeps,
831            max_substeps: config.max_substeps,
832            grab: config.grab,
833            max_step_travel: config.max_step_travel,
834            motion_speed: config.motion_speed,
835        }))
836    }
837}
838
839impl<S> TypedDomain<S>
840where
841    S: Homogeneous + PhysicsSpace + Copy,
842    S::Vector: VectorOps + Default,
843    S::Point: Sub<Output = S::Vector>,
844{
845    pub fn physics(&self) -> Option<&Physics<S>> {
846        self.facilities
847            .iter()
848            .find_map(|facility| (facility.as_ref() as &dyn Any).downcast_ref::<Physics<S>>())
849    }
850
851    pub fn physics_mut(&mut self) -> Option<&mut Physics<S>> {
852        self.facilities
853            .iter_mut()
854            .find_map(|facility| (facility.as_mut() as &mut dyn Any).downcast_mut::<Physics<S>>())
855    }
856}
857
858#[cfg(test)]
859mod tests {
860    use loam_math::{EuclideanR4, Rotor4};
861    use loam_physics::euclidean_r4::{register_default_narrowphase, sphere_body_r4};
862    use loam_time::alloc::bytes_allocated_by;
863
864    use super::*;
865    use crate::command::{Command, SpawnBundle};
866    use crate::domain::{DomainId, DomainOwner, DomainSpace, Instance};
867    use crate::entity::{Entities, Epoch, RuntimeId, SceneId};
868    use crate::input::Input;
869    use crate::phase::Tick;
870    use crate::session::{Material, PreparedGeometry, Publication, Session, SimConfig};
871    use crate::store::DEFAULT_LOG_CAPACITY;
872    use crate::view::{Section4, Vec4, ViewSpec};
873
874    crate::stores! {
875        #[derive(Default)]
876        pub struct Bare {}
877    }
878
879    const MOVING: usize = 64;
880    const WARM_STEPS: u64 = 8;
881
882    fn step_at(tick: u64) -> Step {
883        Step {
884            tick: Tick(tick),
885            dt: 1.0 / 60.0,
886        }
887    }
888
889    #[test]
890    fn a_constrained_grab_does_not_drop_its_release_lever() {
891        let scene = SceneId {
892            runtime: RuntimeId::allocate(),
893            epoch: Epoch::default(),
894        };
895        let mut entities = Entities::new(scene);
896        let mut domain = DomainBuilder::new("r4", EuclideanR4)
897            .tracked(DEFAULT_LOG_CAPACITY)
898            .physics(
899                PhysicsConfig::new(register_default_narrowphase).grab(
900                    GrabConfig::new(20.0, 20.0, 400.0)
901                        .anchor_point(|body: Vec4, picked: Vec4| {
902                            Vec4::new(picked.x, picked.y, picked.z, body.w)
903                        })
904                        .constrain_target(|current: Vec4, target: Vec4| {
905                            Vec4::new(
906                                target.x.clamp(-1.0, 1.0),
907                                target.y.max(0.5),
908                                target.z.clamp(-1.0, 1.0),
909                                current.w,
910                            )
911                        }),
912                ),
913            )
914            .unwrap()
915            .build(DomainId::new(0), scene);
916        let entity = entities.spawn();
917        let start = Vec4::new(0.0, 2.0, 0.0, 0.75);
918        domain.attach_pose(entity, Pose::at(start)).expect("pose");
919        let physics = domain.physics_mut().expect("physics");
920        let id = physics.spawn(
921            entity,
922            sphere_body_r4(start, Vec4::ZERO, 0.5, 1.0).expect("body"),
923        );
924        let mut orientation = physics.world.body(id).expect("body").orientation;
925        orientation.rotation = Rotor4::from_rotation_arc(Vec4::Y, Vec4::W);
926        physics
927            .world
928            .set_pose(id, start, orientation)
929            .expect("pose");
930        physics
931            .grab(id, Vec4::new(0.0, 2.3, 0.0, -4.0))
932            .expect("grab");
933        physics
934            .move_to(id, Vec4::new(9.0, -9.0, 0.0, 7.0))
935            .expect("move");
936        assert_eq!(
937            physics.held.expect("held").target,
938            Vec4::new(1.0, 0.5, 0.0, 0.75)
939        );
940        physics.release_grab(id).expect("release");
941        let handle = physics.released_anchor(entity).expect("released");
942        assert!(
943            (handle - Vec4::new(0.0, 2.3, 0.0, 0.75)).length() < 1e-5,
944            "the release lost the grabbed handle: {handle}"
945        );
946    }
947
948    #[test]
949    fn the_grab_hold_decides_whether_an_off_center_body_swings() {
950        let lowest = |hold: GrabHold| {
951            let scene = SceneId {
952                runtime: RuntimeId::allocate(),
953                epoch: Epoch::default(),
954            };
955            let mut entities = Entities::new(scene);
956            let mut domain = DomainBuilder::new("r4", EuclideanR4)
957                .tracked(DEFAULT_LOG_CAPACITY)
958                .physics(
959                    PhysicsConfig::new(register_default_narrowphase)
960                        .gravity(Vec4::NEG_Y * 9.8)
961                        .substeps(4)
962                        .grab(GrabConfig::new(20.0, 20.0, 400.0).hold(hold)),
963                )
964                .unwrap()
965                .build(DomainId::new(0), scene);
966            let entity = entities.spawn();
967            domain
968                .attach_pose(entity, Pose::at(Vec4::ZERO))
969                .expect("pose");
970            let physics = domain.physics_mut().expect("physics");
971            let id = physics.spawn(
972                entity,
973                sphere_body_r4(Vec4::ZERO, Vec4::ZERO, 0.5, 1.0).expect("body"),
974            );
975            physics.grab(id, Vec4::X * 0.4).expect("grab");
976            let mut lowest = f32::INFINITY;
977            for tick in 0..120 {
978                domain.step(step_at(tick)).expect("step");
979                let physics = domain.physics().expect("physics");
980                lowest = lowest.min(physics.world.body(id).expect("body").position.y);
981            }
982            let grip = domain
983                .physics()
984                .expect("physics")
985                .held_anchor(entity)
986                .expect("held");
987            assert!(
988                (grip - Vec4::X * 0.4).length() < 0.1,
989                "the {hold:?} hold let the grab point drift to {grip}"
990            );
991            lowest
992        };
993        let swung = lowest(GrabHold::Anchor);
994        assert!(
995            swung < -0.3,
996            "the anchor hold never swung the body under its grab point; its center bottomed out at y {swung}"
997        );
998        let carried = lowest(GrabHold::Center);
999        assert!(
1000            carried > -0.05,
1001            "the center hold let the body swing down to y {carried}"
1002        );
1003    }
1004
1005    #[test]
1006    fn a_held_body_transfers_motion_through_sleeping_contacts() {
1007        let scene = SceneId {
1008            runtime: RuntimeId::allocate(),
1009            epoch: Epoch::default(),
1010        };
1011        let mut entities = Entities::new(scene);
1012        let mut domain = DomainBuilder::new("r4", EuclideanR4)
1013            .tracked(DEFAULT_LOG_CAPACITY)
1014            .physics(
1015                PhysicsConfig::new(register_default_narrowphase)
1016                    .substeps(4)
1017                    .grab(GrabConfig::new(20.0, 20.0, 400.0)),
1018            )
1019            .unwrap()
1020            .build(DomainId::new(0), scene);
1021        let held_at = Vec4::ZERO;
1022        let far_neighbor_at = Vec4::X * 2.0;
1023        let [held_body, neighbor_body, far_neighbor_body] = [0.0, 1.0, 2.0].map(|x| {
1024            let entity = entities.spawn();
1025            let at = Vec4::X * x;
1026            domain.attach_pose(entity, Pose::at(at)).expect("pose");
1027            domain.physics_mut().expect("physics").spawn(
1028                entity,
1029                sphere_body_r4(at, Vec4::ZERO, 0.5, 1.0).expect("body"),
1030            )
1031        });
1032        let physics = domain.physics_mut().expect("physics");
1033        physics.world.sleep_body(neighbor_body).expect("sleep");
1034        physics.world.sleep_body(far_neighbor_body).expect("sleep");
1035        physics.grab(held_body, held_at).expect("grab");
1036        physics.move_to(held_body, Vec4::X * 2.0).expect("move");
1037
1038        for tick in 0..4 {
1039            domain.step(step_at(tick)).expect("step");
1040        }
1041
1042        let physics = domain.physics().expect("physics");
1043        let neighbor = physics.world.body(neighbor_body).expect("neighbor");
1044        let far_neighbor = physics.world.body(far_neighbor_body).expect("far neighbor");
1045        assert!(
1046            !neighbor.is_sleeping() && !far_neighbor.is_sleeping(),
1047            "the struck chain stayed asleep"
1048        );
1049        assert!(
1050            far_neighbor.position.x > far_neighbor_at.x || far_neighbor.velocity.x > 0.1,
1051            "the held body transferred no motion through the chain: position {} velocity {}",
1052            far_neighbor.position.x,
1053            far_neighbor.velocity.x
1054        );
1055    }
1056
1057    #[test]
1058    fn a_held_body_dragged_and_thrown_between_ticks_keeps_its_blend() {
1059        let mut session = Session::new(Bare::default(), SimConfig::default());
1060        let r4 = session.register_domain(
1061            DomainBuilder::new("r4", EuclideanR4)
1062                .tracked(DEFAULT_LOG_CAPACITY)
1063                .physics(
1064                    PhysicsConfig::new(register_default_narrowphase)
1065                        .grab(GrabConfig::new(20.0, 20.0, 400.0)),
1066                )
1067                .unwrap(),
1068        );
1069        let geometry = session.prepare(PreparedGeometry::Lines4 {
1070            segments: vec![[[0.1, 0.0, 0.0, 0.0], [-0.1, 0.0, 0.0, 0.0]]],
1071        });
1072        let material = session.add_material(Material::flat([1.0; 4]));
1073        let root = session.views().root();
1074        let body = session.dispatch(|dispatch| {
1075            let eye = dispatch
1076                .spawn(SpawnBundle::new().at(r4, Pose::at(Vec4::Z * 4.0)))
1077                .unwrap();
1078            let body = dispatch
1079                .spawn(
1080                    SpawnBundle::new()
1081                        .at(r4, Pose::at(Vec4::ZERO))
1082                        .instance(Instance::new(geometry, material)),
1083                )
1084                .unwrap();
1085            let domain = dispatch.domains.typed(r4).unwrap();
1086            domain
1087                .add_view(ViewSpec::new(root, eye, Section4 { w: 0.0 }))
1088                .unwrap();
1089            domain
1090                .spawn_body(
1091                    body,
1092                    sphere_body_r4(Vec4::ZERO, Vec4::ZERO, 0.5, 1.0).unwrap(),
1093                )
1094                .unwrap();
1095            body
1096        });
1097        let chart = |x: f32| EuclideanR4.chart_point(Vec4::X * x);
1098        session.submit(Command::Chart(
1099            r4.id(),
1100            ChartCommand::Grab {
1101                entity: body,
1102                point: chart(0.0),
1103            },
1104        ));
1105        session.submit(Command::Chart(
1106            r4.id(),
1107            ChartCommand::Move {
1108                entity: body,
1109                point: chart(5.0),
1110            },
1111        ));
1112        session.boundary(Input::default()).unwrap();
1113        session.tick().unwrap();
1114        let mut publication = Publication::default();
1115        let mut drawn_x = |session: &mut Session<Bare>| {
1116            session.publish_at(&mut publication, 0.5).unwrap();
1117            publication.views[0].records.instances.rows()[0]
1118                .pose
1119                .coordinates[0]
1120        };
1121        let blended = drawn_x(&mut session);
1122        assert!(blended > 0.0);
1123        session.submit(Command::Chart(
1124            r4.id(),
1125            ChartCommand::Move {
1126                entity: body,
1127                point: chart(6.0),
1128            },
1129        ));
1130        session.boundary(Input::default()).unwrap();
1131        session
1132            .domains_mut()
1133            .typed(r4)
1134            .unwrap()
1135            .physics_mut()
1136            .unwrap()
1137            .throw(body, Vec4::X)
1138            .unwrap();
1139        assert_eq!(drawn_x(&mut session), blended);
1140    }
1141
1142    #[test]
1143    fn a_warmed_step_with_sixty_four_moving_bodies_allocates_nothing() {
1144        let mut entities = Entities::new(SceneId {
1145            runtime: RuntimeId::allocate(),
1146            epoch: Epoch::default(),
1147        });
1148        let mut domain = DomainBuilder::new("r4", EuclideanR4)
1149            .tracked(DEFAULT_LOG_CAPACITY)
1150            .physics(PhysicsConfig::new(register_default_narrowphase).gravity(Vec4::NEG_Y))
1151            .unwrap()
1152            .build(DomainId::new(0), entities.scene());
1153        for index in 0..MOVING {
1154            let at = Vec4::new(index as f32 * 4.0, 10.0, 0.0, 0.0);
1155            let entity = entities.spawn();
1156            domain.attach_pose(entity, Pose::at(at)).expect("pose row");
1157            domain.physics_mut().expect("physics").spawn(
1158                entity,
1159                sphere_body_r4(at, Vec4::ZERO, 0.5, 1.0).expect("body"),
1160            );
1161        }
1162        for tick in 0..WARM_STEPS {
1163            domain.step(step_at(tick)).expect("warm step");
1164        }
1165
1166        let bytes = bytes_allocated_by(|| {
1167            domain.step(step_at(WARM_STEPS)).expect("step");
1168        })
1169        .expect("the counting allocator is installed");
1170        assert_eq!(bytes, 0, "a warmed physics step allocated {bytes} bytes");
1171    }
1172
1173    #[test]
1174    fn invalid_physics_configuration_is_refused_before_installation() {
1175        let invalid_gravity =
1176            PhysicsConfig::new(register_default_narrowphase).gravity(Vec4::splat(f32::NAN));
1177        assert_eq!(
1178            DomainBuilder::new("r4", EuclideanR4)
1179                .physics(invalid_gravity)
1180                .err(),
1181            Some(EditError::InvalidGravity)
1182        );
1183
1184        let invalid_grab = PhysicsConfig::new(register_default_narrowphase).grab(GrabConfig::new(
1185            f32::NAN,
1186            1.0,
1187            1.0,
1188        ));
1189        assert_eq!(
1190            DomainBuilder::new("r4", EuclideanR4)
1191                .physics(invalid_grab)
1192                .err(),
1193            Some(EditError::InvalidGrabConfig)
1194        );
1195
1196        let invalid_travel = PhysicsConfig::new(register_default_narrowphase).adaptive_substeps(
1197            1,
1198            4,
1199            0.0,
1200            linear_speed::<EuclideanR4>,
1201        );
1202        assert_eq!(
1203            DomainBuilder::new("r4", EuclideanR4)
1204                .physics(invalid_travel)
1205                .err(),
1206            Some(EditError::InvalidStepTravel)
1207        );
1208    }
1209}