use std::collections::BTreeMap;
use crate::world_segment::ids::ClusterId;
use crate::world_segment::provenance::StableBuildId;
use crate::world_segment::stitch::Build;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct PriorBuild {
pub stable_id: StableBuildId,
pub bbox: ((i32, i32, i32), (i32, i32, i32)),
pub block_count: u64,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Outcome {
New,
Same(StableBuildId),
Split { inherits: StableBuildId },
Merge { from: Vec<StableBuildId> },
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SnapshotMatch {
pub build_id: ClusterId,
pub stable_id: StableBuildId,
pub outcome: Outcome,
}
pub fn bbox_iou(a: ((i32, i32, i32), (i32, i32, i32)), b: ((i32, i32, i32), (i32, i32, i32))) -> f32 {
fn axis_len(min: i32, max: i32) -> i64 {
(max - min + 1) as i64
}
fn axis_inter(amin: i32, amax: i32, bmin: i32, bmax: i32) -> i64 {
let lo = amin.max(bmin);
let hi = amax.min(bmax);
(hi - lo + 1).max(0) as i64
}
let ((amin_x, amin_y, amin_z), (amax_x, amax_y, amax_z)) = a;
let ((bmin_x, bmin_y, bmin_z), (bmax_x, bmax_y, bmax_z)) = b;
let ix = axis_inter(amin_x, amax_x, bmin_x, bmax_x);
let iy = axis_inter(amin_y, amax_y, bmin_y, bmax_y);
let iz = axis_inter(amin_z, amax_z, bmin_z, bmax_z);
let inter = ix * iy * iz;
let vol_a = axis_len(amin_x, amax_x) * axis_len(amin_y, amax_y) * axis_len(amin_z, amax_z);
let vol_b = axis_len(bmin_x, bmax_x) * axis_len(bmin_y, bmax_y) * axis_len(bmin_z, bmax_z);
let union = vol_a + vol_b - inter;
if union <= 0 {
0.0
} else {
inter as f32 / union as f32
}
}
pub fn match_snapshots(
current: &[Build],
prior: &[PriorBuild],
source_id: &str,
iou_threshold: f32,
) -> Vec<SnapshotMatch> {
let mut current_matches: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
let mut prior_matches: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for (ci, c) in current.iter().enumerate() {
for (pi, p) in prior.iter().enumerate() {
if bbox_iou(c.bbox, p.bbox) >= iou_threshold {
current_matches.entry(ci).or_default().push(pi);
prior_matches.entry(pi).or_default().push(ci);
}
}
}
let mut split_survivor: BTreeMap<usize, usize> = BTreeMap::new();
for (&pi, cis) in &prior_matches {
if cis.len() > 1 {
let survivor = *cis
.iter()
.max_by(|&&a, &&b| {
current[a]
.block_count
.cmp(¤t[b].block_count)
.then(current[a].id.cmp(¤t[b].id))
})
.unwrap();
split_survivor.insert(pi, survivor);
}
}
let mut results = Vec::with_capacity(current.len());
for (ci, c) in current.iter().enumerate() {
let matched_priors = current_matches.get(&ci).cloned().unwrap_or_default();
let (stable_id, outcome) = if matched_priors.is_empty() {
(StableBuildId::seed(source_id, c.id), Outcome::New)
} else if matched_priors.len() > 1 {
let inherited_pi = *matched_priors
.iter()
.max_by(|&&a, &&b| {
prior[a]
.block_count
.cmp(&prior[b].block_count)
.then(prior[a].stable_id.cmp(&prior[b].stable_id))
})
.unwrap();
let mut from: Vec<StableBuildId> =
matched_priors.iter().map(|&pi| prior[pi].stable_id).collect();
from.sort();
(prior[inherited_pi].stable_id, Outcome::Merge { from })
} else {
let pi = matched_priors[0];
let is_split_of_this_prior = prior_matches.get(&pi).map_or(false, |cis| cis.len() > 1);
if is_split_of_this_prior {
if split_survivor.get(&pi) == Some(&ci) {
(prior[pi].stable_id, Outcome::Same(prior[pi].stable_id))
} else {
(
StableBuildId::seed(source_id, c.id),
Outcome::Split { inherits: prior[pi].stable_id },
)
}
} else {
(prior[pi].stable_id, Outcome::Same(prior[pi].stable_id))
}
};
results.push(SnapshotMatch { build_id: c.id, stable_id, outcome });
}
results.sort_by_key(|m| m.build_id);
results
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world_segment::ids::{ClusterId, ContentId, TileId};
use crate::world_segment::stitch::Build;
use crate::world_segment::provenance::StableBuildId;
fn build(tag: &[u8], bbox: ((i32,i32,i32),(i32,i32,i32)), n: u64) -> Build {
let id = ClusterId::new(ContentId::of(&[tag]), TileId{x:0,z:0}, None, (0,0,0));
Build { id, cluster_ids: vec![id], bbox, block_count: n, cell_count: n, partition_id: None }
}
#[test]
fn iou_of_identical_boxes_is_one() {
assert!((bbox_iou(((0,0,0),(9,9,9)), ((0,0,0),(9,9,9))) - 1.0).abs() < 1e-6);
}
#[test]
fn iou_of_disjoint_boxes_is_zero() {
assert_eq!(bbox_iou(((0,0,0),(1,1,1)), ((100,100,100),(101,101,101))), 0.0);
}
#[test]
fn unmatched_current_is_new() {
let cur = vec![build(b"a", ((0,0,0),(9,9,9)), 100)];
let out = match_snapshots(&cur, &[], "w", 0.5);
assert!(matches!(out[0].outcome, Outcome::New));
}
#[test]
fn overlapping_current_inherits_prior_stable_id() {
let prior = vec![PriorBuild { stable_id: StableBuildId::seed("w", build(b"old", ((0,0,0),(9,9,9)), 90).id),
bbox: ((0,0,0),(9,9,9)), block_count: 90 }];
let cur = vec![build(b"a", ((0,0,0),(9,9,9)), 100)]; let out = match_snapshots(&cur, &prior, "w", 0.5);
assert_eq!(out[0].stable_id, prior[0].stable_id);
assert!(matches!(out[0].outcome, Outcome::Same(_)));
}
#[test]
fn split_gives_the_largest_current_the_inherited_id() {
let old = build(b"old", ((0,0,0),(9,0,9)), 200);
let prior_id = StableBuildId::seed("w", old.id);
let prior = vec![PriorBuild { stable_id: prior_id, bbox: old.bbox, block_count: 200 }];
let big = build(b"big", ((0,0,0),(9,0,9)), 150);
let small = build(b"small", ((0,0,0),(4,0,4)), 50);
let cur = vec![big.clone(), small.clone()];
let out = match_snapshots(&cur, &prior, "w", 0.1);
assert_eq!(out.len(), 2);
let big_match = out.iter().find(|m| m.build_id == big.id).unwrap();
let small_match = out.iter().find(|m| m.build_id == small.id).unwrap();
assert_eq!(big_match.stable_id, prior_id, "largest current inherits the prior id");
assert!(matches!(big_match.outcome, Outcome::Same(sid) if sid == prior_id));
assert_ne!(small_match.stable_id, prior_id, "split-off current gets a fresh id");
assert!(matches!(&small_match.outcome, Outcome::Split { inherits } if *inherits == prior_id));
assert!(out[0].build_id <= out[1].build_id);
}
#[test]
fn merge_records_all_priors() {
let old_a = build(b"old_a", ((0,0,0),(4,0,9)), 40);
let old_b = build(b"old_b", ((5,0,0),(9,0,9)), 90);
let stable_a = StableBuildId::seed("w", old_a.id);
let stable_b = StableBuildId::seed("w", old_b.id);
let prior = vec![
PriorBuild { stable_id: stable_a, bbox: old_a.bbox, block_count: 40 },
PriorBuild { stable_id: stable_b, bbox: old_b.bbox, block_count: 90 },
];
let merged = build(b"merged", ((0,0,0),(9,0,9)), 130);
let cur = vec![merged.clone()];
let out = match_snapshots(&cur, &prior, "w", 0.1);
assert_eq!(out.len(), 1);
assert_eq!(out[0].build_id, merged.id);
assert_eq!(out[0].stable_id, stable_b);
let mut expected_from = vec![stable_a, stable_b];
expected_from.sort();
match &out[0].outcome {
Outcome::Merge { from } => assert_eq!(from, &expected_from),
other => panic!("expected Merge, got {other:?}"),
}
}
}