use crate::block_position::BlockPosition;
use crate::bounding_box::BoundingBox;
use crate::selection::connectivity::Connectivity;
use crate::selection::mask::Mask;
use crate::selection::visited::VisitedSet;
use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, Default)]
pub struct Limits {
pub max_blocks: Option<usize>,
pub max_extent: Option<i32>,
}
impl Limits {
pub fn unbounded() -> Self {
Self::default()
}
pub fn with_max_blocks(mut self, n: usize) -> Self {
self.max_blocks = Some(n);
self
}
pub fn with_max_extent(mut self, n: i32) -> Self {
self.max_extent = Some(n);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopReason {
Exhausted,
MaxBlocks,
MaxExtent,
}
#[derive(Debug, Clone)]
pub struct Component {
pub seed: BlockPosition,
pub bounds: BoundingBox,
pub blocks: Vec<BlockPosition>,
pub stop_reason: StopReason,
}
impl Component {
pub fn block_count(&self) -> usize {
self.blocks.len()
}
}
pub fn flood<M: Mask>(
seed: BlockPosition,
mask: &M,
connectivity: Connectivity,
limits: &Limits,
) -> Component {
let mut visited = VisitedSet::new();
flood_with_visited(seed, mask, connectivity, limits, &mut visited)
}
pub fn flood_with_visited<M: Mask>(
seed: BlockPosition,
mask: &M,
connectivity: Connectivity,
limits: &Limits,
visited: &mut VisitedSet,
) -> Component {
let offsets = connectivity.offsets();
let mut blocks: Vec<BlockPosition> = Vec::new();
let mut queue: VecDeque<BlockPosition> = VecDeque::new();
if !visited.contains(seed.x, seed.y, seed.z) && mask.test(seed.x, seed.y, seed.z) {
visited.insert(seed.x, seed.y, seed.z);
queue.push_back(seed);
}
let mut min = (seed.x, seed.y, seed.z);
let mut max = (seed.x, seed.y, seed.z);
let mut stop = StopReason::Exhausted;
while let Some(pos) = queue.pop_front() {
blocks.push(pos);
min.0 = min.0.min(pos.x);
min.1 = min.1.min(pos.y);
min.2 = min.2.min(pos.z);
max.0 = max.0.max(pos.x);
max.1 = max.1.max(pos.y);
max.2 = max.2.max(pos.z);
if let Some(cap) = limits.max_blocks {
if blocks.len() >= cap {
stop = StopReason::MaxBlocks;
break;
}
}
if let Some(ext) = limits.max_extent {
let dx = max.0 - min.0;
let dy = max.1 - min.1;
let dz = max.2 - min.2;
if dx > ext || dy > ext || dz > ext {
stop = StopReason::MaxExtent;
break;
}
}
for &(dx, dy, dz) in offsets {
let nx = pos.x + dx;
let ny = pos.y + dy;
let nz = pos.z + dz;
if !visited.insert(nx, ny, nz) {
continue; }
if mask.test(nx, ny, nz) {
queue.push_back(BlockPosition::new(nx, ny, nz));
}
}
}
let bounds = if blocks.is_empty() {
BoundingBox::new((seed.x, seed.y, seed.z), (seed.x, seed.y, seed.z))
} else {
BoundingBox::new(min, max)
};
Component {
seed,
bounds,
blocks,
stop_reason: stop,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Continue {
Yes,
Stop,
}
pub fn connected_components<M, I, F>(
candidates: I,
mask: &M,
connectivity: Connectivity,
limits: &Limits,
mut on_component: F,
) -> usize
where
M: Mask,
I: IntoIterator<Item = BlockPosition>,
F: FnMut(Component) -> Continue,
{
let mut visited = VisitedSet::new();
let mut count = 0usize;
for seed in candidates {
if visited.contains(seed.x, seed.y, seed.z) {
continue;
}
if !mask.test(seed.x, seed.y, seed.z) {
visited.insert(seed.x, seed.y, seed.z);
continue;
}
let component = flood_with_visited(seed, mask, connectivity, limits, &mut visited);
debug_assert!(!component.blocks.is_empty());
count += 1;
match on_component(component) {
Continue::Yes => {}
Continue::Stop => break,
}
}
count
}
pub fn connected_components_collect<M, I>(
candidates: I,
mask: &M,
connectivity: Connectivity,
limits: &Limits,
) -> Vec<Component>
where
M: Mask,
I: IntoIterator<Item = BlockPosition>,
{
let mut out = Vec::new();
connected_components(candidates, mask, connectivity, limits, |c| {
out.push(c);
Continue::Yes
});
out
}
pub fn iter_bounds(bounds: &BoundingBox) -> impl Iterator<Item = BlockPosition> {
let (mnx, mny, mnz) = bounds.min;
let (mxx, mxy, mxz) = bounds.max;
(mny..=mxy).flat_map(move |y| {
(mnz..=mxz).flat_map(move |z| (mnx..=mxx).map(move |x| BlockPosition::new(x, y, z)))
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn bp(x: i32, y: i32, z: i32) -> BlockPosition {
BlockPosition::new(x, y, z)
}
struct SetMask {
on: HashSet<(i32, i32, i32)>,
}
impl SetMask {
fn from_iter<I: IntoIterator<Item = (i32, i32, i32)>>(iter: I) -> Self {
Self {
on: iter.into_iter().collect(),
}
}
}
impl Mask for SetMask {
fn test(&self, x: i32, y: i32, z: i32) -> bool {
self.on.contains(&(x, y, z))
}
}
#[test]
fn flood_empty_when_mask_misses_seed() {
let mask = SetMask::from_iter([]);
let c = flood(bp(0, 0, 0), &mask, Connectivity::Face, &Limits::unbounded());
assert!(c.blocks.is_empty());
assert_eq!(c.stop_reason, StopReason::Exhausted);
}
#[test]
fn flood_single_isolated_block() {
let mask = SetMask::from_iter([(0, 0, 0)]);
let c = flood(bp(0, 0, 0), &mask, Connectivity::Face, &Limits::unbounded());
assert_eq!(c.blocks.len(), 1);
assert_eq!(c.bounds.min, (0, 0, 0));
assert_eq!(c.bounds.max, (0, 0, 0));
assert_eq!(c.stop_reason, StopReason::Exhausted);
}
#[test]
fn flood_3x3x3_solid_cube_face_connectivity() {
let mut on = Vec::new();
for x in 0..3 {
for y in 0..3 {
for z in 0..3 {
on.push((x, y, z));
}
}
}
let mask = SetMask::from_iter(on);
let c = flood(bp(1, 1, 1), &mask, Connectivity::Face, &Limits::unbounded());
assert_eq!(c.blocks.len(), 27);
assert_eq!(c.bounds.min, (0, 0, 0));
assert_eq!(c.bounds.max, (2, 2, 2));
}
#[test]
fn flood_diagonal_pair_face_vs_corner() {
let mask = SetMask::from_iter([(0, 0, 0), (1, 1, 1)]);
let face = flood(bp(0, 0, 0), &mask, Connectivity::Face, &Limits::unbounded());
assert_eq!(face.blocks.len(), 1, "Face: corner-touch is disconnected");
let corner = flood(
bp(0, 0, 0),
&mask,
Connectivity::Corner,
&Limits::unbounded(),
);
assert_eq!(corner.blocks.len(), 2, "Corner: corner-touch is connected");
}
#[test]
fn flood_edge_diagonal_with_edge_connectivity() {
let mask = SetMask::from_iter([(0, 0, 0), (1, 1, 0)]);
let face = flood(bp(0, 0, 0), &mask, Connectivity::Face, &Limits::unbounded());
assert_eq!(face.blocks.len(), 1);
let edge = flood(bp(0, 0, 0), &mask, Connectivity::Edge, &Limits::unbounded());
assert_eq!(edge.blocks.len(), 2);
}
#[test]
fn flood_hollow_shell_does_not_include_interior() {
let mut on = Vec::new();
for x in 0..3 {
for y in 0..3 {
for z in 0..3 {
if !(x == 1 && y == 1 && z == 1) {
on.push((x, y, z));
}
}
}
}
let mask = SetMask::from_iter(on);
let c = flood(bp(0, 0, 0), &mask, Connectivity::Face, &Limits::unbounded());
assert_eq!(c.blocks.len(), 26);
assert!(!c.blocks.iter().any(|b| (b.x, b.y, b.z) == (1, 1, 1)));
}
#[test]
fn limit_max_blocks_stops_early() {
let mask = SetMask::from_iter((0..100).map(|x| (x, 0, 0)));
let limits = Limits::unbounded().with_max_blocks(10);
let c = flood(bp(0, 0, 0), &mask, Connectivity::Face, &limits);
assert_eq!(c.blocks.len(), 10);
assert_eq!(c.stop_reason, StopReason::MaxBlocks);
}
#[test]
fn limit_max_extent_stops_early() {
let mask = SetMask::from_iter((0..100).map(|x| (x, 0, 0)));
let limits = Limits::unbounded().with_max_extent(5);
let c = flood(bp(0, 0, 0), &mask, Connectivity::Face, &limits);
assert_eq!(c.stop_reason, StopReason::MaxExtent);
let dx = c.bounds.max.0 - c.bounds.min.0;
assert!(
dx > 5,
"extent that triggered the stop should exceed the cap (got dx={dx})"
);
}
#[test]
fn connected_components_finds_two_separated_cubes() {
let mut on = Vec::new();
for x in 0..2 {
for y in 0..2 {
for z in 0..2 {
on.push((x, y, z));
on.push((x + 10, y, z));
}
}
}
let mask = SetMask::from_iter(on);
let candidates = iter_bounds(&BoundingBox::new((0, 0, 0), (11, 1, 1)));
let comps = connected_components_collect(
candidates,
&mask,
Connectivity::Face,
&Limits::unbounded(),
);
assert_eq!(comps.len(), 2);
let sizes: Vec<usize> = comps.iter().map(|c| c.blocks.len()).collect();
assert_eq!(sizes, vec![8, 8]);
}
#[test]
fn connected_components_visited_extracts_in_one_pass() {
let mut on = Vec::new();
for x in 0..5 {
on.push((x, 0, 0));
}
for y in 0..3 {
on.push((100, y, 0));
}
on.push((0, 0, 100));
let mask = SetMask::from_iter(on.clone());
let candidates = on.iter().map(|&(x, y, z)| BlockPosition::new(x, y, z));
let comps = connected_components_collect(
candidates,
&mask,
Connectivity::Face,
&Limits::unbounded(),
);
assert_eq!(comps.len(), 3);
let total: usize = comps.iter().map(|c| c.blocks.len()).sum();
assert_eq!(
total,
on.len(),
"every mask-on block appears in exactly one component"
);
}
#[test]
fn connected_components_callback_can_stop_early() {
let seeds: Vec<(i32, i32, i32)> = (0..6).map(|i| (i * 10, 0, 0)).collect();
let mask = SetMask::from_iter(seeds.iter().copied());
let candidates = seeds.iter().map(|&(x, y, z)| BlockPosition::new(x, y, z));
let mut yielded = 0usize;
let returned = connected_components(
candidates,
&mask,
Connectivity::Face,
&Limits::unbounded(),
|_| {
yielded += 1;
if yielded == 2 {
Continue::Stop
} else {
Continue::Yes
}
},
);
assert_eq!(yielded, 2);
assert_eq!(returned, 2);
}
#[test]
fn connectivity_changes_component_count() {
let blocks = vec![(0, 0, 0), (1, 1, 0), (2, 2, 0)];
let mask = SetMask::from_iter(blocks.clone());
let candidates = || blocks.iter().map(|&(x, y, z)| BlockPosition::new(x, y, z));
let face = connected_components_collect(
candidates(),
&mask,
Connectivity::Face,
&Limits::unbounded(),
);
assert_eq!(face.len(), 3, "Face: 3 isolated blocks");
let edge = connected_components_collect(
candidates(),
&mask,
Connectivity::Edge,
&Limits::unbounded(),
);
assert_eq!(
edge.len(),
1,
"Edge: all three connect via x/y edge diagonals"
);
}
}