use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::world_segment::classify::{classify, BlockClass};
use crate::world_segment::grid::OccupancyGrid;
use crate::world_segment::ids::{ClusterId, ContentId, TileId};
use crate::world_segment::partition::{PartitionIndex, PartitionPolicy};
use crate::world_segment::profile::WorldProfile;
use crate::world_segment::tile::VoxelTile;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct SegConfig {
pub cell_size: u32,
pub closing_radius: u32,
pub min_cluster_blocks: u64,
pub partition_policy: PartitionPolicy,
pub algorithm_version: u32,
pub partition_floor_share: Option<f32>,
#[serde(default)]
pub partition_dense_layer_coverage: Option<f32>,
#[serde(default)]
pub preserve_support_blocks: bool,
pub split_disconnected: Option<DisconnectedSplit>,
#[serde(default)]
pub drop_unpartitioned: bool,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct DisconnectedSplit {
pub min_component_blocks: u64,
pub min_component_share: f32,
pub min_gap_cells: u32,
}
impl Default for DisconnectedSplit {
fn default() -> Self {
DisconnectedSplit {
min_component_blocks: 4_096,
min_component_share: 0.40,
min_gap_cells: 2,
}
}
}
impl SegConfig {
pub fn config_hash(&self, profile: &WorldProfile, partitions: &PartitionIndex) -> ContentId {
let policy: u8 = match self.partition_policy {
PartitionPolicy::HardCut => 0,
PartitionPolicy::Prefer => 1,
PartitionPolicy::Off => 2,
};
let hints = match self.partition_policy {
PartitionPolicy::HardCut => partitions.hints_hash(),
PartitionPolicy::Prefer | PartitionPolicy::Off => {
ContentId::of(&[b"parthints.ignored"])
}
};
let floor_share: Option<[u8; 5]> = self.partition_floor_share.map(|s| {
let mut v = [1u8; 5];
v[1..].copy_from_slice(&s.to_le_bytes());
v
});
let split: Option<[u8; 17]> = self.split_disconnected.as_ref().map(|s| {
let mut v = [0u8; 17];
v[0] = 1;
v[1..9].copy_from_slice(&s.min_component_blocks.to_le_bytes());
v[9..13].copy_from_slice(&s.min_component_share.to_le_bytes());
v[13..17].copy_from_slice(&s.min_gap_cells.to_le_bytes());
v
});
let drop_unpartitioned = self.drop_unpartitioned.then_some([1u8]);
let dense_layer: Option<[u8; 5]> = self.partition_dense_layer_coverage.map(|coverage| {
let mut value = [2u8; 5];
value[1..].copy_from_slice(&coverage.to_le_bytes());
value
});
let preserve_support_blocks = self.preserve_support_blocks.then_some([3u8]);
let cell = self.cell_size.to_le_bytes();
let closing = self.closing_radius.to_le_bytes();
let min = self.min_cluster_blocks.to_le_bytes();
let algo = self.algorithm_version.to_le_bytes();
let policy_bytes = [policy];
let profile_hash = profile.profile_hash();
let mut parts: Vec<&[u8]> = vec![
b"segconfig.v2",
&cell,
&closing,
&min,
&policy_bytes,
&algo,
profile_hash.as_bytes(),
hints.as_bytes(),
];
if let Some(bytes) = floor_share.as_ref() {
parts.push(bytes);
}
if let Some(bytes) = split.as_ref() {
parts.push(bytes);
}
if let Some(bytes) = drop_unpartitioned.as_ref() {
parts.push(bytes);
}
if let Some(bytes) = dense_layer.as_ref() {
parts.push(bytes);
}
if let Some(bytes) = preserve_support_blocks.as_ref() {
parts.push(bytes);
}
ContentId::of(&parts)
}
}
impl Default for SegConfig {
fn default() -> Self {
SegConfig {
cell_size: 4,
closing_radius: 2,
min_cluster_blocks: 1,
partition_policy: PartitionPolicy::Off,
algorithm_version: 1,
partition_floor_share: None,
partition_dense_layer_coverage: None,
preserve_support_blocks: false,
split_disconnected: None,
drop_unpartitioned: false,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct Cluster {
pub id: ClusterId,
pub bbox: ((i32, i32, i32), (i32, i32, i32)),
pub block_count: u64,
pub cell_count: u64,
pub partition_id: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct MarginCell {
pub cell: (i32, i32, i32),
pub cluster: ClusterId,
pub partition: Option<String>,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct TileSegments {
pub tile_id: TileId,
pub clusters: Vec<Cluster>,
pub margin: Vec<MarginCell>,
}
pub fn segment_tile(
tile: &VoxelTile,
profile: &WorldProfile,
config: &SegConfig,
partitions: &PartitionIndex,
) -> TileSegments {
segment_tile_inner(tile, profile, config, partitions, false).0
}
pub fn segment_tile_membership(
tile: &VoxelTile,
profile: &WorldProfile,
config: &SegConfig,
partitions: &PartitionIndex,
) -> (TileSegments, BTreeMap<(i32, i32, i32), ClusterId>) {
segment_tile_inner(tile, profile, config, partitions, true)
}
fn segment_tile_inner(
tile: &VoxelTile,
profile: &WorldProfile,
config: &SegConfig,
partitions: &PartitionIndex,
want_membership: bool,
) -> (TileSegments, BTreeMap<(i32, i32, i32), ClusterId>) {
let bounds = tile.bounds();
let cell = config.cell_size.max(1);
let config_id = config.config_hash(profile, partitions);
let dims = (
span_cells(bounds.min.0, bounds.max.0, cell),
span_cells(bounds.min.1, bounds.max.1, cell),
span_cells(bounds.min.2, bounds.max.2, cell),
);
let origin = bounds.min;
let use_partitions =
config.partition_policy == PartitionPolicy::HardCut && !partitions.is_empty();
let (band_lo, band_hi) = profile.substrate_y_band;
let floor_materials: BTreeMap<u32, std::collections::BTreeSet<String>> =
match (use_partitions, config.partition_floor_share) {
(true, Some(share)) => partition_floor_materials(tile, profile, partitions, share),
_ => BTreeMap::new(),
};
let dense_floor_layers = match (use_partitions, config.partition_dense_layer_coverage) {
(true, Some(coverage)) => partition_dense_floor_layers(tile, profile, partitions, coverage),
_ => std::collections::BTreeSet::new(),
};
let mut artificial: Vec<((i32, i32, i32), Option<u32>)> = Vec::new();
let mut grids: BTreeMap<Option<u32>, OccupancyGrid> = BTreeMap::new();
for (pos, state) in tile.blocks() {
if classify(state, pos.1, profile) == BlockClass::Substrate {
continue;
}
let pidx = if use_partitions {
partitions.id_index_at(pos.0, pos.1, pos.2)
} else {
None
};
if use_partitions && config.drop_unpartitioned && pidx.is_none() {
continue;
}
if pidx.is_some_and(|partition| dense_floor_layers.contains(&(partition, pos.1))) {
continue;
}
if let Some(p) = pidx {
if pos.1 >= band_lo && pos.1 <= band_hi {
if let Some(names) = floor_materials.get(&p) {
if names.contains(state.get_name()) {
continue;
}
}
}
}
artificial.push((pos, pidx));
grids
.entry(pidx)
.or_insert_with(|| OccupancyGrid::new(origin, dims, cell))
.mark(pos.0, pos.1, pos.2);
}
if artificial.is_empty() {
return (
TileSegments {
tile_id: tile.id(),
clusters: Vec::new(),
margin: Vec::new(),
},
BTreeMap::new(),
);
}
let geometry = OccupancyGrid::new(origin, dims, cell);
let mut groups: BTreeMap<GroupKey, GroupAcc> = BTreeMap::new();
for (pidx, part_grid) in &grids {
let name = pidx.map(|i| partitions.id_of_index(i).to_string());
group_into(part_grid, config.closing_radius, *pidx, &name, &mut groups);
}
let (cluster_of_cell, mut partition_of_cluster) = assign_ids(config_id, tile.id(), groups);
let cluster_of_cell = match &config.split_disconnected {
Some(policy) => split_disconnected_clusters(
policy,
config_id,
tile.id(),
&artificial,
&geometry,
cluster_of_cell,
&mut partition_of_cluster,
),
None => cluster_of_cell,
};
let mut acc: BTreeMap<ClusterId, ClusterAcc> = BTreeMap::new();
let mut pos_to_cluster: BTreeMap<(i32, i32, i32), ClusterId> = BTreeMap::new();
for (pos, pidx) in artificial {
let cell_coord = geometry.cell_of(pos.0, pos.1, pos.2);
let Some(id) = cluster_of_cell.get(&(pidx, cell_coord)) else {
continue;
};
acc.entry(*id)
.or_insert_with(ClusterAcc::new)
.push(pos, cell_coord);
if want_membership {
pos_to_cluster.insert(pos, *id);
}
}
let mut clusters: Vec<Cluster> = Vec::new();
for (id, a) in &acc {
if a.block_count < config.min_cluster_blocks {
continue;
}
clusters.push(Cluster {
id: *id,
bbox: (a.min, a.max),
block_count: a.block_count,
cell_count: a.cells.len() as u64,
partition_id: partition_of_cluster.get(id).cloned().flatten(),
});
}
clusters.sort_by_key(|c| c.id);
let band = (config.closing_radius * 2 + 1) as i32;
let kept: std::collections::BTreeSet<ClusterId> = clusters.iter().map(|c| c.id).collect();
let mut margin: Vec<MarginCell> = Vec::new();
for (id, a) in &acc {
if !kept.contains(id) {
continue;
}
let partition = partition_of_cluster.get(id).cloned().flatten();
for cell_coord in &a.cells {
if in_margin(*cell_coord, dims, band) {
margin.push(MarginCell {
cell: *cell_coord,
cluster: *id,
partition: partition.clone(),
});
}
}
}
margin.sort_by(|a, b| {
a.cell
.cmp(&b.cell)
.then_with(|| a.cluster.cmp(&b.cluster))
.then_with(|| a.partition.cmp(&b.partition))
});
let membership = if want_membership {
pos_to_cluster
.into_iter()
.filter(|(_, id)| kept.contains(id))
.collect()
} else {
BTreeMap::new()
};
(
TileSegments {
tile_id: tile.id(),
clusters,
margin,
},
membership,
)
}
struct ClusterAcc {
min: (i32, i32, i32),
max: (i32, i32, i32),
block_count: u64,
cells: std::collections::BTreeSet<(i32, i32, i32)>,
}
impl ClusterAcc {
fn new() -> Self {
ClusterAcc {
min: (i32::MAX, i32::MAX, i32::MAX),
max: (i32::MIN, i32::MIN, i32::MIN),
block_count: 0,
cells: std::collections::BTreeSet::new(),
}
}
fn push(&mut self, pos: (i32, i32, i32), cell: (i32, i32, i32)) {
self.min = (
self.min.0.min(pos.0),
self.min.1.min(pos.1),
self.min.2.min(pos.2),
);
self.max = (
self.max.0.max(pos.0),
self.max.1.max(pos.1),
self.max.2.max(pos.2),
);
self.block_count += 1;
self.cells.insert(cell);
}
}
type GroupKey = (Option<u32>, u32);
struct GroupAcc {
cells: std::collections::BTreeSet<(i32, i32, i32)>,
partition: Option<String>,
}
fn group_into(
grid: &OccupancyGrid,
radius: u32,
pidx: Option<u32>,
partition: &Option<String>,
groups: &mut BTreeMap<GroupKey, GroupAcc>,
) {
let labels = grid.dilated(radius).label_components();
for cell in grid.occupied_cells() {
let Some(label) = labels.label_of(cell) else {
continue;
};
groups
.entry((pidx, label))
.or_insert_with(|| GroupAcc {
cells: std::collections::BTreeSet::new(),
partition: partition.clone(),
})
.cells
.insert(cell);
}
}
fn assign_ids(
config: ContentId,
tile: TileId,
groups: BTreeMap<GroupKey, GroupAcc>,
) -> (
BTreeMap<(Option<u32>, (i32, i32, i32)), ClusterId>,
BTreeMap<ClusterId, Option<String>>,
) {
let mut cluster_of_cell: BTreeMap<(Option<u32>, (i32, i32, i32)), ClusterId> = BTreeMap::new();
let mut partition_of_cluster: BTreeMap<ClusterId, Option<String>> = BTreeMap::new();
for ((pidx, _label), group) in groups {
let GroupAcc { cells, partition } = group;
let Some(anchor) = cells.iter().next().copied() else {
continue;
};
let id = ClusterId::new(config, tile, partition.as_deref(), anchor);
debug_assert!(
!partition_of_cluster.contains_key(&id),
"ClusterId collision on anchor {anchor:?} in partition {partition:?}: \
(partition, anchor) must be unique across groups"
);
partition_of_cluster.insert(id, partition);
for cell in cells {
cluster_of_cell.insert((pidx, cell), id);
}
}
(cluster_of_cell, partition_of_cluster)
}
fn split_disconnected_clusters(
policy: &DisconnectedSplit,
config: ContentId,
tile: TileId,
artificial: &[((i32, i32, i32), Option<u32>)],
geometry: &OccupancyGrid,
cluster_of_cell: BTreeMap<(Option<u32>, (i32, i32, i32)), ClusterId>,
partition_of_cluster: &mut BTreeMap<ClusterId, Option<String>>,
) -> BTreeMap<(Option<u32>, (i32, i32, i32)), ClusterId> {
let mut cell_blocks: BTreeMap<(Option<u32>, (i32, i32, i32)), u64> = BTreeMap::new();
for (pos, pidx) in artificial {
let cell = geometry.cell_of(pos.0, pos.1, pos.2);
*cell_blocks.entry((*pidx, cell)).or_insert(0) += 1;
}
let mut cells_of: BTreeMap<(Option<u32>, ClusterId), Vec<(i32, i32, i32)>> = BTreeMap::new();
for ((pidx, cell), id) in &cluster_of_cell {
cells_of.entry((*pidx, *id)).or_default().push(*cell);
}
let mut out: BTreeMap<(Option<u32>, (i32, i32, i32)), ClusterId> = BTreeMap::new();
for ((pidx, old_id), cells) in cells_of {
let comps = six_connected_components(&cells);
if comps.len() < 2 {
for c in &cells {
out.insert((pidx, *c), old_id);
}
continue;
}
let partition = partition_of_cluster.get(&old_id).cloned().flatten();
let comp_blocks: Vec<u64> = comps
.iter()
.map(|comp| {
comp.iter()
.map(|c| cell_blocks.get(&(pidx, *c)).copied().unwrap_or(0))
.sum()
})
.collect();
let total: u64 = comp_blocks.iter().sum();
let share_floor = (f64::from(policy.min_component_share) * total as f64).ceil() as u64;
let seeds: Vec<usize> = (0..comps.len())
.filter(|&i| {
comp_blocks[i] >= policy.min_component_blocks && comp_blocks[i] >= share_floor
})
.collect();
let split_ok = seeds.len() >= 2
&& seeds.iter().enumerate().all(|(a, &si)| {
seeds[a + 1..]
.iter()
.all(|&sj| min_cell_gap_at_least(&comps[si], &comps[sj], policy.min_gap_cells))
});
if !split_ok {
for c in &cells {
out.insert((pidx, *c), old_id);
}
continue;
}
let mut seeds_meta: Vec<(usize, ClusterId, (i32, i32, i32))> = Vec::new();
for &si in &seeds {
let anchor = *comps[si]
.iter()
.min()
.expect("a seed component is non-empty");
let id = ClusterId::new(config, tile, partition.as_deref(), anchor);
partition_of_cluster
.entry(id)
.or_insert_with(|| partition.clone());
for c in &comps[si] {
out.insert((pidx, *c), id);
}
seeds_meta.push((si, id, anchor));
}
let seed_set: std::collections::BTreeSet<usize> = seeds.iter().copied().collect();
for (ci, comp) in comps.iter().enumerate() {
if seed_set.contains(&ci) {
continue;
}
let cc = centroid(comp);
let mut best: Option<(i64, (i32, i32, i32), ClusterId)> = None;
for (si, id, anchor) in &seeds_meta {
let d = cheb(cc, centroid(&comps[*si]));
let cand = (d, *anchor, *id);
let take = match &best {
None => true,
Some(b) => cand.0 < b.0 || (cand.0 == b.0 && cand.1 < b.1),
};
if take {
best = Some(cand);
}
}
let id = best.expect("at least one seed exists").2;
for c in comp {
out.insert((pidx, *c), id);
}
}
}
out
}
fn six_connected_components(cells: &[(i32, i32, i32)]) -> Vec<Vec<(i32, i32, i32)>> {
let set: std::collections::BTreeSet<(i32, i32, i32)> = cells.iter().copied().collect();
let mut seen: std::collections::BTreeSet<(i32, i32, i32)> = std::collections::BTreeSet::new();
let mut comps: Vec<Vec<(i32, i32, i32)>> = Vec::new();
for &start in &set {
if seen.contains(&start) {
continue;
}
seen.insert(start);
let mut stack = vec![start];
let mut comp: Vec<(i32, i32, i32)> = Vec::new();
while let Some(c) = stack.pop() {
comp.push(c);
let nbrs = [
(c.0 - 1, c.1, c.2),
(c.0 + 1, c.1, c.2),
(c.0, c.1 - 1, c.2),
(c.0, c.1 + 1, c.2),
(c.0, c.1, c.2 - 1),
(c.0, c.1, c.2 + 1),
];
for nb in nbrs {
if set.contains(&nb) && seen.insert(nb) {
stack.push(nb);
}
}
}
comp.sort_unstable();
comps.push(comp);
}
comps
}
fn min_cell_gap_at_least(a: &[(i32, i32, i32)], b: &[(i32, i32, i32)], g: u32) -> bool {
if g == 0 {
return true;
}
let (scan, other) = if a.len() <= b.len() { (a, b) } else { (b, a) };
let other_set: std::collections::BTreeSet<(i32, i32, i32)> = other.iter().copied().collect();
let r = (g - 1) as i32;
for c in scan {
for dx in -r..=r {
for dy in -r..=r {
for dz in -r..=r {
if other_set.contains(&(c.0 + dx, c.1 + dy, c.2 + dz)) {
return false;
}
}
}
}
}
true
}
fn centroid(cells: &[(i32, i32, i32)]) -> (i64, i64, i64) {
let n = cells.len() as i64;
let (mut sx, mut sy, mut sz) = (0i64, 0i64, 0i64);
for c in cells {
sx += i64::from(c.0);
sy += i64::from(c.1);
sz += i64::from(c.2);
}
(sx / n, sy / n, sz / n)
}
fn cheb(a: (i64, i64, i64), b: (i64, i64, i64)) -> i64 {
(a.0 - b.0)
.abs()
.max((a.1 - b.1).abs())
.max((a.2 - b.2).abs())
}
fn partition_floor_materials(
tile: &VoxelTile,
profile: &WorldProfile,
partitions: &PartitionIndex,
share: f32,
) -> BTreeMap<u32, std::collections::BTreeSet<String>> {
let (lo, hi) = profile.substrate_y_band;
let mut counts: BTreeMap<(u32, String), u64> = BTreeMap::new();
let mut totals: BTreeMap<u32, u64> = BTreeMap::new();
for (pos, state) in tile.blocks() {
if pos.1 < lo || pos.1 > hi {
continue;
}
let Some(p) = partitions.id_index_at(pos.0, pos.1, pos.2) else {
continue;
};
*counts.entry((p, state.get_name().to_string())).or_insert(0) += 1;
*totals.entry(p).or_insert(0) += 1;
}
let share = share as f64;
let mut floors: BTreeMap<u32, std::collections::BTreeSet<String>> = BTreeMap::new();
for ((p, name), count) in counts {
let total = totals[&p];
if (count as f64) / (total as f64) >= share {
floors.entry(p).or_default().insert(name);
}
}
floors
}
fn partition_dense_floor_layers(
tile: &VoxelTile,
profile: &WorldProfile,
partitions: &PartitionIndex,
coverage: f32,
) -> std::collections::BTreeSet<(u32, i32)> {
let (lo, hi) = profile.substrate_y_band;
let mut occupied: BTreeMap<(u32, i32), u64> = BTreeMap::new();
for (pos, _state) in tile.blocks() {
if pos.1 < lo || pos.1 > hi {
continue;
}
if let Some(partition) = partitions.id_index_at(pos.0, pos.1, pos.2) {
*occupied.entry((partition, pos.1)).or_default() += 1;
}
}
let bounds = tile.bounds();
let coverage = coverage.clamp(0.0, 1.0) as f64;
occupied
.into_iter()
.filter_map(|((partition, y), count)| {
let hint = partitions.hint_of_index(partition);
if hint.y_range.is_some_and(|(y0, y1)| y < y0 || y > y1) {
return None;
}
let (x0, x1, z0, z1) = hint.bbox_xz;
let x0 = x0.max(bounds.min.0);
let x1 = x1.min(bounds.max.0);
let z0 = z0.max(bounds.min.2);
let z1 = z1.min(bounds.max.2);
if x0 > x1 || z0 > z1 {
return None;
}
let area = (i64::from(x1) - i64::from(x0) + 1) * (i64::from(z1) - i64::from(z0) + 1);
((count as f64) / (area as f64) >= coverage).then_some((partition, y))
})
.collect()
}
fn span_cells(lo: i32, hi: i32, cell: u32) -> usize {
debug_assert!(hi >= lo, "span_cells: hi ({hi}) must be >= lo ({lo})");
debug_assert!(cell > 0, "span_cells: cell size must be positive");
let span = i64::from(hi) - i64::from(lo);
if span < 0 {
return 0;
}
((span / i64::from(cell.max(1))) + 1) as usize
}
fn in_margin(cell: (i32, i32, i32), dims: (usize, usize, usize), band: i32) -> bool {
cell.0 < band
|| cell.2 < band
|| cell.0 >= dims.0 as i32 - band
|| cell.2 >= dims.2 as i32 - band
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block_state::BlockState;
use crate::world_segment::ids::TileId;
use crate::world_segment::partition::{PartitionHint, PartitionIndex};
use crate::world_segment::tile::TileBounds;
fn profile() -> WorldProfile {
WorldProfile::new(
["minecraft:stone", "minecraft:bedrock"]
.iter()
.map(|s| s.to_string())
.collect(),
(-64, -50),
)
}
fn cfg() -> SegConfig {
SegConfig {
cell_size: 4,
closing_radius: 2,
min_cluster_blocks: 1,
..SegConfig::default()
}
}
fn bounds() -> TileBounds {
TileBounds {
min: (0, -64, 0),
max: (127, 63, 127),
}
}
fn tile(blocks: Vec<((i32, i32, i32), &str)>) -> VoxelTile {
VoxelTile::from_blocks(
TileId { x: 0, z: 0 },
bounds(),
blocks.into_iter().map(|(p, n)| (p, BlockState::new(n))),
)
}
fn no_hints() -> PartitionIndex {
PartitionIndex::new(vec![])
}
#[test]
fn substrate_is_dropped_entirely() {
let t = tile(vec![
((10, -60, 10), "minecraft:stone"),
((11, -60, 10), "minecraft:stone"),
]);
let segs = segment_tile(&t, &profile(), &cfg(), &no_hints());
assert!(
segs.clusters.is_empty(),
"a tile of pure substrate yields no clusters"
);
}
#[test]
fn a_build_standing_on_substrate_does_not_merge_with_it() {
let mut blocks = vec![];
for x in 0..40 {
for z in 0..40 {
blocks.push(((x, -60, z), "minecraft:stone")); }
}
blocks.push(((10, -59, 10), "minecraft:redstone_wire")); blocks.push(((30, -59, 30), "minecraft:redstone_wire")); let segs = segment_tile(&tile(blocks), &profile(), &cfg(), &no_hints());
assert_eq!(segs.clusters.len(), 2, "two builds, ground removed");
}
#[test]
fn a_detached_floating_component_does_not_split() {
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1, "an 8-block gap must bridge");
}
#[test]
fn structures_beyond_the_closing_distance_stay_separate() {
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((90, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 2);
}
#[test]
fn cluster_bbox_and_block_count_describe_the_original_blocks() {
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((12, 14, 11), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1);
let c = &segs.clusters[0];
assert_eq!(c.block_count, 2);
assert_eq!(c.bbox, ((10, 10, 10), (12, 14, 11)));
}
#[test]
fn min_cluster_blocks_filters_small_clusters() {
let config = SegConfig {
min_cluster_blocks: 2,
..cfg()
};
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((90, 10, 10), "minecraft:redstone_wire"),
((91, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&no_hints(),
);
assert_eq!(
segs.clusters.len(),
1,
"the single-block cluster is dropped"
);
assert_eq!(segs.clusters[0].block_count, 2);
}
#[test]
fn hard_cut_prevents_merging_across_a_partition_boundary() {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "left".into(),
bbox_xz: (0, 13, 0, 127),
y_range: None,
},
PartitionHint {
id: "right".into(),
bbox_xz: (14, 127, 0, 127),
y_range: None,
},
]);
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let segs = segment_tile(
&tile(vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(segs.clusters.len(), 2, "the boundary splits them");
let mut got: Vec<_> = segs
.clusters
.iter()
.map(|c| c.partition_id.clone().unwrap())
.collect();
got.sort();
assert_eq!(got, vec!["left".to_string(), "right".to_string()]);
}
#[test]
fn hard_cut_clusters_near_a_grid_min_face_keep_distinct_identities() {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "near".into(),
bbox_xz: (0, 127, 0, 3),
y_range: None,
},
PartitionHint {
id: "far".into(),
bbox_xz: (0, 127, 4, 127),
y_range: None,
},
]);
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let segs = segment_tile(
&tile(vec![
((0, 8, 0), "minecraft:redstone_wire"),
((0, 8, 4), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(
segs.clusters.len(),
2,
"a clipped dilation anchor must not fuse two partitions' clusters"
);
assert_ne!(
segs.clusters[0].id, segs.clusters[1].id,
"ids must stay distinct"
);
let mut got: Vec<_> = segs
.clusters
.iter()
.map(|c| c.partition_id.clone().unwrap())
.collect();
got.sort();
assert_eq!(got, vec!["far".to_string(), "near".to_string()]);
}
#[test]
fn hard_cut_holds_on_a_boundary_that_is_not_cell_aligned() {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "L".into(),
bbox_xz: (0, 61, 0, 127),
y_range: None,
},
PartitionHint {
id: "R".into(),
bbox_xz: (62, 127, 0, 127),
y_range: None,
},
]);
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let segs = segment_tile(
&tile(vec![
((60, 10, 40), "minecraft:redstone_wire"),
((63, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
for c in &segs.clusters {
assert!(
!(c.bbox.0 .0 <= 61 && c.bbox.1 .0 >= 62),
"cluster {} spans the boundary at x=62: bbox {:?}",
c.id,
c.bbox
);
}
assert_eq!(
segs.clusters.len(),
2,
"the boundary splits the straddling cell"
);
let mut got: Vec<_> = segs
.clusters
.iter()
.map(|c| (c.partition_id.clone().unwrap(), c.bbox))
.collect();
got.sort();
assert_eq!(
got,
vec![
("L".to_string(), ((60, 10, 40), (60, 10, 40))),
("R".to_string(), ((63, 10, 40), (63, 10, 40))),
],
"each block is attributed to the partition it actually sits in"
);
}
#[test]
fn a_cell_shared_by_two_partitions_yields_two_distinct_ids() {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "L".into(),
bbox_xz: (0, 61, 0, 127),
y_range: None,
},
PartitionHint {
id: "R".into(),
bbox_xz: (62, 127, 0, 127),
y_range: None,
},
]);
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let segs = segment_tile(
&tile(vec![
((60, 10, 40), "minecraft:redstone_wire"),
((63, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(segs.clusters.len(), 2);
assert_ne!(
segs.clusters[0].id, segs.clusters[1].id,
"a shared anchor cell in two partitions must still give two ids"
);
}
#[test]
fn margin_cells_at_a_shared_cell_carry_their_own_partitions() {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "L".into(),
bbox_xz: (0, 1, 0, 127),
y_range: None,
},
PartitionHint {
id: "R".into(),
bbox_xz: (2, 127, 0, 127),
y_range: None,
},
]);
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let segs = segment_tile(
&tile(vec![
((1, 10, 40), "minecraft:redstone_wire"),
((2, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&hints,
);
assert_eq!(
segs.clusters.len(),
2,
"the boundary splits the straddling cell"
);
let shared: Vec<&MarginCell> = segs
.margin
.iter()
.filter(|m| m.cell == (0, 18, 10))
.collect();
assert_eq!(
shared.len(),
2,
"the shared cell must appear once per partition, got {:?}",
segs.margin
);
assert_ne!(
shared[0].cluster, shared[1].cluster,
"two distinct clusters occupy the shared cell"
);
assert_ne!(
shared[0].partition, shared[1].partition,
"margin entries at a shared cell must be distinguishable by partition; \
without this a stitcher keying on `cell` unions across the boundary"
);
for m in &shared {
let owner = segs
.clusters
.iter()
.find(|c| c.id == m.cluster)
.expect("margin entry must reference a surviving cluster");
assert_eq!(&m.partition, &owner.partition_id);
}
let mut names: Vec<Option<String>> = shared.iter().map(|m| m.partition.clone()).collect();
names.sort();
assert_eq!(names, vec![Some("L".to_string()), Some("R".to_string())]);
}
#[test]
fn policy_off_reproduces_the_unpartitioned_result() {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "left".into(),
bbox_xz: (0, 13, 0, 127),
y_range: None,
},
PartitionHint {
id: "right".into(),
bbox_xz: (14, 127, 0, 127),
y_range: None,
},
]);
let config = SegConfig {
partition_policy: PartitionPolicy::Off,
..cfg()
};
let blocks = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
let with = segment_tile(&tile(blocks.clone()), &profile(), &config, &hints);
let without = segment_tile(&tile(blocks), &profile(), &cfg(), &no_hints());
assert_eq!(with, without);
}
#[allow(clippy::type_complexity)]
fn shape_of(
segs: &TileSegments,
) -> (
Vec<(((i32, i32, i32), (i32, i32, i32)), u64, u64, Option<String>)>,
Vec<(i32, i32, i32)>,
) {
let mut clusters: Vec<_> = segs
.clusters
.iter()
.map(|c| (c.bbox, c.block_count, c.cell_count, c.partition_id.clone()))
.collect();
clusters.sort();
let mut margin: Vec<_> = segs.margin.iter().map(|m| m.cell).collect();
margin.sort();
(clusters, margin)
}
#[test]
fn prefer_policy_is_currently_inert_and_behaves_like_off() {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "left".into(),
bbox_xz: (0, 13, 0, 127),
y_range: None,
},
PartitionHint {
id: "right".into(),
bbox_xz: (14, 127, 0, 127),
y_range: None,
},
]);
let blocks = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
let prefer = SegConfig {
partition_policy: PartitionPolicy::Prefer,
..cfg()
};
let off = SegConfig {
partition_policy: PartitionPolicy::Off,
..cfg()
};
let prefer_segs = segment_tile(&tile(blocks.clone()), &profile(), &prefer, &hints);
let off_segs = segment_tile(&tile(blocks.clone()), &profile(), &off, &hints);
assert_eq!(prefer_segs.tile_id, off_segs.tile_id);
assert_eq!(
shape_of(&prefer_segs),
shape_of(&off_segs),
"Prefer must segment exactly as Off does"
);
assert_ne!(
prefer_segs.clusters[0].id, off_segs.clusters[0].id,
"different configs must still mint different ids"
);
assert_eq!(
prefer_segs.clusters.len(),
1,
"the boundary is not enforced under Prefer"
);
assert!(
prefer_segs
.clusters
.iter()
.all(|c| c.partition_id.is_none()),
"nothing is recorded: every partition_id is None"
);
let hard = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let hard_segs = segment_tile(&tile(blocks), &profile(), &hard, &hints);
assert_eq!(hard_segs.clusters.len(), 2);
}
#[test]
fn a_cluster_dropped_by_min_cluster_blocks_leaves_no_margin_entry() {
let config = SegConfig {
min_cluster_blocks: 2,
..cfg()
};
let segs = segment_tile(
&tile(vec![
((2, 10, 2), "minecraft:redstone_wire"),
((64, 10, 64), "minecraft:redstone_wire"),
((65, 10, 64), "minecraft:redstone_wire"),
]),
&profile(),
&config,
&no_hints(),
);
assert_eq!(
segs.clusters.len(),
1,
"the near-face single-block cluster is dropped"
);
assert_eq!(segs.clusters[0].block_count, 2);
let kept: std::collections::BTreeSet<ClusterId> =
segs.clusters.iter().map(|c| c.id).collect();
assert!(
segs.margin.iter().all(|m| kept.contains(&m.cluster)),
"every margin entry must reference a surviving cluster"
);
assert!(
segs.margin.is_empty(),
"the dropped cluster was the only one in the band, so margin is empty"
);
}
fn margin_cells(block: (i32, i32, i32)) -> Vec<(i32, i32, i32)> {
let segs = segment_tile(
&tile(vec![(block, "minecraft:redstone_wire")]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(
segs.clusters.len(),
1,
"one block must give exactly one cluster"
);
segs.margin.iter().map(|m| m.cell).collect()
}
#[test]
fn two_cells_exactly_2r_plus_1_apart_are_one_cluster() {
let segs = segment_tile(
&tile(vec![
((40, 10, 40), "minecraft:redstone_wire"),
((60, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(
segs.clusters.len(),
1,
"cell distance 2R+1 must merge: the dilated cubes are face-adjacent"
);
}
#[test]
fn two_cells_2r_plus_2_apart_are_two_clusters() {
let segs = segment_tile(
&tile(vec![
((40, 10, 40), "minecraft:redstone_wire"),
((64, 10, 40), "minecraft:redstone_wire"),
]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(
segs.clusters.len(),
2,
"cell distance 2R+2 leaves a one-cell gap"
);
}
#[test]
fn margin_band_covers_cell_depths_0_through_2r() {
assert_eq!(margin_cells((2, 10, 2)), vec![(0, 18, 0)]);
assert_eq!(margin_cells((13, 10, 13)), vec![(3, 18, 3)]);
assert_eq!(
margin_cells((17, 10, 17)),
vec![(4, 18, 4)],
"depth 2R must be IN band; this fails for any band width <= 4"
);
assert!(
margin_cells((21, 10, 21)).is_empty(),
"depth 2R+1 must be outside the band; this fails for any width >= 6"
);
assert_eq!(margin_cells((108, 10, 108)), vec![(27, 18, 27)]);
assert!(
margin_cells((107, 10, 107)).is_empty(),
"cell 26 is interior"
);
}
fn margin_cells_of(b: TileBounds, block: (i32, i32, i32)) -> Vec<(i32, i32, i32)> {
let t = VoxelTile::from_blocks(
TileId { x: 0, z: 0 },
b,
std::iter::once((block, BlockState::new("minecraft:redstone_wire"))),
);
let segs = segment_tile(&t, &profile(), &cfg(), &no_hints());
assert_eq!(
segs.clusters.len(),
1,
"one block must give exactly one cluster"
);
segs.margin.iter().map(|m| m.cell).collect()
}
#[test]
fn margin_band_measures_each_axis_against_its_own_extent() {
let b = TileBounds {
min: (0, -64, 0),
max: (127, 63, 255),
};
assert_eq!(margin_cells_of(b, (13, 10, 64)), vec![(3, 18, 16)]);
assert_eq!(margin_cells_of(b, (64, 10, 13)), vec![(16, 18, 3)]);
assert_eq!(
margin_cells_of(b, (108, 10, 64)),
vec![(27, 18, 16)],
"the far-X band starts at dims.0 - band, not dims.2 - band"
);
assert_eq!(
margin_cells_of(b, (64, 10, 236)),
vec![(16, 18, 59)],
"the far-Z band starts at dims.2 - band"
);
assert!(
margin_cells_of(b, (64, 10, 108)).is_empty(),
"cell z = 27 is interior on a 64-cell Z axis"
);
assert!(
margin_cells_of(b, (64, 10, 232)).is_empty(),
"cell z = 58 is interior"
);
}
#[test]
fn duplicate_positions_do_not_make_the_result_depend_on_input_order() {
let forward = vec![
((10, -60, 10), "minecraft:stone"),
((10, -60, 10), "minecraft:redstone_wire"),
];
let mut reverse = forward.clone();
reverse.reverse();
let a = segment_tile(&tile(forward), &profile(), &cfg(), &no_hints());
let b = segment_tile(&tile(reverse), &profile(), &cfg(), &no_hints());
assert_eq!(
a, b,
"a duplicated position must not let input order reach the output"
);
assert_eq!(
a.clusters.len(),
1,
"redstone_wire wins the position, so one cluster"
);
}
fn ids(config: &SegConfig, profile: &WorldProfile) -> Vec<ClusterId> {
let segs = segment_tile(
&tile(vec![
((40, 10, 40), "minecraft:redstone_wire"),
((41, 10, 40), "minecraft:repeater"),
]),
profile,
config,
&no_hints(),
);
assert_eq!(
segs.clusters.len(),
1,
"the probe input must be a single cluster"
);
segs.clusters.iter().map(|c| c.id).collect()
}
#[test]
fn identical_config_and_profile_give_identical_cluster_ids() {
assert_eq!(ids(&cfg(), &profile()), ids(&cfg(), &profile()));
}
#[test]
fn cluster_ids_change_when_only_a_non_geometric_config_field_changes() {
let v2 = SegConfig {
algorithm_version: 2,
..cfg()
};
assert_ne!(
ids(&cfg(), &profile()),
ids(&v2, &profile()),
"a config change with no geometric effect must still change ids"
);
}
#[test]
fn cluster_ids_change_when_only_the_hint_geometry_changes() {
let wide = PartitionIndex::new(vec![
PartitionHint {
id: "L".into(),
bbox_xz: (0, 61, 0, 127),
y_range: None,
},
PartitionHint {
id: "R".into(),
bbox_xz: (62, 127, 0, 127),
y_range: None,
},
]);
let narrow = PartitionIndex::new(vec![
PartitionHint {
id: "L".into(),
bbox_xz: (0, 40, 0, 127),
y_range: None,
},
PartitionHint {
id: "R".into(),
bbox_xz: (41, 127, 0, 127),
y_range: None,
},
]);
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let blocks = vec![((10, 10, 40), "minecraft:redstone_wire")];
let a = segment_tile(&tile(blocks.clone()), &profile(), &config, &wide);
let b = segment_tile(&tile(blocks), &profile(), &config, &narrow);
assert_eq!(a.clusters.len(), 1);
assert_eq!(b.clusters.len(), 1);
assert_eq!(a.clusters[0].partition_id.as_deref(), Some("L"));
assert_eq!(b.clusters[0].partition_id.as_deref(), Some("L"));
assert_eq!(a.clusters[0].bbox, b.clusters[0].bbox);
assert_ne!(
a.clusters[0].id, b.clusters[0].id,
"hints sharing ids but differing in extent describe different \
segmentations and must not mint the same ClusterId"
);
}
#[test]
fn cluster_ids_change_when_only_the_profile_changes() {
let other = WorldProfile::new(
["minecraft:stone"].iter().map(|s| s.to_string()).collect(),
(-64, -50),
);
assert_ne!(ids(&cfg(), &profile()), ids(&cfg(), &other));
}
#[test]
fn interior_clusters_emit_no_margin_cells() {
let segs = segment_tile(
&tile(vec![((64, 10, 64), "minecraft:redstone_wire")]),
&profile(),
&cfg(),
&no_hints(),
);
assert_eq!(segs.clusters.len(), 1);
assert!(
segs.margin.is_empty(),
"a centre cluster is nowhere near a face"
);
}
#[test]
fn membership_maps_every_emitted_block_to_its_cluster() {
let profile = WorldProfile::new(
["minecraft:stone"].iter().map(|s| s.to_string()).collect(),
(-64, -50),
);
let cfg = SegConfig {
cell_size: 4,
closing_radius: 2,
min_cluster_blocks: 1,
..SegConfig::default()
};
let mut blocks = vec![];
for x in 0..40 {
for z in 0..40 {
blocks.push(((x, -60, z), BlockState::new("minecraft:stone")));
}
}
blocks.push(((10, -59, 10), BlockState::new("minecraft:redstone_wire")));
let t = VoxelTile::from_blocks(
TileId { x: 0, z: 0 },
TileBounds {
min: (0, -64, 0),
max: (127, 63, 127),
},
blocks.into_iter(),
);
let (segs, membership) =
segment_tile_membership(&t, &profile, &cfg, &PartitionIndex::new(vec![]));
assert_eq!(segs.clusters.len(), 1);
let cluster = segs.clusters[0].id;
assert_eq!(
membership.get(&(10, -59, 10)),
Some(&cluster),
"build block maps to its cluster"
);
assert_eq!(
membership.get(&(0, -60, 0)),
None,
"substrate is not in the membership map"
);
for (_pos, cid) in &membership {
assert!(segs.clusters.iter().any(|c| c.id == *cid));
}
}
#[test]
fn segment_tile_and_membership_agree_on_segments() {
let profile = WorldProfile::new(
["minecraft:stone"].iter().map(|s| s.to_string()).collect(),
(-64, -50),
);
let cfg = SegConfig::default();
let t = VoxelTile::from_blocks(
TileId { x: 0, z: 0 },
TileBounds {
min: (0, -64, 0),
max: (127, 63, 127),
},
vec![((10, 10, 10), BlockState::new("minecraft:redstone_wire"))].into_iter(),
);
let a = segment_tile(&t, &profile, &cfg, &PartitionIndex::new(vec![]));
let (b, _) = segment_tile_membership(&t, &profile, &cfg, &PartitionIndex::new(vec![]));
assert_eq!(
a, b,
"membership variant must return identical TileSegments"
);
}
fn floor_profile() -> WorldProfile {
WorldProfile::new(
["minecraft:stone"].iter().map(|s| s.to_string()).collect(),
(-64, -57),
)
}
fn two_plots() -> (PartitionIndex, Vec<((i32, i32, i32), &'static str)>) {
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "L".into(),
bbox_xz: (0, 63, 0, 127),
y_range: None,
},
PartitionHint {
id: "R".into(),
bbox_xz: (64, 127, 0, 127),
y_range: None,
},
]);
let mut blocks: Vec<((i32, i32, i32), &'static str)> = Vec::new();
for x in 4..=35 {
for z in 4..=15 {
blocks.push(((x, -60, z), "minecraft:blue_wool"));
}
}
for x in 68..=99 {
for z in 4..=15 {
blocks.push(((x, -60, z), "minecraft:white_concrete"));
}
}
blocks.push(((8, -59, 10), "minecraft:redstone_wire"));
blocks.push(((32, -59, 10), "minecraft:redstone_wire"));
blocks.push(((72, -59, 10), "minecraft:redstone_wire"));
blocks.push(((96, -59, 10), "minecraft:redstone_wire"));
(hints, blocks)
}
#[test]
fn partition_floor_is_subtracted_per_partition() {
let (hints, blocks) = two_plots();
let on = SegConfig {
partition_policy: PartitionPolicy::HardCut,
partition_floor_share: Some(0.3),
..cfg()
};
let segs = segment_tile(&tile(blocks.clone()), &floor_profile(), &on, &hints);
assert_eq!(
segs.clusters.len(),
4,
"floor subtracted: two separate builds per plot, four total; got {:?}",
segs.clusters
.iter()
.map(|c| (c.bbox, c.block_count))
.collect::<Vec<_>>()
);
for c in &segs.clusters {
let x_extent = c.bbox.1 .0 - c.bbox.0 .0;
assert!(
x_extent < 15,
"each build is small; a plot-sheet bbox would span the whole plot. bbox {:?}",
c.bbox
);
assert_eq!(
c.block_count, 1,
"only the one-block build survives, not the 384-block floor sheet"
);
}
let off = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
assert!(off.partition_floor_share.is_none());
let merged = segment_tile(&tile(blocks), &floor_profile(), &off, &hints);
assert_eq!(
merged.clusters.len(),
2,
"no floor subtraction: one whole-plot mega-cluster per partition"
);
for c in &merged.clusters {
assert!(
c.block_count > 300,
"the merged cluster swallows the ~384-block floor sheet; got {}",
c.block_count
);
}
}
#[test]
fn dense_layer_subtracts_a_patterned_multi_material_floor() {
let hints = PartitionIndex::new(vec![PartitionHint {
id: "plot".into(),
bbox_xz: (0, 31, 0, 31),
y_range: None,
}]);
let mut blocks = Vec::new();
for x in 0..32 {
for z in 0..32 {
let material = if (x + z) % 2 == 0 {
"minecraft:petrified_oak_slab"
} else {
"minecraft:sandstone_slab"
};
blocks.push(((x, -60, z), material));
}
}
blocks.push(((4, -59, 4), "minecraft:redstone_wire"));
blocks.push(((28, -59, 4), "minecraft:redstone_wire"));
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
partition_floor_share: None,
partition_dense_layer_coverage: Some(0.80),
..cfg()
};
let result = segment_tile(&tile(blocks), &floor_profile(), &config, &hints);
assert_eq!(result.clusters.len(), 2);
assert!(result
.clusters
.iter()
.all(|cluster| cluster.block_count == 1));
}
#[test]
fn floor_share_none_is_behavior_preserving() {
assert!(SegConfig::default().partition_floor_share.is_none());
let explicit_none = SegConfig {
partition_floor_share: None,
..cfg()
};
let plain = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
assert_eq!(
segment_tile(&tile(plain.clone()), &profile(), &cfg(), &no_hints()),
segment_tile(&tile(plain), &profile(), &explicit_none, &no_hints()),
);
let mut slab = vec![];
for x in 0..40 {
for z in 0..40 {
slab.push(((x, -60, z), "minecraft:stone"));
}
}
slab.push(((10, -59, 10), "minecraft:redstone_wire"));
slab.push(((30, -59, 30), "minecraft:redstone_wire"));
assert_eq!(
segment_tile(&tile(slab.clone()), &profile(), &cfg(), &no_hints()),
segment_tile(&tile(slab), &profile(), &explicit_none, &no_hints()),
);
let hints = PartitionIndex::new(vec![
PartitionHint {
id: "left".into(),
bbox_xz: (0, 13, 0, 127),
y_range: None,
},
PartitionHint {
id: "right".into(),
bbox_xz: (14, 127, 0, 127),
y_range: None,
},
]);
let hard = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let hard_none = SegConfig {
partition_floor_share: None,
..hard.clone()
};
let split = vec![
((10, 10, 10), "minecraft:redstone_wire"),
((18, 10, 10), "minecraft:redstone_wire"),
];
assert_eq!(
segment_tile(&tile(split.clone()), &profile(), &hard, &hints),
segment_tile(&tile(split), &profile(), &hard_none, &hints),
);
}
#[test]
fn config_hash_changes_with_floor_share() {
let parts = PartitionIndex::new(vec![PartitionHint {
id: "L".into(),
bbox_xz: (0, 63, 0, 127),
y_range: None,
}]);
let base = SegConfig {
partition_policy: PartitionPolicy::HardCut,
..cfg()
};
let with_floor = SegConfig {
partition_floor_share: Some(0.3),
..base.clone()
};
assert_ne!(
base.config_hash(&floor_profile(), &parts),
with_floor.config_hash(&floor_profile(), &parts),
"partition_floor_share must be folded into config_hash"
);
let probe = vec![((10, 10, 40), "minecraft:redstone_wire")];
let a = segment_tile(&tile(probe.clone()), &floor_profile(), &base, &parts);
let b = segment_tile(&tile(probe), &floor_profile(), &with_floor, &parts);
assert_eq!(a.clusters.len(), 1);
assert_eq!(b.clusters.len(), 1);
assert_eq!(a.clusters[0].bbox, b.clusters[0].bbox, "geometry identical");
assert_ne!(
a.clusters[0].id, b.clusters[0].id,
"different partition_floor_share must mint different ids"
);
}
fn cuboid(
x0: i32,
x1: i32,
y0: i32,
y1: i32,
z0: i32,
z1: i32,
name: &str,
) -> Vec<((i32, i32, i32), &str)> {
let mut v = Vec::new();
for x in x0..=x1 {
for y in y0..=y1 {
for z in z0..=z1 {
v.push(((x, y, z), name));
}
}
}
v
}
fn split_policy() -> DisconnectedSplit {
DisconnectedSplit {
min_component_blocks: 8,
min_component_share: 0.40,
min_gap_cells: 2,
}
}
fn two_disconnected_builds() -> Vec<((i32, i32, i32), &'static str)> {
let mut b = cuboid(8, 11, 0, 3, 8, 11, "minecraft:oak_planks");
b.extend(cuboid(8, 11, 0, 3, 20, 23, "minecraft:oak_planks"));
b
}
#[test]
fn closing_merges_two_disconnected_builds_by_default() {
let t = tile(two_disconnected_builds());
let segs = segment_tile(&t, &profile(), &cfg(), &no_hints());
assert_eq!(
segs.clusters.len(),
1,
"closing fuses two builds within 2R+1 cells into one cluster"
);
assert_eq!(
segs.clusters[0].block_count, 128,
"both builds land in the one cluster"
);
}
#[test]
fn split_disconnected_separates_two_substantial_builds() {
let t = tile(two_disconnected_builds());
let on = SegConfig {
split_disconnected: Some(split_policy()),
..cfg()
};
let segs = segment_tile(&t, &profile(), &on, &no_hints());
assert_eq!(
segs.clusters.len(),
2,
"two disconnected substantial builds must split"
);
let counts: Vec<u64> = segs.clusters.iter().map(|c| c.block_count).collect();
assert_eq!(counts, vec![64, 64], "each build keeps its own 64 blocks");
assert_ne!(
segs.clusters[0].id, segs.clusters[1].id,
"distinct clusters get distinct ids"
);
}
#[test]
fn split_does_not_fragment_a_single_build_with_a_minor_detached_part() {
let mut blocks = cuboid(8, 11, 0, 3, 8, 11, "minecraft:oak_planks");
blocks.extend(cuboid(8, 9, 0, 1, 20, 21, "minecraft:oak_planks"));
let t = tile(blocks);
let on = SegConfig {
split_disconnected: Some(split_policy()),
..cfg()
};
let segs = segment_tile(&t, &profile(), &on, &no_hints());
assert_eq!(
segs.clusters.len(),
1,
"a minor detached part must not split the build"
);
assert_eq!(
segs.clusters[0].block_count, 72,
"every block stays in the one cluster"
);
}
#[test]
fn split_respects_the_gap_tolerance() {
let mut blocks = cuboid(8, 11, 0, 3, 8, 11, "minecraft:oak_planks");
blocks.extend(cuboid(12, 15, 0, 3, 12, 15, "minecraft:oak_planks"));
let t = tile(blocks);
let on = SegConfig {
split_disconnected: Some(split_policy()),
..cfg()
};
let segs = segment_tile(&t, &profile(), &on, &no_hints());
assert_eq!(
segs.clusters.len(),
1,
"seeds closer than min_gap_cells stay merged"
);
}
#[test]
fn split_disconnected_is_folded_into_config_hash() {
let base = cfg();
let with_split = SegConfig {
split_disconnected: Some(split_policy()),
..cfg()
};
assert_eq!(
base.config_hash(&profile(), &no_hints()),
SegConfig {
split_disconnected: None,
..cfg()
}
.config_hash(&profile(), &no_hints()),
"None must be byte-for-byte inert in config_hash"
);
assert_ne!(
base.config_hash(&profile(), &no_hints()),
with_split.config_hash(&profile(), &no_hints()),
"split_disconnected must be folded into config_hash"
);
}
#[test]
fn drop_unpartitioned_keeps_only_declared_hard_cut_interiors() {
let hints = PartitionIndex::new(vec![PartitionHint {
id: "interior".into(),
bbox_xz: (0, 31, 0, 31),
y_range: None,
}]);
let input = tile(vec![
((8, 10, 8), "minecraft:redstone_wire"),
((80, 10, 80), "minecraft:redstone_wire"),
]);
let config = SegConfig {
partition_policy: PartitionPolicy::HardCut,
drop_unpartitioned: true,
..cfg()
};
let result = segment_tile(&input, &profile(), &config, &hints);
assert_eq!(result.clusters.len(), 1);
assert_eq!(result.clusters[0].block_count, 1);
assert_eq!(result.clusters[0].partition_id.as_deref(), Some("interior"));
}
#[test]
fn drop_unpartitioned_is_folded_into_config_hash() {
let base = cfg();
let enabled = SegConfig {
drop_unpartitioned: true,
..cfg()
};
assert_ne!(
base.config_hash(&profile(), &no_hints()),
enabled.config_hash(&profile(), &no_hints())
);
}
#[test]
fn dense_layer_coverage_is_folded_into_config_hash() {
let base = cfg();
let enabled = SegConfig {
partition_dense_layer_coverage: Some(0.80),
..cfg()
};
assert_ne!(
base.config_hash(&profile(), &no_hints()),
enabled.config_hash(&profile(), &no_hints())
);
}
#[test]
fn preserve_support_blocks_is_folded_into_config_hash() {
let base = cfg();
let enabled = SegConfig {
preserve_support_blocks: true,
..cfg()
};
assert_ne!(
base.config_hash(&profile(), &no_hints()),
enabled.config_hash(&profile(), &no_hints())
);
}
}