use crate::octree::OctreeNode;
use crate::store::ZoneStore;
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Hole {
pub center: [f64; 3],
pub size_m: f64,
pub depth: u8,
}
fn voxel_size(octree: &OctreeNode, depth: u8) -> Option<f64> {
let v = octree.half_len * 2.0 / (1u64 << depth) as f64;
(v > 0.0).then_some(v)
}
fn for_each_zone_voxel<F: FnMut([f64; 3], [f64; 3])>(
octree: &OctreeNode,
store: &ZoneStore,
zone_id: u32,
depth: u8,
mut f: F,
) {
let Some(aabb) = store.zone_aabb(zone_id) else {
return;
};
let Some(voxel) = voxel_size(octree, depth) else {
return;
};
let mut x = aabb[0];
while x < aabb[3] {
let mut y = aabb[1];
while y < aabb[4] {
let mut z = aabb[2];
while z < aabb[5] {
let center = [x + voxel / 2.0, y + voxel / 2.0, z + voxel / 2.0];
if store.zone_contains(zone_id, center) {
f([x, y, z], center);
}
z += voxel;
}
y += voxel;
}
x += voxel;
}
}
#[must_use]
pub fn scan_holes(octree: &OctreeNode, store: &ZoneStore, zone_id: u32, depth: u8) -> Vec<Hole> {
let Some(voxel) = voxel_size(octree, depth) else {
return vec![];
};
let mut holes = vec![];
for_each_zone_voxel(octree, store, zone_id, depth, |min, center| {
let occupied = !octree
.range_query(min, [min[0] + voxel, min[1] + voxel, min[2] + voxel])
.is_empty();
if !occupied {
holes.push(Hole { center, size_m: voxel, depth });
}
});
holes
}
pub fn voxel_count_in_zone(octree: &OctreeNode, store: &ZoneStore, zone_id: u32, depth: u8) -> usize {
let mut count = 0;
for_each_zone_voxel(octree, store, zone_id, depth, |_, _| count += 1);
count
}
#[must_use]
pub fn scan_holes_multiscale(
octree: &OctreeNode,
store: &ZoneStore,
zone_id: u32,
depths: &[u8],
) -> Vec<(u8, Vec<Hole>)> {
depths.iter().map(|&d| (d, scan_holes(octree, store, zone_id, d))).collect()
}
#[derive(Debug, Clone, Copy)]
pub enum ScanMode {
Coarse { depth: u8 },
Precise { depth: u8 },
Adaptive { coarse_depth: u8, refine_depth: u8, min_hole_frac: f64 },
}
#[derive(Debug, Clone)]
pub struct ScanResult {
pub holes: Vec<Hole>,
pub coverage_pct: f64,
pub depth_used: u8,
pub node_count: usize,
}
#[must_use]
pub fn run_scan(octree: &OctreeNode, store: &ZoneStore, zone_id: u32, mode: &ScanMode) -> ScanResult {
let scan = |depth: u8| -> ScanResult {
let holes = scan_holes(octree, store, zone_id, depth);
let total = voxel_count_in_zone(octree, store, zone_id, depth);
ScanResult {
coverage_pct: 100.0 * (1.0 - holes.len() as f64 / total.max(1) as f64),
node_count: total,
depth_used: depth,
holes,
}
};
match mode {
ScanMode::Coarse { depth } | ScanMode::Precise { depth } => scan(*depth),
ScanMode::Adaptive { coarse_depth, refine_depth, min_hole_frac } => {
let coarse = scan(*coarse_depth);
let frac = coarse.holes.len() as f64 / coarse.node_count.max(1) as f64;
if frac >= *min_hole_frac {
scan(*refine_depth)
} else {
coarse
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coord::EnuConverter;
use crate::zone::{Zone, ZoneEntry};
fn fixture() -> (OctreeNode, ZoneStore) {
let conv = EnuConverter::new(0.0, 0.0, 0.0);
let store = ZoneStore::from_entries(
&[ZoneEntry::new(
1,
Zone::Cylinder { center: [0.0, 0.0], radius_m: 40.0, z_min: -5.0, z_max: 5.0 },
)],
&conv,
);
let octree = OctreeNode::new([0.0; 3], 64.0); (octree, store)
}
#[test]
fn empty_octree_is_all_holes() {
let (octree, store) = fixture();
let holes = scan_holes(&octree, &store, 1, 4);
let total = voxel_count_in_zone(&octree, &store, 1, 4);
assert!(total > 0);
assert_eq!(holes.len(), total, "no points → every in-zone voxel is a hole");
}
#[test]
fn occupied_voxel_is_not_a_hole() {
let (mut octree, store) = fixture();
let total = voxel_count_in_zone(&octree, &store, 1, 4);
octree.insert([4.0, 4.0, -1.0], 1);
let holes = scan_holes(&octree, &store, 1, 4);
assert_eq!(holes.len(), total - 1, "exactly one voxel now occupied");
}
#[test]
fn unknown_zone_yields_nothing() {
let (octree, store) = fixture();
assert!(scan_holes(&octree, &store, 99, 4).is_empty());
assert_eq!(voxel_count_in_zone(&octree, &store, 99, 4), 0);
}
#[test]
fn coverage_full_when_no_holes() {
let (mut octree, store) = fixture();
let voxel = octree.half_len * 2.0 / (1u64 << 4) as f64;
let mut x = -40.0;
while x <= 40.0 {
let mut y = -40.0;
while y <= 40.0 {
octree.insert([x, y, 0.0], 64);
y += voxel / 2.0;
}
x += voxel / 2.0;
}
let res = run_scan(&octree, &store, 1, &ScanMode::Coarse { depth: 4 });
assert!(res.coverage_pct > 99.0, "expected near-full coverage, got {}", res.coverage_pct);
}
#[test]
fn adaptive_refines_when_sparse() {
let (octree, store) = fixture();
let res = run_scan(
&octree,
&store,
1,
&ScanMode::Adaptive { coarse_depth: 3, refine_depth: 5, min_hole_frac: 0.5 },
);
assert_eq!(res.depth_used, 5, "should have refined to depth 5");
}
#[test]
fn multiscale_returns_each_depth() {
let (octree, store) = fixture();
let out = scan_holes_multiscale(&octree, &store, 1, &[3, 4]);
assert_eq!(out.len(), 2);
assert_eq!(out[0].0, 3);
assert_eq!(out[1].0, 4);
assert!(out[1].1.len() > out[0].1.len());
}
}