use serde::{Deserialize, Serialize};
use crate::world_segment::ids::ContentId;
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct PartitionHint {
pub id: String,
pub bbox_xz: (i32, i32, i32, i32),
pub y_range: Option<(i32, i32)>,
}
impl PartitionHint {
pub fn contains(&self, x: i32, y: i32, z: i32) -> bool {
let (x0, x1, z0, z1) = self.bbox_xz;
if x < x0 || x > x1 || z < z0 || z > z1 {
return false;
}
match self.y_range {
Some((y0, y1)) => y >= y0 && y <= y1,
None => true,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum PartitionPolicy {
HardCut,
Prefer,
Off,
}
pub struct PartitionIndex {
hints: Vec<PartitionHint>,
}
impl PartitionIndex {
pub fn new(mut hints: Vec<PartitionHint>) -> Self {
hints.sort_by(|a, b| {
a.id.cmp(&b.id)
.then_with(|| a.bbox_xz.cmp(&b.bbox_xz))
.then_with(|| a.y_range.cmp(&b.y_range))
});
PartitionIndex { hints }
}
pub fn is_empty(&self) -> bool {
self.hints.is_empty()
}
pub fn hints_hash(&self) -> ContentId {
let mut parts: Vec<Vec<u8>> = Vec::with_capacity(self.hints.len() * 8 + 2);
parts.push(b"parthints.v1".to_vec());
parts.push((self.hints.len() as u64).to_le_bytes().to_vec());
for h in &self.hints {
parts.push(h.id.as_bytes().to_vec());
let (x0, x1, z0, z1) = h.bbox_xz;
for v in [x0, x1, z0, z1] {
parts.push(v.to_le_bytes().to_vec());
}
match h.y_range {
Some((y0, y1)) => {
parts.push(vec![1]);
parts.push(y0.to_le_bytes().to_vec());
parts.push(y1.to_le_bytes().to_vec());
}
None => {
parts.push(vec![0]);
parts.push(Vec::new());
parts.push(Vec::new());
}
}
}
let refs: Vec<&[u8]> = parts.iter().map(|p| p.as_slice()).collect();
ContentId::of(&refs)
}
pub fn partition_at(&self, x: i32, y: i32, z: i32) -> Option<&str> {
self.hints.iter().find(|h| h.contains(x, y, z)).map(|h| h.id.as_str())
}
pub fn id_index_at(&self, x: i32, y: i32, z: i32) -> Option<u32> {
self.hints.iter().position(|h| h.contains(x, y, z)).map(|i| i as u32)
}
pub fn id_of_index(&self, index: u32) -> &str {
debug_assert!(
(index as usize) < self.hints.len(),
"id_of_index: index {index} out of range for {} hints; index must come from id_index_at on this same PartitionIndex",
self.hints.len()
);
&self.hints[index as usize].id
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hints() -> Vec<PartitionHint> {
vec![
PartitionHint { id: "a".into(), bbox_xz: (0, 9, 0, 9), y_range: None },
PartitionHint { id: "b".into(), bbox_xz: (11, 20, 0, 9), y_range: None },
PartitionHint { id: "c".into(), bbox_xz: (0, 9, 11, 20), y_range: Some((0, 10)) },
]
}
#[test]
fn point_inside_a_hint_resolves_to_it() {
let idx = PartitionIndex::new(hints());
assert_eq!(idx.partition_at(5, 100, 5), Some("a"));
assert_eq!(idx.partition_at(15, 100, 5), Some("b"));
}
#[test]
fn bbox_edges_are_inclusive() {
let idx = PartitionIndex::new(hints());
assert_eq!(idx.partition_at(0, 0, 0), Some("a"));
assert_eq!(idx.partition_at(9, 0, 9), Some("a"));
}
#[test]
fn gaps_between_hints_resolve_to_none() {
assert_eq!(PartitionIndex::new(hints()).partition_at(10, 0, 5), None);
}
#[test]
fn y_range_is_respected_when_present() {
let idx = PartitionIndex::new(hints());
assert_eq!(idx.partition_at(5, 5, 15), Some("c"));
assert_eq!(idx.partition_at(5, 50, 15), None, "outside c's y_range");
}
#[test]
fn none_y_range_means_full_column() {
let idx = PartitionIndex::new(hints());
assert_eq!(idx.partition_at(5, -1000, 5), Some("a"));
assert_eq!(idx.partition_at(5, 1000, 5), Some("a"));
}
#[test]
fn id_index_is_stable_and_matches_sorted_id_order() {
let mut shuffled = hints();
shuffled.reverse();
let a = PartitionIndex::new(hints());
let b = PartitionIndex::new(shuffled);
assert_eq!(a.id_index_at(5, 0, 5), b.id_index_at(5, 0, 5));
assert_eq!(a.id_index_at(15, 0, 5), b.id_index_at(15, 0, 5));
}
#[test]
fn hints_hash_is_order_independent_but_content_sensitive() {
let mut reversed = hints();
reversed.reverse();
assert_eq!(
PartitionIndex::new(hints()).hints_hash(),
PartitionIndex::new(reversed).hints_hash(),
"hints are sorted at construction, so input order cannot reach the digest"
);
let narrower = vec![
PartitionHint { id: "a".into(), bbox_xz: (0, 8, 0, 9), y_range: None },
PartitionHint { id: "b".into(), bbox_xz: (11, 20, 0, 9), y_range: None },
PartitionHint { id: "c".into(), bbox_xz: (0, 9, 11, 20), y_range: Some((0, 10)) },
];
assert_ne!(
PartitionIndex::new(hints()).hints_hash(),
PartitionIndex::new(narrower).hints_hash(),
"hint geometry must be part of the digest"
);
let full = vec![PartitionHint {
id: "a".into(),
bbox_xz: (0, 9, 0, 9),
y_range: Some((i32::MIN, i32::MAX)),
}];
let unbounded =
vec![PartitionHint { id: "a".into(), bbox_xz: (0, 9, 0, 9), y_range: None }];
assert_ne!(
PartitionIndex::new(full).hints_hash(),
PartitionIndex::new(unbounded).hints_hash()
);
let ab_c = vec![
PartitionHint { id: "ab".into(), bbox_xz: (0, 0, 0, 0), y_range: None },
PartitionHint { id: "c".into(), bbox_xz: (0, 0, 0, 0), y_range: None },
];
let a_bc = vec![
PartitionHint { id: "a".into(), bbox_xz: (0, 0, 0, 0), y_range: None },
PartitionHint { id: "bc".into(), bbox_xz: (0, 0, 0, 0), y_range: None },
];
assert_ne!(PartitionIndex::new(ab_c).hints_hash(), PartitionIndex::new(a_bc).hints_hash());
assert_ne!(
PartitionIndex::new(hints()).hints_hash(),
PartitionIndex::new(hints()[..2].to_vec()).hints_hash()
);
}
#[test]
fn duplicate_ids_with_different_boxes_resolve_the_same_regardless_of_input_order() {
let dup_first = PartitionHint { id: "dup".into(), bbox_xz: (0, 4, 0, 4), y_range: None };
let dup_second = PartitionHint { id: "dup".into(), bbox_xz: (10, 14, 0, 4), y_range: None };
let forward = vec![dup_first.clone(), dup_second.clone()];
let mut reversed = forward.clone();
reversed.reverse();
let idx_forward = PartitionIndex::new(forward);
let idx_reversed = PartitionIndex::new(reversed);
assert_eq!(idx_forward.partition_at(2, 0, 2), idx_reversed.partition_at(2, 0, 2));
assert_eq!(idx_forward.id_index_at(2, 0, 2), idx_reversed.id_index_at(2, 0, 2));
assert_eq!(idx_forward.partition_at(12, 0, 2), idx_reversed.partition_at(12, 0, 2));
assert_eq!(idx_forward.id_index_at(12, 0, 2), idx_reversed.id_index_at(12, 0, 2));
assert_eq!(idx_forward.partition_at(2, 0, 2), Some("dup"));
assert_eq!(idx_forward.partition_at(12, 0, 2), Some("dup"));
}
}