all_is_cubes/physics/
body.rs

1#[cfg(feature = "rerun")]
2use alloc::vec::Vec;
3use core::fmt;
4use core::ops;
5
6use bevy_ecs::prelude as ecs;
7use euclid::{Point3D, Vector3D};
8use ordered_float::NotNan;
9
10/// Acts as polyfill for float methods
11#[cfg(not(any(feature = "std", test)))]
12#[allow(
13    unused_imports,
14    reason = "unclear why this warns even though it is needed"
15)]
16use num_traits::float::Float as _;
17
18use super::collision::{
19    Contact, aab_raycast, collide_along_ray, escape_along_ray, find_colliding_cubes, nudge_on_ray,
20};
21use crate::block::{BlockCollision, Resolution};
22use crate::camera::Eye;
23use crate::fluff::Fluff;
24#[cfg(not(any(feature = "std", test)))]
25#[allow(
26    unused_imports,
27    reason = "unclear why this warns even though it is needed"
28)]
29use crate::math::Euclid as _;
30use crate::math::{
31    Aab, Cube, Face6, Face7, FreeCoordinate, FreePoint, FreeVector, PositiveSign, notnan,
32};
33use crate::physics::step::PhysicsOutputs;
34use crate::physics::{POSITION_EPSILON, StopAt, Velocity};
35use crate::raycast::Ray;
36use crate::rerun_glue as rg;
37use crate::space;
38use crate::time::Tick;
39use crate::transaction::{self, Equal, Transaction};
40use crate::util::{ConciseDebug, Fmt, Refmt as _, StatusText};
41
42// -------------------------------------------------------------------------------------------------
43
44/// Velocities shorter than this are treated as zero, to allow things to come to unchanging rest sooner.
45const VELOCITY_EPSILON_SQUARED: NotNan<FreeCoordinate> = notnan!(1e-12);
46
47/// Velocities larger than this (in cubes per second) are clamped.
48///
49/// This provides an upper limit on the collision detection computation,
50/// per body per frame.
51pub(crate) const VELOCITY_MAGNITUDE_LIMIT: FreeCoordinate = 1e4_f64;
52pub(crate) const VELOCITY_MAGNITUDE_LIMIT_SQUARED: FreeCoordinate =
53    VELOCITY_MAGNITUDE_LIMIT * VELOCITY_MAGNITUDE_LIMIT;
54
55/// An object with a position, velocity, and collision volume.
56/// What it collides with is determined externally.
57#[derive(Clone, PartialEq, ecs::Component)]
58#[require(PhysicsOutputs, rg::Destination)]
59#[non_exhaustive]
60pub struct Body {
61    /// Position.
62    ///
63    /// Invariant: `self.occupying` must be updated to fit whenever this is changed.
64    /// `self.occupying` must always contain `self.position`.
65    //---
66    // TODO: The NotNan was added in a hurry and is not integrated as well as it ought to be.
67    // Also, we really want a type that does not have signed zeroes for consistency (see
68    // <https://github.com/kpreid/all-is-cubes/issues/537>) and it might be even better to use
69    // fixed-point positions instead of floating-point.
70    position: Point3D<NotNan<FreeCoordinate>, Cube>,
71
72    /// Velocity, in position units per second.
73    //---
74    // TODO: NaN should be prohibited here too
75    velocity: Vector3D<NotNan<FreeCoordinate>, Velocity>,
76
77    /// Volume that this body attempts to occupy, in coordinates relative to `self.position`.
78    ///
79    /// It should always contain the origin (i.e. always contain the position point).
80    /// TODO: Actually enforce that.
81    ///
82    /// It does not change as a consequence of physics stepping; it is configuration rather than
83    /// instantaneous state.
84    collision_box: Aab,
85
86    /// Volume that this body believes it is successfully occupying, in coordinates relative to
87    /// the [`Space`] it collides with.
88    ///
89    /// In the ideal case, this is always equal to `collision_box.translate(position.to_vector())`.
90    /// In practice, it will differ at least due to rounding errors, and additionally due to
91    /// numerical error during collision resolution, or be shrunk by large distances if the body has
92    /// been squeezed by moving obstacles (TODO: not implemented yet).
93    occupying: Aab,
94
95    /// Is this body not subject to gravity?
96    pub flying: bool,
97    /// Is this body not subject to collision?
98    pub noclip: bool,
99
100    /// Yaw of the camera look direction, in degrees clockwise from looking towards -Z.
101    ///
102    /// The preferred range is 0 inclusive to 360 exclusive.
103    ///
104    /// This does not affect the behavior of the [`Body`] itself; it has nothing to do with
105    /// the direction of the velocity.
106    pub yaw: FreeCoordinate,
107
108    /// Pitch of the camera look direction, in degrees downward from looking horixontally.
109    ///
110    /// The preferred range is -90 to 90, inclusive.
111    ///
112    /// This does not affect the behavior of the [`Body`] itself; it has nothing to do with
113    /// the direction of the velocity.
114    pub pitch: FreeCoordinate,
115}
116
117impl fmt::Debug for Body {
118    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
119        let Self {
120            position,
121            velocity,
122            collision_box,
123            occupying,
124            flying,
125            noclip,
126            yaw,
127            pitch,
128        } = self;
129        fmt.debug_struct("Body")
130            .field("position", &position.refmt(&ConciseDebug))
131            .field("velocity", &velocity.refmt(&ConciseDebug))
132            .field("collision_box", &collision_box)
133            .field("occupying", &occupying)
134            .field("flying", &flying)
135            .field("noclip", &noclip)
136            .field("yaw", &yaw)
137            .field("pitch", &pitch)
138            .finish()
139    }
140}
141
142/// Omits collision box on the grounds that it is presumably constant
143impl Fmt<StatusText> for Body {
144    fn fmt(&self, fmt: &mut fmt::Formatter<'_>, _: &StatusText) -> fmt::Result {
145        let &Self {
146            position,
147            velocity,
148            collision_box: _,
149            occupying: _,
150            flying,
151            noclip,
152            yaw,
153            pitch,
154        } = self;
155        let dir_face = Face6::from_snapped_vector(self.look_direction()).unwrap();
156        write!(
157            fmt,
158            "Position: {}  Yaw: {yaw:5.1}°  Pitch: {pitch:5.1}°\n\
159             Velocity: {}  Nearest axis to eye: {dir_face:?}",
160            position.refmt(&ConciseDebug),
161            velocity.refmt(&ConciseDebug),
162        )?;
163        if flying {
164            write!(fmt, "  Flying")?;
165        }
166        if noclip {
167            write!(fmt, "  Noclip")?;
168        }
169        Ok(())
170    }
171}
172
173impl Body {
174    /// Constructs a [`Body`] requiring only information that can't be reasonably defaulted.
175    ///
176    /// # Panics
177    ///
178    /// Panics if any component of `position` is NaN or infinite.
179    #[track_caller]
180    pub fn new_minimal(position: impl Into<FreePoint>, collision_box: impl Into<Aab>) -> Self {
181        let position = position.into();
182        assert!(position.is_finite(), "body’s position must be finite");
183        let position = position.map(|c| NotNan::new(c).unwrap()); // NotNan is a weaker condition
184
185        let collision_box = collision_box.into();
186        Self {
187            position,
188            velocity: Vector3D::zero(),
189            collision_box,
190            // TODO: should be able to translate by NotNan
191            occupying: collision_box.translate(position.map(|c| c.into_inner()).to_vector()),
192            flying: false,
193            noclip: false,
194            yaw: 0.0,
195            pitch: 0.0,
196        }
197    }
198
199    /// `step_with_rerun()` but with no rerun arg for use by tests.
200    #[cfg(test)]
201    pub(crate) fn step<CC>(
202        &mut self,
203        tick: Tick,
204        colliding_space: Option<&space::Read<'_>>,
205        collision_callback: CC,
206    ) -> BodyStepDetails
207    where
208        CC: FnMut(Contact),
209    {
210        self.step_with_rerun(
211            tick,
212            Vector3D::zero(),
213            colliding_space,
214            collision_callback,
215            &Default::default(),
216        )
217    }
218
219    /// Advances time for the body.
220    ///
221    /// If `colliding_space` is present then the body may collide with blocks in that space
222    /// (constraining possible movement) and `collision_callback` will be called with all
223    /// such blocks. It is not guaranteed that `collision_callback` will be called only once
224    /// per block.
225    ///
226    /// This method is private because the exact details of what inputs are required are
227    /// unstable.
228    // TODO(ecs): make this part of the stepping system instead, after updating tests to permit it
229    pub(crate) fn step_with_rerun<CC>(
230        &mut self,
231        tick: Tick,
232        external_delta_v: Vector3D<NotNan<FreeCoordinate>, Velocity>,
233        mut colliding_space: Option<&space::Read<'_>>,
234        mut collision_callback: CC,
235        rerun_destination: &crate::rerun_glue::Destination,
236    ) -> BodyStepDetails
237    where
238        CC: FnMut(Contact),
239    {
240        #[cfg(not(feature = "rerun"))]
241        let _ = rerun_destination;
242
243        let velocity_before_gravity_and_collision = self.velocity;
244        let dt = NotNan::new(tick.delta_t().as_secs_f64()).unwrap();
245        let mut move_segments = [MoveSegment::default(); 3];
246        let mut move_segment_index = 0;
247        let mut already_colliding = None;
248        #[cfg(feature = "rerun")]
249        let mut contact_accum: Vec<Contact> = Vec::new();
250
251        self.velocity += external_delta_v;
252
253        if self.noclip {
254            colliding_space = None;
255        }
256
257        let mut collision_callback = |contact: Contact| {
258            if contact.normal() == Face7::Within {
259                already_colliding = Some(contact);
260            }
261            collision_callback(contact);
262
263            #[cfg(feature = "rerun")]
264            contact_accum.push(contact);
265        };
266
267        if !self.position.to_vector().square_length().is_finite() {
268            // If position is NaN or infinite, can't do anything, but don't panic
269            return BodyStepDetails {
270                quiescent: false,
271                already_colliding,
272                push_out: None,
273                move_segments,
274                delta_v: Vector3D::zero(),
275            };
276        }
277
278        if !self.flying
279            && !tick.paused()
280            && let Some(space) = colliding_space
281        {
282            self.velocity += space.physics().gravity.cast_unit() * dt;
283        }
284
285        // TODO: attempt to expand `occupying` to fit `collision_box`.
286
287        #[cfg(feature = "rerun")]
288        let position_before_push_out = self.position;
289        let push_out_info = if let Some(space) = colliding_space {
290            self.push_out(space)
291        } else {
292            None
293        };
294
295        let velocity_magnitude_squared = self.velocity.square_length();
296        if !velocity_magnitude_squared.is_finite() {
297            self.velocity = Vector3D::zero();
298        } else if velocity_magnitude_squared <= VELOCITY_EPSILON_SQUARED || tick.paused() {
299            return BodyStepDetails {
300                quiescent: true,
301                already_colliding,
302                push_out: push_out_info,
303                move_segments,
304                delta_v: self.velocity - velocity_before_gravity_and_collision,
305            };
306        } else if velocity_magnitude_squared.into_inner() > VELOCITY_MAGNITUDE_LIMIT_SQUARED {
307            self.velocity *=
308                NotNan::new(VELOCITY_MAGNITUDE_LIMIT / velocity_magnitude_squared.sqrt()).unwrap();
309        }
310
311        // TODO: correct integration of acceleration due to gravity
312        let unobstructed_delta_position: Vector3D<_, _> =
313            self.velocity.map(NotNan::into_inner).cast_unit() * dt.into_inner();
314
315        // Do collision detection and resolution.
316        #[cfg(feature = "rerun")]
317        let position_before_move_segments = self.position;
318        if let Some(space) = colliding_space {
319            let mut delta_position = unobstructed_delta_position;
320            while delta_position != Vector3D::zero() {
321                assert!(
322                    move_segment_index < 3,
323                    "sliding collision loop did not finish"
324                );
325                // Each call to collide_and_advance will zero at least one axis of delta_position.
326                // The nonzero axes are for sliding movement.
327                let (new_delta_position, segment) =
328                    self.collide_and_advance(space, &mut collision_callback, delta_position);
329                delta_position = new_delta_position;
330
331                // Diagnostic recording of the individual move segments
332                move_segments[move_segment_index] = segment;
333
334                move_segment_index += 1;
335            }
336        } else {
337            self.set_position(self.position.map(NotNan::into_inner) + unobstructed_delta_position);
338            move_segments[0] = MoveSegment {
339                delta_position: unobstructed_delta_position,
340                stopped_by: None,
341            };
342        }
343
344        // TODO: after gravity, falling-below-the-world protection
345
346        let info = BodyStepDetails {
347            quiescent: false,
348            already_colliding,
349            push_out: push_out_info,
350            move_segments,
351            delta_v: self.velocity - velocity_before_gravity_and_collision,
352        };
353
354        #[cfg(feature = "rerun")]
355        {
356            use crate::content::palette;
357
358            // Log step info as text.
359            rerun_destination.log(
360                &rg::entity_path!["step_info"],
361                &rg::archetypes::TextDocument::new(format!("{:#?}", info.refmt(&ConciseDebug))),
362            );
363
364            // Log nearby cubes and whether they are contacts.
365            if let Some(space) = colliding_space {
366                // TODO: If we make more general use of rerun, this is going to need to be moved from
367                // here to `Space` itself
368                let cubes = self
369                    .collision_box_abs()
370                    .expand(0.875)
371                    .round_up_to_grid()
372                    .interior_iter()
373                    .filter(|cube| {
374                        space.get_evaluated(*cube).uniform_collision() != Some(BlockCollision::None)
375                    });
376                let (class_ids, colors): (Vec<rg::ClassId>, Vec<_>) = cubes
377                    .clone()
378                    .map(|cube| {
379                        // O(n) but n is small
380                        let this_contact =
381                            contact_accum.iter().find(|contact| contact.cube() == cube);
382                        if let Some(this_contact) = this_contact {
383                            if this_contact.normal() == Face7::Within {
384                                (
385                                    rg::ClassId::CollisionContactWithin,
386                                    palette::DEBUG_COLLISION_CUBE_WITHIN,
387                                )
388                            } else {
389                                (
390                                    rg::ClassId::CollisionContactAgainst,
391                                    palette::DEBUG_COLLISION_CUBE_AGAINST,
392                                )
393                            }
394                        } else {
395                            (rg::ClassId::SpaceBlock, space.get_evaluated(cube).color())
396                        }
397                    })
398                    .unzip();
399                rerun_destination.log(
400                    &rg::entity_path!["blocks"],
401                    &rg::convert_aabs(
402                        cubes.map(|cube| {
403                            let ev = space.get_evaluated(cube);
404                            // approximation of block's actual collision bounds
405                            ev.voxels_bounds()
406                                .to_free()
407                                .scale(ev.voxels().resolution().recip_f64())
408                                .translate(cube.lower_bounds().to_f64().to_vector())
409                        }),
410                        FreeVector::zero(),
411                    )
412                    .with_radii(class_ids.iter().map(|class| match class {
413                        rg::ClassId::SpaceBlock => 0.001,
414                        rg::ClassId::CollisionContactAgainst => 0.002,
415                        rg::ClassId::CollisionContactWithin => 0.005,
416                        _ => unreachable!(),
417                    }))
418                    .with_class_ids(class_ids)
419                    .with_colors(colors),
420                );
421            }
422
423            // Log body position point
424            rerun_destination.log(
425                &rg::EntityPath::new(vec![]),
426                &rg::archetypes::Points3D::new([rg::convert_point(self.position)]),
427            );
428
429            // Log body collision box
430            rerun_destination.log(
431                &rg::entity_path!["collision_box"],
432                &rg::convert_aabs(
433                    [self.collision_box],
434                    self.position.map(NotNan::into_inner).to_vector(),
435                )
436                .with_class_ids([rg::ClassId::BodyCollisionBox]),
437            );
438            rerun_destination.log(
439                &rg::entity_path!["occupying"],
440                &rg::convert_aabs([self.occupying], FreeVector::zero())
441                    .with_class_ids([rg::ClassId::BodyCollisionBox]),
442            );
443
444            // Our movement arrows shall be logged relative to all collision box corners
445            // for legibility of how they interact with things.
446            let arrow_offsets = || self.collision_box.corner_points().map(|p| p.to_vector());
447
448            // Log push_out operation
449            // TODO: should this be just a maybe-fourth movement arrow?
450            match push_out_info {
451                Some(push_out_vector) => rerun_destination.log(
452                    &rg::entity_path!["push_out"],
453                    &rg::archetypes::Arrows3D::from_vectors(
454                        arrow_offsets().map(|_| rg::convert_vec(push_out_vector)),
455                    )
456                    .with_origins(arrow_offsets().map(|offset| {
457                        rg::convert_point(position_before_push_out.map(NotNan::into_inner) + offset)
458                    })),
459                ),
460                None => rerun_destination.clear_recursive(&rg::entity_path!["push_out"]),
461            }
462
463            // Log move segments
464            {
465                let move_segments = &move_segments[..move_segment_index]; // trim empty entries
466                rerun_destination.log(
467                    &rg::entity_path!["move_segment"],
468                    &rg::archetypes::Arrows3D::from_vectors(arrow_offsets().flat_map(|_| {
469                        move_segments.iter().map(|seg| rg::convert_vec(seg.delta_position))
470                    }))
471                    .with_origins(arrow_offsets().flat_map(|offset| {
472                        move_segments.iter().scan(
473                            position_before_move_segments.map(NotNan::into_inner) + offset,
474                            |pos, seg| {
475                                let arrow_origin = rg::convert_point(*pos);
476                                *pos += seg.delta_position;
477                                Some(arrow_origin)
478                            },
479                        )
480                    })),
481                );
482            }
483        }
484
485        info
486    }
487
488    /// Perform a single straight-line position change, stopping at the first obstacle.
489    /// Returns the remainder of `delta_position` that should be retried for sliding movement.
490    fn collide_and_advance<CC>(
491        &mut self,
492        space: &space::Read<'_>,
493        collision_callback: &mut CC,
494        mut delta_position: FreeVector,
495    ) -> (FreeVector, MoveSegment)
496    where
497        CC: FnMut(Contact),
498    {
499        let movement_ignoring_collision =
500            Ray::new(self.position.map(NotNan::into_inner), delta_position);
501        let collision = collide_along_ray(
502            space,
503            movement_ignoring_collision,
504            self.collision_box, // TODO: use occupying
505            collision_callback,
506            StopAt::NotAlreadyColliding,
507        );
508
509        if let Some(collision) = collision {
510            let axis = collision
511                .contact
512                .normal()
513                .axis()
514                .expect("Face7::Within collisions should not reach here");
515            // Advance however much straight-line distance is available.
516            // But a little bit back from that, to avoid floating point error pushing us
517            // into being already colliding next frame.
518            let motion_segment = nudge_on_ray(
519                self.collision_box, // TODO: use occupying
520                movement_ignoring_collision.scale_direction(collision.t_distance),
521                collision.contact.normal().opposite(),
522                collision.contact.resolution(),
523                true,
524            );
525            let unobstructed_delta_position = motion_segment.direction;
526            self.set_position(self.position.map(NotNan::into_inner) + unobstructed_delta_position);
527            // Figure the distance we have have left.
528            delta_position -= unobstructed_delta_position;
529            // Convert it to sliding movement for the axes we didn't collide in.
530            delta_position[axis] = 0.0;
531
532            // Zero the velocity in that direction.
533            // (This is the velocity part of collision response. That is, if we supported bouncy
534            // objects, we'd do something different here.)
535            self.velocity[axis] = notnan!(0.0);
536
537            (
538                delta_position,
539                MoveSegment {
540                    delta_position: unobstructed_delta_position,
541                    stopped_by: Some(collision.contact),
542                },
543            )
544        } else {
545            // We did not hit anything for the length of the raycast. Proceed unobstructed.
546            self.set_position(self.position.map(NotNan::into_inner) + delta_position);
547            (
548                Vector3D::zero(),
549                MoveSegment {
550                    delta_position,
551                    stopped_by: None,
552                },
553            )
554        }
555    }
556
557    /// Check if we're intersecting any blocks and fix that if so.
558    fn push_out(&mut self, space: &space::Read<'_>) -> Option<FreeVector> {
559        // TODO: need to unsquash the `occupying` box if possible
560
561        let colliding = find_colliding_cubes(space, self.collision_box_abs()).next().is_some();
562        if colliding {
563            let exit_backwards: FreeVector = -self.velocity.map(NotNan::into_inner).cast_unit(); // don't care about magnitude
564            let shortest_push_out = (-1..=1)
565                .flat_map(move |dx| {
566                    (-1..=1).flat_map(move |dy| {
567                        (-1..=1).map(move |dz| {
568                            let direction = Vector3D::new(dx, dy, dz).map(FreeCoordinate::from);
569                            if direction == Vector3D::zero() {
570                                // We've got an extra case, and an item to delete from the combinations,
571                                // so substitute the one from the other.
572                                exit_backwards
573                            } else {
574                                direction
575                            }
576                        })
577                    })
578                })
579                .filter_map(|direction| self.attempt_push_out(space, direction))
580                .min_by_key(|(_, distance)| *distance);
581
582            if let Some((new_position, _)) = shortest_push_out {
583                let old_position: FreePoint = self.position.map(NotNan::into_inner);
584                self.set_position(new_position);
585                return Some(new_position - old_position);
586            }
587        }
588        None
589    }
590
591    /// Try moving in the given direction, find an empty space, and
592    /// return the new position and distance to it.
593    fn attempt_push_out(
594        &self,
595        space: &space::Read<'_>,
596        direction: FreeVector,
597    ) -> Option<(FreePoint, NotNan<FreeCoordinate>)> {
598        if false {
599            // TODO: This attempted reimplementation does not work yet.
600            // Once `escape_along_ray()` is working properly, we can enable this and make
601            // push-out actually work with recursive blocks.
602
603            let direction = direction.normalize(); // TODO: set this to a max distance
604            if direction.x.is_nan() {
605                // This case happens in push_out() when the velocity is zero.
606                // Checking exactly here is a cheap way to catch it.
607                return None;
608            }
609
610            let ray = Ray::new(self.position.map(NotNan::into_inner), direction);
611
612            let end = escape_along_ray(
613                space,
614                ray,
615                self.collision_box, /* TODO: use occupying */
616            )?;
617
618            let nudged_distance = end.t_distance + POSITION_EPSILON;
619            Some((
620                ray.scale_direction(nudged_distance).unit_endpoint(),
621                NotNan::new(nudged_distance).ok()?,
622            ))
623        } else {
624            let ray = Ray::new(self.position.map(NotNan::into_inner), direction);
625            // TODO: upper bound on distance to try
626            'raycast: for ray_step in
627                aab_raycast(self.collision_box /* TODO: use occupying */, ray, true)
628            {
629                let adjusted_segment = nudge_on_ray(
630                    self.collision_box, /* TODO: use occupying */
631                    ray.scale_direction(ray_step.t_distance()),
632                    ray_step.face(),
633                    Resolution::R1,
634                    true,
635                );
636                let step_aab = self
637                    .collision_box /* TODO: use occupying */
638                    .translate(adjusted_segment.unit_endpoint().to_vector());
639                for cube in step_aab.round_up_to_grid().interior_iter() {
640                    // TODO: refactor to combine this with other collision attribute tests
641                    match space.get_evaluated(cube).uniform_collision() {
642                        Some(BlockCollision::Hard) => {
643                            // Not a clear space
644                            continue 'raycast;
645                        }
646                        Some(BlockCollision::None) => {}
647                        None => {
648                            // TODO: Either check collision, or continue
649                            //continue 'raycast;
650                        }
651                    }
652                }
653                // No collisions, so we can use this.
654                return Some((
655                    adjusted_segment.unit_endpoint(),
656                    NotNan::new(ray_step.t_distance() * direction.length()).ok()?,
657                ));
658            }
659
660            None
661        }
662    }
663
664    /// Returns the body’s current position.
665    ///
666    /// If you are interested in the space it occupies, use [`Self::collision_box_abs()`] instead.
667    pub fn position(&self) -> FreePoint {
668        self.position.map(NotNan::into_inner)
669    }
670
671    /// Sets the position of the body, disregarding collision.
672    ///
673    /// Note: This may have effects that normal time stepping does not. In particular,
674    /// `body.set_position(body.position())` is not guaranteed to do nothing.
675    ///
676    /// If `position` contains any component which is infinite or NaN, this function does nothing.
677    /// This behavior may change in the future.
678    pub fn set_position(&mut self, position: FreePoint) {
679        if !position.is_finite() {
680            return;
681        }
682
683        self.position = position.map(|c| NotNan::new(c).unwrap());
684
685        // This new box might collide with the `Space`, but (TODO: not implemented yet)
686        // stepping will recover from that if possible.
687        self.occupying =
688            self.collision_box.translate(self.position.map(NotNan::into_inner).to_vector());
689    }
690
691    /// Returns the body’s current velocity.
692    pub fn velocity(&self) -> Vector3D<f64, Velocity> {
693        self.velocity.map(NotNan::into_inner)
694    }
695
696    /// Adds the given value to the body’s velocity.
697    ///
698    /// If `Δv` contains any component which is infinite or NaN, this function does nothing.
699    /// This behavior may change in the future.
700    #[allow(non_snake_case)]
701    pub fn add_velocity(&mut self, Δv: Vector3D<f64, Velocity>) {
702        if !Δv.is_finite() {
703            return;
704        }
705
706        self.velocity += Δv.map(|c| NotNan::new(c).unwrap());
707    }
708
709    /// Replaces the body’s velocity with the given value.
710    ///
711    /// If `Δv` contains any component which is infinite or NaN, this function does nothing.
712    /// This behavior may change in the future.
713    pub fn set_velocity(&mut self, v: Vector3D<f64, Velocity>) {
714        if !v.is_finite() {
715            return;
716        }
717
718        self.velocity = v.map(|c| NotNan::new(c).unwrap());
719    }
720
721    /// Returns the body's configured collision box in coordinates relative to [`Self::position()`].
722    ///
723    /// ```
724    /// use all_is_cubes::math::Aab;
725    /// use all_is_cubes::physics::Body;
726    ///
727    /// let body = Body::new_minimal(
728    ///     (0.0, 20.0, 0.0),
729    ///     Aab::new(-1.0, 1.0, -2.0, 2.0, -3.0, 3.0)
730    /// );
731    /// assert_eq!(body.collision_box_abs(), Aab::new(-1.0, 1.0, 18.0, 22.0, -3.0, 3.0));
732    /// ```
733    pub fn collision_box_rel(&self) -> Aab {
734        self.collision_box
735    }
736
737    /// Returns the body's current collision box in world coordinates.
738    ///
739    /// This is not necessarily equal in size to [`Self::collision_box_rel()`].
740    ///
741    /// ```
742    /// use all_is_cubes::math::Aab;
743    /// use all_is_cubes::physics::Body;
744    ///
745    /// let body = Body::new_minimal(
746    ///     (0.0, 20.0, 0.0),
747    ///     Aab::new(-1.0, 1.0, -2.0, 2.0, -3.0, 3.0)
748    /// );
749    /// assert_eq!(body.collision_box_abs(), Aab::new(-1.0, 1.0, 18.0, 22.0, -3.0, 3.0));
750    /// ```
751    //---
752    // TODO: After `occupying` is a little more fleshed out, consider renaming this method to that.
753    pub fn collision_box_abs(&self) -> Aab {
754        self.occupying
755    }
756
757    pub(crate) fn look_rotation(&self) -> euclid::Rotation3D<f64, Eye, Cube> {
758        euclid::Rotation3D::<_, Eye, Cube>::around_x(euclid::Angle {
759            radians: -self.pitch.to_radians(),
760        })
761        .then(&euclid::Rotation3D::around_y(euclid::Angle {
762            radians: -self.yaw.to_radians(),
763        }))
764    }
765
766    /// Returns the direction the body is facing (when it is part of a character).
767    pub fn look_direction(&self) -> FreeVector {
768        self.look_rotation().transform_vector3d(Vector3D::new(0., 0., -1.))
769    }
770
771    /// Changes [`self.yaw`](Self::yaw) and [`self.pitch`](Self::pitch) to look in the given
772    /// direction vector.
773    ///
774    /// If `direction` has zero length, the resulting direction is unspecified but valid.
775    pub fn set_look_direction(&mut self, direction: FreeVector) {
776        let horizontal_distance = direction.x.hypot(direction.z);
777
778        self.yaw = (180.0 - (direction.x).atan2(direction.z).to_degrees()).rem_euclid(360.0);
779        self.pitch = -(direction.y).atan2(horizontal_distance).to_degrees();
780    }
781
782    // TODO: should this be able to compute its answer without needing `PhysicsOutputs`?
783    pub(crate) fn is_on_ground(&self, po: &PhysicsOutputs) -> bool {
784        self.velocity().y <= 0.0
785            && po.colliding_cubes.iter().any(|contact| contact.normal() == Face7::PY)
786    }
787}
788
789#[cfg(feature = "save")]
790impl serde::Serialize for Body {
791    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
792    where
793        S: serde::Serializer,
794    {
795        let &Body {
796            position,
797            velocity,
798            collision_box,
799            occupying,
800            flying,
801            noclip,
802            yaw,
803            pitch,
804        } = self;
805        crate::save::schema::BodySer::BodyV1 {
806            position: position.into(),
807            velocity: velocity.into(),
808            collision_box,
809            occupying,
810            flying,
811            noclip,
812            yaw,
813            pitch,
814        }
815        .serialize(serializer)
816    }
817}
818
819#[cfg(feature = "save")]
820impl<'de> serde::Deserialize<'de> for Body {
821    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
822    where
823        D: serde::Deserializer<'de>,
824    {
825        match crate::save::schema::BodySer::deserialize(deserializer)? {
826            crate::save::schema::BodySer::BodyV1 {
827                position,
828                velocity,
829                collision_box,
830                occupying,
831                flying,
832                noclip,
833                yaw,
834                pitch,
835            } => Ok(Body {
836                position: position.into(),
837                velocity: velocity.into(),
838                collision_box,
839                occupying,
840                flying,
841                noclip,
842                yaw,
843                pitch,
844            }),
845        }
846    }
847}
848
849/// Performance data produced by stepping a [`Body`].
850///
851/// Use [`fmt::Debug`] or [`StatusText`] formatting to examine this.
852#[derive(Clone, Copy, Debug, Default, PartialEq)]
853pub(crate) struct BodyStepInfo {
854    /// Number of bodies whose updates were aggregated into this value.
855    pub(crate) count: usize,
856}
857
858impl Fmt<StatusText> for BodyStepInfo {
859    fn fmt(&self, f: &mut fmt::Formatter<'_>, _: &StatusText) -> fmt::Result {
860        let Self { count } = self;
861        write!(f, "{count} bodies' steps")
862    }
863}
864
865impl ops::AddAssign for BodyStepInfo {
866    fn add_assign(&mut self, other: Self) {
867        let Self { count } = self;
868        *count += other.count;
869    }
870}
871
872/// Diagnostic data produced by stepping a [`Body`] about how it moved and collided.
873#[derive(Clone, Copy, Debug)]
874#[non_exhaustive]
875#[doc(hidden)] // public only for all-is-cubes-gpu debug visualization, and testing
876pub struct BodyStepDetails {
877    /// Whether movement computation was skipped due to approximately zero velocity.
878    quiescent: bool,
879
880    /// If the body was pushed out of something it was found to be already colliding with,
881    /// then this is the change in its position.
882    #[doc(hidden)] // pub for fuzz_physics
883    pub push_out: Option<FreeVector>,
884
885    already_colliding: Option<Contact>,
886
887    /// Details on movement and collision. A single frame's movement may have up to three
888    /// segments as differently oriented faces are collided with.
889    move_segments: [MoveSegment; 3],
890
891    /// Change in velocity during this step.
892    delta_v: Vector3D<NotNan<f64>, Velocity>,
893}
894
895impl Fmt<ConciseDebug> for BodyStepDetails {
896    fn fmt(&self, fmt: &mut fmt::Formatter<'_>, fopt: &ConciseDebug) -> fmt::Result {
897        fmt.debug_struct("BodyStepDetails")
898            .field("quiescent", &self.quiescent)
899            .field("already_colliding", &self.already_colliding)
900            .field("push_out", &self.push_out.as_ref().map(|v| v.refmt(fopt)))
901            .field("move_segments", &self.move_segments.refmt(fopt))
902            .field("delta_v", &self.delta_v.refmt(fopt))
903            .finish()
904    }
905}
906
907impl BodyStepDetails {
908    pub(crate) fn impact_fluff(&self) -> Option<Fluff> {
909        let velocity = self.delta_v.map(NotNan::into_inner).length();
910        // don't emit anything for slow change or movement in the air
911        if velocity >= 0.25 && self.move_segments.iter().any(|s| s.stopped_by.is_some()) {
912            Some(Fluff::BlockImpact {
913                velocity: PositiveSign::try_from(velocity as f32).ok()?,
914            })
915        } else {
916            None
917        }
918    }
919}
920
921/// One of the individual straight-line movement segments of a [`BodyStepDetails`].
922#[derive(Clone, Copy, Debug)]
923#[non_exhaustive]
924pub(crate) struct MoveSegment {
925    /// The change in position.
926    pub delta_position: FreeVector,
927    /// What solid object stopped this segment from continuing further
928    /// (there may be others, but this is one of them), or None if there
929    /// was no obstacle.
930    pub stopped_by: Option<Contact>,
931}
932
933impl Fmt<ConciseDebug> for MoveSegment {
934    fn fmt(&self, fmt: &mut fmt::Formatter<'_>, fopt: &ConciseDebug) -> fmt::Result {
935        let mut nonempty = false;
936        if self.delta_position != FreeVector::zero() {
937            nonempty = true;
938            write!(fmt, "move {:?}", self.delta_position.refmt(fopt))?;
939        }
940        if let Some(stopped_by) = &self.stopped_by {
941            if nonempty {
942                write!(fmt, " ")?;
943            }
944            nonempty = true;
945            write!(fmt, "stopped by {stopped_by:?}")?;
946        }
947        if !nonempty {
948            write!(fmt, "0")?;
949        }
950        Ok(())
951    }
952}
953
954impl Default for MoveSegment {
955    fn default() -> Self {
956        Self {
957            delta_position: Vector3D::zero(),
958            stopped_by: None,
959        }
960    }
961}
962
963/// The [`Transaction`] type for [`Body`].
964///
965/// TODO: Very incomplete; just a sketch of what eventually needs to exist.
966#[derive(Clone, Debug, Default, PartialEq)]
967#[must_use]
968#[non_exhaustive]
969pub struct BodyTransaction {
970    set_position: Equal<FreePoint>,
971    set_look_direction: Equal<FreeVector>,
972}
973
974#[allow(missing_docs)] // TODO
975impl BodyTransaction {
976    #[inline]
977    pub fn with_position(mut self, position: FreePoint) -> Self {
978        self.set_position = Equal(Some(position));
979        self
980    }
981
982    #[inline]
983    pub fn with_look_direction(mut self, direction: FreeVector) -> Self {
984        self.set_look_direction = Equal(Some(direction));
985        self
986    }
987}
988
989impl transaction::Transactional for Body {
990    type Transaction = BodyTransaction;
991}
992
993impl Transaction for BodyTransaction {
994    type Target = Body;
995    type Context<'a> = ();
996    type CommitCheck = ();
997    type Output = transaction::NoOutput;
998    type Mismatch = BodyMismatch;
999
1000    fn check(
1001        &self,
1002        _body: &Body,
1003        (): Self::Context<'_>,
1004    ) -> Result<Self::CommitCheck, Self::Mismatch> {
1005        // No mismatches currently possible.
1006        Ok(())
1007    }
1008
1009    fn commit(
1010        self,
1011        body: &mut Body,
1012        (): Self::CommitCheck,
1013        _outputs: &mut dyn FnMut(Self::Output),
1014    ) -> Result<(), transaction::CommitError> {
1015        let Self {
1016            set_position,
1017            set_look_direction,
1018        } = self;
1019        if let Equal(Some(position)) = set_position {
1020            body.set_position(position);
1021        }
1022        if let Equal(Some(direction)) = set_look_direction {
1023            body.set_look_direction(direction);
1024        }
1025        Ok(())
1026    }
1027}
1028
1029impl transaction::Merge for BodyTransaction {
1030    type MergeCheck = ();
1031    type Conflict = BodyConflict;
1032
1033    fn check_merge(&self, other: &Self) -> Result<Self::MergeCheck, Self::Conflict> {
1034        let Self {
1035            set_position,
1036            set_look_direction,
1037        } = self;
1038        let conflict = BodyConflict {
1039            position: set_position.check_merge(&other.set_position).is_err(),
1040            look_direction: set_look_direction.check_merge(&other.set_look_direction).is_err(),
1041        };
1042        if conflict
1043            != (BodyConflict {
1044                position: false,
1045                look_direction: false,
1046            })
1047        {
1048            return Err(conflict);
1049        }
1050
1051        Ok(())
1052    }
1053
1054    fn commit_merge(&mut self, other: Self, (): Self::MergeCheck) {
1055        let Self {
1056            set_position,
1057            set_look_direction,
1058        } = self;
1059        set_position.commit_merge(other.set_position, ());
1060        set_look_direction.commit_merge(other.set_look_direction, ());
1061    }
1062}
1063
1064/// Transaction precondition error type for a [`BodyTransaction`].
1065#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
1066#[non_exhaustive]
1067pub enum BodyMismatch {}
1068
1069impl core::error::Error for BodyMismatch {
1070    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
1071        match *self {}
1072    }
1073}
1074
1075// TODO: macro-generate these kind of conflict errors?
1076//
1077/// Transaction conflict error type for a [`BodyTransaction`].
1078#[derive(Clone, Debug, Eq, PartialEq)]
1079pub struct BodyConflict {
1080    position: bool,
1081    look_direction: bool,
1082}
1083
1084impl core::error::Error for BodyConflict {}
1085
1086impl fmt::Display for BodyConflict {
1087    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1088        match *self {
1089            BodyConflict {
1090                position: true,
1091                look_direction: true,
1092            } => {
1093                write!(f, "conflicting changes to position and look direction")
1094            }
1095            BodyConflict {
1096                position: true,
1097                look_direction: false,
1098            } => {
1099                write!(f, "conflicting changes to position")
1100            }
1101            BodyConflict {
1102                position: false,
1103                look_direction: true,
1104            } => {
1105                write!(f, "conflicting changes to look direction")
1106            }
1107            BodyConflict {
1108                position: false,
1109                look_direction: false,
1110            } => {
1111                unreachable!()
1112            }
1113        }
1114    }
1115}
1116
1117/// Note: Tests which involve both body and collision code are currently in the parent module.
1118#[cfg(test)]
1119mod tests {
1120    use super::*;
1121    use crate::transaction::{PredicateRes, TransactionTester};
1122    use euclid::{point3, vec3};
1123
1124    fn test_body() -> Body {
1125        Body {
1126            flying: false,
1127            noclip: false,
1128            ..Body::new_minimal([0., 2., 0.], Aab::new(-0.5, 0.5, -0.5, 0.5, -0.5, 0.5))
1129        }
1130    }
1131
1132    #[test]
1133    fn look_direction() {
1134        let do_test = |direction: [f64; 3], yaw, pitch| {
1135            let mut body = Body::new_minimal([10., 0., 0.], Aab::ZERO);
1136            body.set_look_direction(direction.into());
1137            println!("{direction:?} {yaw} {pitch}");
1138            assert_eq!(body.yaw, yaw);
1139            assert_eq!(body.pitch, pitch);
1140        };
1141
1142        do_test([0., 0., -1.], 0., 0.);
1143        do_test([1., 0., -1.], 45., 0.);
1144        do_test([1., 0., 0.], 90., 0.);
1145        do_test([0., 0., 1.], 180., 0.);
1146        do_test([-1., 0., 0.], 270., 0.);
1147
1148        // TODO: would be tidier if this is 0 instead; revisit the math
1149        let exactly_vertical_yaw = 180.;
1150        do_test([0., 1., 0.], exactly_vertical_yaw, -90.);
1151        do_test([0., 1., -1.], 0., -45.);
1152        do_test([0., 0., -1.], 0., 0.);
1153        do_test([0., -1., -1.], 0., 45.);
1154        do_test([0., -1., 0.], exactly_vertical_yaw, 90.);
1155    }
1156
1157    #[test]
1158    fn body_transaction_systematic() {
1159        fn check_position(expected: FreePoint) -> impl Fn(&Body, &Body) -> PredicateRes {
1160            move |_, after| {
1161                let actual = after.position.map(NotNan::into_inner);
1162                if actual != expected {
1163                    return Err(format!("expected position {expected:#?}, got {actual:#?}").into());
1164                }
1165                if !after.occupying.contains(actual) {
1166                    return Err("bad occupying".into());
1167                }
1168                Ok(())
1169            }
1170        }
1171        fn check_look_direction(expected: FreeVector) -> impl Fn(&Body, &Body) -> PredicateRes {
1172            move |_, after| {
1173                let actual = after.look_direction();
1174                // TODO: improve the implementation so this is exact
1175                if actual.angle_to(expected) > euclid::Angle::degrees(0.001) {
1176                    return Err(
1177                        format!("expected look direction {expected:#?}, got {actual:#?}").into(),
1178                    );
1179                }
1180                Ok(())
1181            }
1182        }
1183
1184        TransactionTester::new()
1185            .transaction(BodyTransaction::default(), |_, _| Ok(()))
1186            .transaction(
1187                BodyTransaction::default().with_position(point3(0., 0., 0.)),
1188                check_position(point3(0., 0., 0.)),
1189            )
1190            .transaction(
1191                BodyTransaction::default().with_position(point3(1., 0., 0.)),
1192                check_position(point3(1., 0., 0.)),
1193            )
1194            .transaction(
1195                BodyTransaction::default().with_look_direction(vec3(1., 0., 0.)),
1196                check_look_direction(vec3(1., 0., 0.)),
1197            )
1198            .transaction(
1199                BodyTransaction::default().with_look_direction(vec3(0., 1., 0.)),
1200                check_look_direction(vec3(0., 1., 0.)),
1201            )
1202            .target(test_body)
1203            .test(());
1204    }
1205}