use crate::{ColliderShape, LayerMask};
use crate::sim::aabb::shape_bounds;
use crate::sim::body::Body;
use crate::sim::collide::Pose;
use crate::sim::math::{Quat, Vec3};
use crate::sim::query::field;
use crate::sim::query::gjk::Support;
use crate::sim::query::sweep::sweep;
use crate::sim::scene::Scene;
use crate::sim::sensor::swept;
pub(crate) struct Probe<'a> {
pub(crate) slot: u32,
pub(crate) shape: &'a ColliderShape,
pub(crate) rotation: Quat,
pub(crate) start: Vec3,
pub(crate) motion: Vec3,
pub(crate) mask: LayerMask,
pub(crate) expand: f32,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Blocked {
pub(crate) target: u32,
pub(crate) toi: f32,
pub(crate) normal: Vec3,
pub(crate) relative_motion: Vec3,
}
pub(crate) fn scan(
scene: Scene<'_>,
probe: &Probe<'_>,
motion_of: impl Fn(u32, &Body) -> Vec3,
mut on_region: impl FnMut(u32),
) -> Option<Blocked> {
let pose = Pose {
position: probe.start,
rotation: probe.rotation,
};
let moving = Support::new(probe.shape, pose);
let swept_bounds = shape_bounds(probe.shape, probe.start, probe.rotation)
.union(shape_bounds(
probe.shape,
probe.start + probe.motion,
probe.rotation,
))
.expanded(probe.expand);
let axis = scene.broadphase.axis();
let mut best: Option<Blocked> = None;
for &slot in scene
.broadphase
.slab_window(swept_bounds.min.get(axis), swept_bounds.max.get(axis))
{
if slot == probe.slot {
continue;
}
let proxy = scene.broadphase.proxy(slot);
if !probe.mask.interacts_with(proxy.mask) || !swept_bounds.overlaps(proxy.bounds) {
continue;
}
let Some(body) = scene.bodies.get_at(slot as usize) else {
continue;
};
if body.is_sensor() {
if let Some(shape) = body.convex()
&& swept::passed_through(&moving, probe.motion, &Support::new(shape, pose_of(body)))
{
on_region(slot);
}
continue;
}
let target_motion = motion_of(slot, body);
let relative = probe.motion - target_motion;
let found = match body.terrain_index() {
Some(index) => field::sweep(
scene.fields,
index,
probe.shape,
pose,
relative,
swept_bounds,
),
None => body.convex().and_then(|shape| {
let began = Pose {
position: body.position - target_motion,
rotation: body.orientation,
};
sweep(&moving, relative, &Support::new(shape, began))
}),
};
let Some(impact) = found else {
continue;
};
if impact.started_touching || impact.toi >= 1.0 {
continue;
}
if nearer(&best, slot, impact.toi) {
best = Some(Blocked {
target: slot,
toi: impact.toi,
normal: impact.normal,
relative_motion: relative,
});
}
}
best
}
pub(crate) fn still_crossed(
scene: Scene<'_>,
probe: &Probe<'_>,
motion: Vec3,
region: u32,
) -> bool {
let Some(body) = scene.bodies.get_at(region as usize) else {
return false;
};
let Some(shape) = body.convex() else {
return false;
};
let moving = Support::new(
probe.shape,
Pose {
position: probe.start,
rotation: probe.rotation,
},
);
swept::passed_through(&moving, motion, &Support::new(shape, pose_of(body)))
}
fn nearer(best: &Option<Blocked>, slot: u32, toi: f32) -> bool {
match best {
None => true,
Some(held) => toi < held.toi || (toi == held.toi && slot < held.target),
}
}
fn pose_of(body: &Body) -> Pose {
Pose {
position: body.position,
rotation: body.orientation,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::math::vec3;
fn blocked(target: u32, toi: f32) -> Option<Blocked> {
Some(Blocked {
target,
toi,
normal: vec3(0.0, 1.0, 0.0),
relative_motion: Vec3::ZERO,
})
}
#[test]
fn the_earliest_impact_wins_and_the_slot_breaks_a_tie() {
assert!(nearer(&None, 9, 0.9));
assert!(nearer(&blocked(3, 0.5), 7, 0.4));
assert!(!nearer(&blocked(3, 0.5), 7, 0.6));
assert!(nearer(&blocked(7, 0.5), 3, 0.5), "the lower slot holds it");
assert!(!nearer(&blocked(3, 0.5), 7, 0.5));
}
}