use crate::OccupancyGrid3D;
pub const CLASS_PERSON: u8 = 10;
pub const CLASS_FREE: u8 = 17;
pub const GRID_WIDTH: usize = 200;
pub const GRID_HEIGHT: usize = 200;
pub const GRID_DEPTH: usize = 16;
const MAX_HEIGHT_M: f32 = 3.2;
#[derive(Debug, Clone)]
pub struct PersonPosition {
pub track_id: u64,
pub east_m: f64,
pub north_m: f64,
pub up_m: f64,
}
#[derive(Debug, Clone)]
pub struct SceneBounds {
pub min_e: f64,
pub min_n: f64,
pub max_e: f64,
pub max_n: f64,
}
impl SceneBounds {
fn extents(&self) -> (f64, f64) {
let e = (self.max_e - self.min_e).max(1.0);
let n = (self.max_n - self.min_n).max(1.0);
(e, n)
}
pub fn to_voxel_xy(&self, east_m: f64, north_m: f64) -> (usize, usize) {
let (e_ext, n_ext) = self.extents();
let fx = (east_m - self.min_e) / e_ext; let fy = (north_m - self.min_n) / n_ext; let vx = (fx * GRID_WIDTH as f64)
.floor()
.clamp(0.0, (GRID_WIDTH - 1) as f64) as usize;
let vy = (fy * GRID_HEIGHT as f64)
.floor()
.clamp(0.0, (GRID_HEIGHT - 1) as f64) as usize;
(vx, vy)
}
pub fn to_voxel_z(up_m: f64) -> usize {
let fz = (up_m as f32).clamp(0.0, MAX_HEIGHT_M) / MAX_HEIGHT_M;
let vz = (fz * GRID_DEPTH as f32)
.floor()
.clamp(0.0, (GRID_DEPTH - 1) as f32) as usize;
vz
}
}
pub fn worldgraph_to_occupancy(
persons: &[PersonPosition],
bounds: &SceneBounds,
_resolution_m: f32,
) -> OccupancyGrid3D {
let total = GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH;
let mut voxels = vec![CLASS_FREE; total];
for p in persons {
let (vx, vy) = bounds.to_voxel_xy(p.east_m, p.north_m);
let vz = SceneBounds::to_voxel_z(p.up_m);
let idx = vz * GRID_HEIGHT * GRID_WIDTH + vy * GRID_WIDTH + vx;
voxels[idx] = CLASS_PERSON;
}
OccupancyGrid3D {
width: GRID_WIDTH as u32,
height: GRID_HEIGHT as u32,
depth: GRID_DEPTH as u32,
voxels,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_bounds() -> SceneBounds {
SceneBounds {
min_e: -10.0,
min_n: -10.0,
max_e: 10.0,
max_n: 10.0,
}
}
#[test]
fn empty_persons_all_free() {
let g = worldgraph_to_occupancy(&[], &default_bounds(), 0.1);
assert!(g.voxels.iter().all(|&v| v == CLASS_FREE));
assert_eq!(g.voxels.len(), GRID_WIDTH * GRID_HEIGHT * GRID_DEPTH);
}
#[test]
fn person_at_origin_maps_to_centre_voxel() {
let bounds = default_bounds(); let persons = vec![PersonPosition {
track_id: 1,
east_m: 0.0,
north_m: 0.0,
up_m: 0.0,
}];
let g = worldgraph_to_occupancy(&persons, &bounds, 0.1);
let expected_idx = 0 * GRID_HEIGHT * GRID_WIDTH + 100 * GRID_WIDTH + 100;
assert_eq!(g.voxels[expected_idx], CLASS_PERSON);
let person_count = g.voxels.iter().filter(|&&v| v == CLASS_PERSON).count();
assert_eq!(person_count, 1);
}
#[test]
fn out_of_bounds_position_is_clamped() {
let bounds = default_bounds();
let persons = vec![PersonPosition {
track_id: 2,
east_m: 99.0, north_m: 99.0,
up_m: 100.0,
}];
let g = worldgraph_to_occupancy(&persons, &bounds, 0.1);
let person_count = g.voxels.iter().filter(|&&v| v == CLASS_PERSON).count();
assert_eq!(person_count, 1);
}
#[test]
fn multiple_persons_independent_voxels() {
let bounds = default_bounds();
let persons = vec![
PersonPosition { track_id: 1, east_m: -5.0, north_m: -5.0, up_m: 0.5 },
PersonPosition { track_id: 2, east_m: 5.0, north_m: 5.0, up_m: 1.5 },
];
let g = worldgraph_to_occupancy(&persons, &bounds, 0.1);
let person_count = g.voxels.iter().filter(|&&v| v == CLASS_PERSON).count();
assert_eq!(person_count, 2);
}
#[test]
fn grid_dimensions_correct() {
let g = worldgraph_to_occupancy(&[], &default_bounds(), 0.4);
assert_eq!(g.width, 200);
assert_eq!(g.height, 200);
assert_eq!(g.depth, 16);
assert_eq!(g.voxels.len(), 200 * 200 * 16);
}
}