use slotmap::SecondaryMap;
use crate::meshes::MeshKey;
#[derive(Clone, Copy, Debug)]
pub struct LodLevel {
pub mesh_key: MeshKey,
pub error: f32,
}
#[derive(Clone, Debug, Default)]
pub struct LodChain {
pub levels: Vec<LodLevel>,
pub bounds_radius: f32,
pub current_level: usize,
}
impl LodChain {
pub fn key_for_level(&self, base: MeshKey, level: usize) -> MeshKey {
if level == 0 {
base
} else {
self.levels[level - 1].mesh_key
}
}
}
#[derive(Default)]
pub struct LodRegistry {
chains: SecondaryMap<MeshKey, LodChain>,
}
impl LodRegistry {
pub fn is_empty(&self) -> bool {
self.chains.is_empty()
}
pub fn register(&mut self, base: MeshKey, chain: LodChain) {
self.chains.insert(base, chain);
}
pub fn get(&self, base: MeshKey) -> Option<&LodChain> {
self.chains.get(base)
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (MeshKey, &mut LodChain)> {
self.chains.iter_mut()
}
pub fn clear(&mut self) {
self.chains.clear();
}
pub fn level_keys(&self) -> impl Iterator<Item = MeshKey> + '_ {
self.chains
.values()
.flat_map(|c| c.levels.iter().map(|l| l.mesh_key))
}
}
pub fn projected_px_per_unit(distance: f32, tan_half_fov_y: f32, viewport_h: f32) -> f32 {
if distance <= 1e-6 || tan_half_fov_y <= 1e-6 {
return f32::INFINITY;
}
(viewport_h * 0.5) / (distance * tan_half_fov_y)
}
pub fn select_level(
chain: &LodChain,
px_per_unit: f32,
world_scale: f32,
error_threshold_px: f32,
) -> usize {
let mut chosen = 0;
for (i, lvl) in chain.levels.iter().enumerate() {
let projected = lvl.error * world_scale * px_per_unit;
if projected <= error_threshold_px {
chosen = i + 1;
} else {
break;
}
}
chosen
}
#[cfg(test)]
mod tests {
use super::*;
fn keys(n: usize) -> Vec<MeshKey> {
let mut sm: slotmap::SlotMap<MeshKey, ()> = slotmap::SlotMap::with_key();
(0..n).map(|_| sm.insert(())).collect()
}
fn chain(errors: &[f32], ks: &[MeshKey]) -> LodChain {
LodChain {
levels: errors
.iter()
.zip(ks)
.map(|(&error, &mesh_key)| LodLevel { mesh_key, error })
.collect(),
bounds_radius: 1.0,
..Default::default()
}
}
#[test]
fn projection_scales_inversely_with_distance() {
let near = projected_px_per_unit(1.0, 0.5, 1080.0);
let far = projected_px_per_unit(10.0, 0.5, 1080.0);
assert!((near / far - 10.0).abs() < 1e-3, "px/unit ∝ 1/distance");
assert_eq!(projected_px_per_unit(0.0, 0.5, 1080.0), f32::INFINITY);
}
#[test]
fn close_instance_picks_base() {
let ks = keys(3);
let c = chain(&[0.01, 0.05, 0.2], &ks);
let level = select_level(&c, 10_000.0, 1.0, 1.0);
assert_eq!(level, 0);
}
#[test]
fn far_instance_picks_coarsest() {
let ks = keys(3);
let c = chain(&[0.01, 0.05, 0.2], &ks);
let level = select_level(&c, 1.0, 1.0, 1.0);
assert_eq!(level, 3);
assert_eq!(c.key_for_level(ks[0], level), ks[2]);
}
#[test]
fn mid_distance_picks_a_middle_level() {
let ks = keys(3);
let c = chain(&[0.01, 0.05, 0.2], &ks);
let level = select_level(&c, 20.0, 1.0, 1.0);
assert_eq!(level, 2);
assert_eq!(c.key_for_level(ks[0], 0), ks[0]); assert_eq!(c.key_for_level(ks[0], 2), ks[1]);
}
#[test]
fn larger_scale_biases_toward_finer_levels() {
let ks = keys(3);
let c = chain(&[0.01, 0.05, 0.2], &ks);
let at_1 = select_level(&c, 20.0, 1.0, 1.0);
let at_5 = select_level(&c, 20.0, 5.0, 1.0);
assert!(at_5 <= at_1, "scaling up must not pick a coarser level");
assert!(at_5 < at_1, "5× scale should pick strictly finer here");
}
#[test]
fn registry_round_trip_and_level_keys() {
let ks = keys(3);
let base = ks[0];
let mut reg = LodRegistry::default();
assert!(reg.is_empty());
reg.register(base, chain(&[0.05, 0.2], &ks[1..]));
assert!(!reg.is_empty());
let got = reg.get(base).unwrap();
assert_eq!(got.levels.len(), 2);
let level_keys: Vec<_> = reg.level_keys().collect();
assert_eq!(level_keys, vec![ks[1], ks[2]]);
reg.clear();
assert!(reg.is_empty());
}
}