use rand_chacha::ChaCha8Rng;
use crate::placement::ir::{CandidateIx, Placement};
use crate::rng::DetRng;
pub const INNER_GRID: usize = 9;
#[derive(Clone, Copy, Debug)]
pub struct SupportSurface {
pub cx: f32,
pub cz: f32,
pub half_x: f32,
pub half_z: f32,
pub top_y: f32,
pub yaw: f32,
pub provides: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct ScatterProp {
pub candidate: CandidateIx,
pub half_x: f32,
pub half_z: f32,
pub requires: u32,
}
#[inline]
fn idx(ox: usize, oz: usize) -> usize {
oz * INNER_GRID + ox
}
fn block_free(occ: &[bool], ox: usize, oz: usize, span_x: usize, span_z: usize) -> bool {
for dz in 0..span_z {
for dx in 0..span_x {
if occ[idx(ox + dx, oz + dz)] {
return false;
}
}
}
true
}
fn mark(occ: &mut [bool], ox: usize, oz: usize, span_x: usize, span_z: usize) {
for dz in 0..span_z {
for dx in 0..span_x {
occ[idx(ox + dx, oz + dz)] = true;
}
}
}
pub fn scatter_on_surfaces(
surfaces: &[SupportSurface],
props: &[ScatterProp],
rng: &mut ChaCha8Rng,
) -> Vec<Placement> {
let mut out = Vec::new();
if surfaces.is_empty() {
return out;
}
let mut occ: Vec<Vec<bool>> = vec![vec![false; INNER_GRID * INNER_GRID]; surfaces.len()];
for prop in props {
let start = rng.below(surfaces.len());
for s_off in 0..surfaces.len() {
let si = (start + s_off) % surfaces.len();
let s = surfaces[si];
if s.provides & prop.requires == 0 {
continue;
}
let cell_w = (2.0 * s.half_x) / INNER_GRID as f32;
let cell_d = (2.0 * s.half_z) / INNER_GRID as f32;
if cell_w <= 0.0 || cell_d <= 0.0 {
continue;
}
let span_x = ((prop.half_x * 2.0) / cell_w).ceil().max(1.0) as usize;
let span_z = ((prop.half_z * 2.0) / cell_d).ceil().max(1.0) as usize;
if span_x > INNER_GRID || span_z > INNER_GRID {
continue; }
let max_ox = INNER_GRID - span_x;
let max_oz = INNER_GRID - span_z;
let ox0 = rng.below(max_ox + 1);
let oz0 = rng.below(max_oz + 1);
let mut done = false;
for j in 0..=max_oz {
for i in 0..=max_ox {
let ox = (ox0 + i) % (max_ox + 1);
let oz = (oz0 + j) % (max_oz + 1);
if block_free(&occ[si], ox, oz, span_x, span_z) {
mark(&mut occ[si], ox, oz, span_x, span_z);
let bx = ox as f32 + span_x as f32 * 0.5;
let bz = oz as f32 + span_z as f32 * 0.5;
let x = s.cx - s.half_x + bx * cell_w;
let z = s.cz - s.half_z + bz * cell_d;
out.push(Placement {
candidate: prop.candidate,
pos: [x, s.top_y, z],
yaw: s.yaw,
});
done = true;
break;
}
}
if done {
break;
}
}
if done {
break;
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rng::seeded;
fn surface(cx: f32, cz: f32, half_x: f32, half_z: f32, top_y: f32) -> SupportSurface {
SupportSurface {
cx,
cz,
half_x,
half_z,
top_y,
yaw: 0.0,
provides: !0,
}
}
fn prop(candidate: usize, half_x: f32, half_z: f32) -> ScatterProp {
ScatterProp {
candidate,
half_x,
half_z,
requires: 1,
}
}
#[test]
fn props_rest_on_the_surface_within_its_footprint() {
let s = surface(5.0, 3.0, 0.6, 0.3, 0.74);
let props = [prop(0, 0.16, 0.16), prop(1, 0.09, 0.09), prop(2, 0.1, 0.1)];
let mut rng = seeded(4);
let placed = scatter_on_surfaces(&[s], &props, &mut rng);
assert_eq!(placed.len(), 3, "all three small props fit on the desk");
for p in &placed {
assert!(
(p.pos[1] - 0.74).abs() < 1e-6,
"prop must rest at the surface top, not the floor"
);
assert!(
(p.pos[0] - s.cx).abs() <= s.half_x + 1e-4,
"prop x within surface footprint"
);
assert!(
(p.pos[2] - s.cz).abs() <= s.half_z + 1e-4,
"prop z within surface footprint"
);
}
for a in 0..placed.len() {
for b in (a + 1)..placed.len() {
let same = (placed[a].pos[0] - placed[b].pos[0]).abs() < 1e-6
&& (placed[a].pos[2] - placed[b].pos[2]).abs() < 1e-6;
assert!(!same, "two props landed on the same sub-cell");
}
}
}
#[test]
fn a_prop_too_big_for_any_surface_is_dropped() {
let s = surface(0.0, 0.0, 0.25, 0.25, 0.5); let props = [prop(0, 0.9, 0.9)]; let mut rng = seeded(1);
let placed = scatter_on_surfaces(&[s], &props, &mut rng);
assert!(
placed.is_empty(),
"an over-sized prop is dropped, never forced on"
);
}
#[test]
fn a_prop_only_rests_on_a_matching_surface_class() {
const SUPPORT: u32 = 0b01;
const WORKTOP: u32 = 0b10;
let worktop = SupportSurface {
cx: 0.0,
cz: 0.0,
half_x: 0.6,
half_z: 0.3,
top_y: 0.74,
yaw: 0.0,
provides: SUPPORT | WORKTOP,
};
let bed = SupportSurface {
cx: 10.0,
cz: 10.0,
half_x: 0.86,
half_z: 1.11,
top_y: 0.55,
yaw: 0.0,
provides: SUPPORT,
};
let lamp = ScatterProp {
candidate: 0,
half_x: 0.16,
half_z: 0.16,
requires: WORKTOP,
};
for seed in 0..32u64 {
let mut rng = seeded(seed);
let placed = scatter_on_surfaces(&[worktop, bed], &[lamp], &mut rng);
assert_eq!(placed.len(), 1, "lamp fits on the worktop");
assert!(
(placed[0].pos[1] - worktop.top_y).abs() < 1e-6,
"lamp rested at the worktop height, not the bed"
);
assert!(
(placed[0].pos[0] - worktop.cx).abs() <= worktop.half_x + 1e-4
&& (placed[0].pos[2] - worktop.cz).abs() <= worktop.half_z + 1e-4,
"lamp landed inside the worktop footprint (never near the bed at (10,10))"
);
}
let mut rng = seeded(1);
assert!(
scatter_on_surfaces(&[bed], &[lamp], &mut rng).is_empty(),
"no worktop -> no lamp (never falls back onto a bed)"
);
}
#[test]
fn no_surfaces_places_nothing() {
let props = [prop(0, 0.1, 0.1)];
let mut rng = seeded(1);
assert!(scatter_on_surfaces(&[], &props, &mut rng).is_empty());
}
#[test]
fn deterministic_under_a_seed() {
let s = surface(2.0, 2.0, 0.6, 0.3, 0.74);
let props = [prop(0, 0.16, 0.16), prop(1, 0.1, 0.1)];
let run = || {
let mut rng = seeded(9);
scatter_on_surfaces(&[s], &props, &mut rng)
.iter()
.map(|p| (p.pos[0].to_bits(), p.pos[1].to_bits(), p.pos[2].to_bits()))
.collect::<Vec<_>>()
};
assert_eq!(run(), run(), "same seed must reproduce the same scatter");
}
}