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::provenance::StableBuildId;
use crate::world_segment::stitch::Build;
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:?}"),
}
}
}