use rustsim_geometry::vec2::Vec2;
use rustsim_geometry::vec3::Vec3;
use crate::common::Pedestrian;
use crate::common::WallSegment;
pub type FloorId = i32;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct Pedestrian3D {
pub base: Pedestrian,
pub floor: FloorId,
pub z_offset: f64,
pub transition: Option<ActiveTransition>,
pub target_floor: Option<FloorId>,
}
impl Pedestrian3D {
pub fn grounded(base: Pedestrian, floor: FloorId) -> Self {
Self {
base,
floor,
z_offset: 0.0,
transition: None,
target_floor: None,
}
}
pub fn heading_to_floor(base: Pedestrian, floor: FloorId, target: FloorId) -> Self {
Self {
base,
floor,
z_offset: 0.0,
transition: None,
target_floor: Some(target),
}
}
pub fn pos_3d(&self, space: &LayeredSpace) -> Vec3 {
let z = space.floor_z(self.floor) + self.z_offset;
[self.base.pos[0], self.base.pos[1], z]
}
}
#[derive(Debug, Clone)]
pub struct WallPolygon3D {
pub floor: FloorId,
pub vertices: Vec<Vec2>,
}
impl WallPolygon3D {
pub fn to_segments(&self) -> Vec<WallSegment> {
if self.vertices.len() < 2 {
return Vec::new();
}
self.vertices
.windows(2)
.map(|w| WallSegment { a: w[0], b: w[1] })
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectorKind {
Stair,
Escalator,
Ramp,
Lift,
}
#[derive(Debug, Clone)]
pub struct FloorTransition {
pub id: u64,
pub kind: ConnectorKind,
pub from_floor: FloorId,
pub from_pos: Vec2,
pub to_floor: FloorId,
pub to_pos: Vec2,
pub boarding_radius: f64,
pub travel_time: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ActiveTransition {
pub connector_id: u64,
pub remaining: f64,
}
#[derive(Debug, Clone)]
pub struct LayeredSpace {
pub floor_elevations: Vec<(FloorId, f64)>,
pub walls: Vec<WallPolygon3D>,
pub connectors: Vec<FloorTransition>,
}
impl LayeredSpace {
pub fn new() -> Self {
Self {
floor_elevations: Vec::new(),
walls: Vec::new(),
connectors: Vec::new(),
}
}
pub fn set_floor(&mut self, floor: FloorId, z: f64) {
if let Some(entry) = self.floor_elevations.iter_mut().find(|(f, _)| *f == floor) {
entry.1 = z;
} else {
self.floor_elevations.push((floor, z));
}
}
pub fn floor_z(&self, floor: FloorId) -> f64 {
self.floor_elevations
.iter()
.find(|(f, _)| *f == floor)
.map(|(_, z)| *z)
.unwrap_or(0.0)
}
pub fn segments_on_floor(&self, floor: FloorId) -> Vec<WallSegment> {
let mut out = Vec::new();
for p in &self.walls {
if p.floor == floor {
out.extend(p.to_segments());
}
}
out
}
pub fn connector(&self, id: u64) -> Option<&FloorTransition> {
self.connectors.iter().find(|c| c.id == id)
}
pub fn connector_at(&self, floor: FloorId, pos: Vec2) -> Option<&FloorTransition> {
self.connectors.iter().find(|c| {
c.from_floor == floor
&& rustsim_geometry::vec2::distance(pos, c.from_pos) <= c.boarding_radius
})
}
}
impl Default for LayeredSpace {
fn default() -> Self {
Self::new()
}
}
pub type PlanarStepFn = fn(&mut [Pedestrian], &[WallSegment], &crate::social_force::Params, f64);
#[derive(Debug, Default)]
pub struct LayeredScratch {
pub(crate) floors: Vec<FloorId>,
pub(crate) idxs: Vec<usize>,
pub(crate) buf: Vec<Pedestrian>,
pub(crate) walls: Vec<WallSegment>,
}
impl LayeredScratch {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(n_peds: usize) -> Self {
Self {
floors: Vec::with_capacity(4),
idxs: Vec::with_capacity(n_peds),
buf: Vec::with_capacity(n_peds),
walls: Vec::with_capacity(32),
}
}
}
pub fn step_layered(
peds: &mut [Pedestrian3D],
space: &LayeredSpace,
planar_step: PlanarStepFn,
params: &crate::social_force::Params,
dt: f64,
) {
let mut scratch = LayeredScratch::with_capacity(peds.len());
step_layered_scratch(peds, space, planar_step, params, dt, &mut scratch);
}
pub fn step_layered_scratch(
peds: &mut [Pedestrian3D],
space: &LayeredSpace,
planar_step: PlanarStepFn,
params: &crate::social_force::Params,
dt: f64,
scratch: &mut LayeredScratch,
) {
step_layered_scratch_observed(
peds,
space,
planar_step,
params,
dt,
scratch,
&mut NoopLayeredObserver,
);
}
pub trait LayeredObserver {
fn observe(&mut self, index: usize, ped: &Pedestrian3D);
}
impl<F> LayeredObserver for F
where
F: FnMut(usize, &Pedestrian3D),
{
#[inline]
fn observe(&mut self, index: usize, ped: &Pedestrian3D) {
self(index, ped);
}
}
#[derive(Debug, Clone, Copy, Default)]
struct NoopLayeredObserver;
impl LayeredObserver for NoopLayeredObserver {
#[inline]
fn observe(&mut self, _index: usize, _ped: &Pedestrian3D) {}
}
#[allow(clippy::too_many_arguments)]
pub fn step_layered_scratch_observed<O>(
peds: &mut [Pedestrian3D],
space: &LayeredSpace,
planar_step: PlanarStepFn,
params: &crate::social_force::Params,
dt: f64,
scratch: &mut LayeredScratch,
observer: &mut O,
) where
O: LayeredObserver + ?Sized,
{
step_layered_scratch_inner(peds, space, planar_step, params, dt, scratch);
for (i, ped) in peds.iter().enumerate() {
observer.observe(i, ped);
}
}
fn step_layered_scratch_inner(
peds: &mut [Pedestrian3D],
space: &LayeredSpace,
planar_step: PlanarStepFn,
params: &crate::social_force::Params,
dt: f64,
scratch: &mut LayeredScratch,
) {
for p in peds.iter_mut() {
if let Some(t) = p.transition.as_mut() {
t.remaining -= dt;
if t.remaining <= 0.0 {
if let Some(c) = space.connector(t.connector_id) {
p.floor = c.to_floor;
p.base.pos = c.to_pos;
p.base.vel = [0.0, 0.0];
p.z_offset = 0.0;
}
p.transition = None;
} else if let Some(c) = space.connector(t.connector_id) {
let travel = c.travel_time.max(1e-9);
let t_frac = 1.0 - (t.remaining / travel).clamp(0.0, 1.0);
let dz = space.floor_z(c.to_floor) - space.floor_z(c.from_floor);
p.z_offset = t_frac * dz;
}
}
}
scratch.floors.clear();
for p in peds.iter() {
if p.transition.is_none() && !scratch.floors.contains(&p.floor) {
scratch.floors.push(p.floor);
}
}
scratch.floors.sort();
let n_floors = scratch.floors.len();
for fi in 0..n_floors {
let floor = scratch.floors[fi];
scratch.idxs.clear();
scratch.buf.clear();
for (i, p) in peds.iter().enumerate() {
if p.transition.is_none() && p.floor == floor {
scratch.idxs.push(i);
scratch.buf.push(p.base);
}
}
scratch.walls.clear();
for polygon in &space.walls {
if polygon.floor == floor {
if polygon.vertices.len() >= 2 {
for w in polygon.vertices.windows(2) {
scratch.walls.push(WallSegment { a: w[0], b: w[1] });
}
}
}
}
planar_step(&mut scratch.buf, &scratch.walls, params, dt);
for (k, &i) in scratch.idxs.iter().enumerate() {
peds[i].base = scratch.buf[k];
}
}
for p in peds.iter_mut() {
if p.transition.is_some() {
continue;
}
let wants_transfer = match p.target_floor {
Some(f) => f != p.floor,
None => false,
};
if !wants_transfer {
continue;
}
if let Some(c) = space.connector_at(p.floor, p.base.pos) {
if Some(c.to_floor) != p.target_floor {
continue;
}
p.transition = Some(ActiveTransition {
connector_id: c.id,
remaining: c.travel_time,
});
}
}
}
#[cfg(test)]
#[allow(deprecated)] mod tests {
use super::*;
use crate::common::Pedestrian;
use crate::social_force;
fn ped(pos: Vec2, dest: Vec2) -> Pedestrian {
Pedestrian {
pos,
vel: [0.0, 0.0],
radius: 0.25,
desired_speed: 1.34,
destination: dest,
}
}
fn two_floor_space() -> LayeredSpace {
let mut s = LayeredSpace::new();
s.set_floor(0, 0.0);
s.set_floor(1, 4.0);
s.connectors.push(FloorTransition {
id: 1,
kind: ConnectorKind::Escalator,
from_floor: 0,
from_pos: [10.0, 0.0],
to_floor: 1,
to_pos: [10.0, 0.0],
boarding_radius: 0.4,
travel_time: 10.0,
});
s
}
#[test]
fn grounded_pedestrian_moves_on_its_floor() {
let space = two_floor_space();
let mut peds = vec![Pedestrian3D::grounded(ped([0.0, 0.0], [5.0, 0.0]), 0)];
for _ in 0..30 {
step_layered(
&mut peds,
&space,
social_force::step,
&social_force::Params::default(),
0.1,
);
}
assert!(peds[0].base.pos[0] > 0.5);
assert_eq!(peds[0].floor, 0);
}
#[test]
fn boarding_and_alighting_transitions_between_floors() {
let space = two_floor_space();
let mut peds = vec![Pedestrian3D::heading_to_floor(
ped([10.0, 0.0], [10.0, 0.0]),
0,
1,
)];
step_layered(
&mut peds,
&space,
social_force::step,
&social_force::Params::default(),
0.1,
);
assert!(peds[0].transition.is_some());
for _ in 0..200 {
step_layered(
&mut peds,
&space,
social_force::step,
&social_force::Params::default(),
0.1,
);
}
assert!(peds[0].transition.is_none());
assert_eq!(peds[0].floor, 1);
}
#[test]
fn does_not_reboard_after_alighting_into_same_boarding_zone() {
let space = two_floor_space();
let mut peds = vec![Pedestrian3D::heading_to_floor(
ped([10.0, 0.0], [10.0, 0.0]),
0,
1,
)];
for _ in 0..200 {
step_layered(
&mut peds,
&space,
social_force::step,
&social_force::Params::default(),
0.1,
);
}
assert_eq!(peds[0].floor, 1);
peds[0].target_floor = None;
for _ in 0..500 {
step_layered(
&mut peds,
&space,
social_force::step,
&social_force::Params::default(),
0.1,
);
assert!(peds[0].transition.is_none(), "pedestrian re-boarded");
assert_eq!(peds[0].floor, 1);
}
}
#[test]
fn pos_3d_combines_floor_elevation_and_z_offset() {
let space = two_floor_space();
let p = Pedestrian3D {
base: ped([1.0, 2.0], [0.0, 0.0]),
floor: 1,
z_offset: 0.5,
transition: None,
target_floor: None,
};
let p3 = p.pos_3d(&space);
assert_eq!(p3, [1.0, 2.0, 4.5]);
}
#[test]
fn step_layered_scratch_matches_step_layered_bit_exact() {
let space = two_floor_space();
let params = social_force::Params::default();
let seed_peds = || -> Vec<Pedestrian3D> {
vec![
Pedestrian3D::heading_to_floor(ped([0.0, 0.0], [10.0, 0.0]), 0, 1),
Pedestrian3D::heading_to_floor(ped([2.0, 1.0], [10.0, 0.0]), 0, 1),
Pedestrian3D::grounded(ped([5.0, -0.5], [20.0, 0.0]), 0),
Pedestrian3D::grounded(ped([7.0, 0.5], [-5.0, 0.0]), 1),
]
};
let mut a = seed_peds();
let mut b = seed_peds();
let mut scratch = LayeredScratch::with_capacity(a.len());
for _ in 0..50 {
step_layered(&mut a, &space, social_force::step, ¶ms, 0.1);
step_layered_scratch(
&mut b,
&space,
social_force::step,
¶ms,
0.1,
&mut scratch,
);
}
assert_eq!(a.len(), b.len());
for (pa, pb) in a.iter().zip(b.iter()) {
assert_eq!(pa.floor, pb.floor);
assert_eq!(pa.base.pos, pb.base.pos);
assert_eq!(pa.base.vel, pb.base.vel);
assert_eq!(pa.transition.is_some(), pb.transition.is_some());
}
}
#[test]
fn layered_scratch_reuses_capacity_across_ticks() {
let space = two_floor_space();
let params = social_force::Params::default();
let mut peds = vec![
Pedestrian3D::grounded(ped([0.0, 0.0], [5.0, 0.0]), 0),
Pedestrian3D::grounded(ped([1.0, 1.0], [5.0, 0.0]), 0),
Pedestrian3D::grounded(ped([2.0, -1.0], [5.0, 0.0]), 1),
];
let mut scratch = LayeredScratch::with_capacity(peds.len());
step_layered_scratch(
&mut peds,
&space,
social_force::step,
¶ms,
0.1,
&mut scratch,
);
let (cap_floors, cap_idxs, cap_buf, cap_walls) = (
scratch.floors.capacity(),
scratch.idxs.capacity(),
scratch.buf.capacity(),
scratch.walls.capacity(),
);
for _ in 0..20 {
step_layered_scratch(
&mut peds,
&space,
social_force::step,
¶ms,
0.1,
&mut scratch,
);
}
assert_eq!(scratch.floors.capacity(), cap_floors);
assert_eq!(scratch.idxs.capacity(), cap_idxs);
assert_eq!(scratch.buf.capacity(), cap_buf);
assert_eq!(scratch.walls.capacity(), cap_walls);
}
#[test]
fn observed_layered_step_matches_unobserved_and_streams_in_index_order() {
let space = two_floor_space();
let params = social_force::Params::default();
let make_peds = || {
vec![
Pedestrian3D::heading_to_floor(ped([9.7, 0.0], [10.0, 0.0]), 0, 1),
Pedestrian3D::grounded(ped([0.0, 0.0], [3.0, 0.0]), 0),
Pedestrian3D::grounded(ped([0.0, 5.0], [3.0, 5.0]), 1),
]
};
let mut peds_a = make_peds();
let mut peds_b = make_peds();
let mut scratch_a = LayeredScratch::with_capacity(peds_a.len());
let mut scratch_b = LayeredScratch::with_capacity(peds_b.len());
let mut log: Vec<(usize, FloorId, bool, f64)> = Vec::new();
let total_ticks = 20;
for _ in 0..total_ticks {
step_layered_scratch(
&mut peds_a,
&space,
social_force::step,
¶ms,
0.1,
&mut scratch_a,
);
step_layered_scratch_observed(
&mut peds_b,
&space,
social_force::step,
¶ms,
0.1,
&mut scratch_b,
&mut |i: usize, ped: &Pedestrian3D| {
log.push((i, ped.floor, ped.transition.is_some(), ped.base.pos[0]));
},
);
}
assert_eq!(
peds_a, peds_b,
"observed step must not perturb the underlying tick"
);
let n = peds_a.len();
assert_eq!(log.len(), n * total_ticks);
for (row_idx, row) in log.iter().enumerate() {
assert_eq!(row.0, row_idx % n, "observer must stream in index order");
}
let last_for_zero = log
.iter()
.rev()
.find(|r| r.0 == 0)
.expect("agent 0 must appear in the log");
assert!(
last_for_zero.1 == 1 || last_for_zero.2,
"agent 0 should have boarded the connector or already alighted on floor 1, got {:?}",
last_for_zero
);
}
}