use std::collections::{BTreeMap, BTreeSet};
use crate::error::{AnalysisError, FieldError};
use crate::field::{Field, Value, leaf_place};
use crate::topology::{Location, Topology};
use crate::tree::{Coord, Place, TileLevel, align, child_place};
use crate::tree::{LEAF_LOG2, LOWER_SPAN_LOG2, UPPER_SPAN_LOG2};
mod euler;
pub use euler::euler;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Connectivity {
Six,
TwentySix,
}
impl Connectivity {
fn offsets(self) -> Vec<[i32; 3]> {
let all =
(-1..=1_i32).flat_map(|z| (-1..=1).flat_map(move |y| (-1..=1).map(move |x| [x, y, z])));
all.filter(|d| {
let n: i32 = d.iter().map(|c: &i32| c.abs()).sum();
n > 0 && (self == Connectivity::TwentySix || n == 1)
})
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Label {
Component(u32),
Excluded,
}
const ROOT_CELLS: usize = 1 << 22;
const LOG: [u32; 4] = [UPPER_SPAN_LOG2, LOWER_SPAN_LOG2, LEAF_LOG2, 0];
#[derive(Debug, Clone)]
struct Axis(Vec<(i64, i64)>);
impl Axis {
fn new(keys: &BTreeSet<i64>) -> Self {
let near: BTreeSet<i64> = keys.iter().flat_map(|k| [k - 1, *k, k + 1]).collect();
let mut out = vec![];
let mut next = i64::MIN;
for v in near {
if v > next {
out.push((next, v - 1));
}
out.push((v, v));
next = v + 1;
}
out.push((next, i64::MAX));
Self(out)
}
fn find(&self, r: i64) -> usize {
self.0.partition_point(|(_, hi)| *hi < r)
}
fn singleton(&self, i: usize) -> Option<i64> {
self.0.get(i).and_then(|(lo, hi)| (lo == hi).then_some(*lo))
}
fn unbounded(&self, i: usize) -> bool {
i == 0 || i + 1 >= self.0.len()
}
}
#[derive(Debug, Clone)]
struct RootGrid {
axes: [Axis; 3],
keys: BTreeSet<Coord>,
}
impl RootGrid {
fn new(t: &Topology) -> Result<Self, AnalysisError> {
let keys: BTreeSet<Coord> = t.root_keys().collect();
let axis = |k: usize| Axis::new(&keys.iter().map(|c| i64::from(c[k] >> LOG[0])).collect());
let axes = [axis(0), axis(1), axis(2)];
let cells = axes.iter().try_fold(1_usize, |n, a| n.checked_mul(a.0.len()));
if cells.is_none_or(|n| n > ROOT_CELLS) {
return Err(AnalysisError::TooSpread);
}
Ok(Self { axes, keys })
}
fn dims(&self) -> [usize; 3] {
self.axes.each_ref().map(|a| a.0.len())
}
fn cell_of(&self, v: Coord) -> [usize; 3] {
[0, 1, 2].map(|k| self.axes[k].find(i64::from(v[k] >> LOG[0])))
}
fn key(&self, c: [usize; 3]) -> Option<Coord> {
let r = [0, 1, 2].map(|k| self.axes[k].singleton(c[k]));
let [Some(x), Some(y), Some(z)] = r else { return None };
let key = [x, y, z].map(|r| i32::try_from(r << LOG[0]).ok());
let [Some(x), Some(y), Some(z)] = key else { return None };
self.keys.contains(&[x, y, z]).then_some([x, y, z])
}
fn unbounded(&self, c: [usize; 3]) -> bool {
(0..3).any(|k| self.axes[k].unbounded(c[k]))
}
}
#[derive(Debug, Clone, Default)]
struct Units {
parent: Vec<u32>,
meets: Vec<bool>,
overflow: bool,
}
impl Units {
fn add(&mut self, meets: bool) -> u32 {
let Ok(id) = u32::try_from(self.parent.len()) else {
self.overflow = true;
return 0;
};
self.parent.push(id);
self.meets.push(meets);
id
}
fn len(&self) -> u32 {
u32::try_from(self.parent.len()).unwrap_or(u32::MAX)
}
fn find(&mut self, mut x: u32) -> u32 {
loop {
let p = self.parent[x as usize];
if p == x {
return x;
}
let grand = self.parent[p as usize];
self.parent[x as usize] = grand;
x = grand;
}
}
fn join(&mut self, a: u32, b: u32) {
if !(self.meets[a as usize] && self.meets[b as usize]) {
return;
}
let (ra, rb) = (self.find(a), self.find(b));
let (lo, hi) = (ra.min(rb), ra.max(rb));
self.parent[hi as usize] = lo;
}
}
#[derive(Debug, Clone, Copy)]
enum Kind {
Uniform(u32),
Split,
}
#[derive(Debug, Clone, Copy)]
enum Work {
Inside(Coord, usize),
Pair(Coord, Coord, usize, [i32; 3]),
Mixed(Coord, usize, u32, [i32; 3]),
}
fn place(o: Coord, level: usize) -> u32 {
child_place(o, LOG[level], LOG[level - 1] - LOG[level])
}
fn across(c: [i32; 3], f: [i32; 3], e: [i32; 3], n: i32) -> [i32; 3] {
[0, 1, 2].map(|k| c[k] + f[k] - e[k] * n)
}
fn per_axis(level: usize) -> i32 {
1 << (LOG[level] - LOG[level + 1])
}
fn child(o: Coord, level: usize, c: [i32; 3]) -> Coord {
[0, 1, 2].map(|k| o[k] + (c[k] << LOG[level + 1]))
}
fn children(level: usize) -> impl Iterator<Item = [i32; 3]> {
let n = per_axis(level);
(0..n).flat_map(move |z| (0..n).flat_map(move |y| (0..n).map(move |x| [x, y, z])))
}
fn on_side(c: [i32; 3], e: [i32; 3], n: i32) -> bool {
(0..3).all(|k| match e[k] {
1 => c[k] == n - 1,
-1 => c[k] == 0,
_ => true,
})
}
struct Walk<'a> {
t: &'a Topology,
units: Units,
voxel_base: u32,
tile_base: u32,
empty: BTreeMap<(usize, Coord), u32>,
background: bool,
offsets: Vec<[i32; 3]>,
stack: Vec<Work>,
}
impl Walk<'_> {
fn empty_unit(&mut self, level: usize, o: Coord) -> u32 {
let node = align(o, LOG[level - 1]);
let base = match self.empty.get(&(level, node)) {
Some(b) => *b,
None => {
let n = per_axis(level - 1);
let b = self.units.len();
for _ in 0..n * n * n {
self.units.add(false);
}
self.empty.insert((level, node), b);
b
}
};
let Some(u) = base.checked_add(place(o, level)) else {
self.units.overflow = true;
return 0;
};
if let Some(m) = self.units.meets.get_mut(u as usize) {
*m = self.background;
}
u
}
fn voxel(base: u32, r: Coord, c: [i32; 3]) -> u32 {
base + leaf_place(child(r, 2, c))
}
fn leaf_base(&self, r: Coord) -> u32 {
match self.t.locate(r) {
Location::Leaf(n) => self.voxel_base + n.get() * 512,
Location::Tile(..) | Location::Background => 0,
}
}
fn classify(&mut self, r: Coord, level: usize) -> Kind {
match (level, self.t.locate(r)) {
(2, Location::Leaf(_)) => Kind::Split,
(_, Location::Tile(n, l)) if l.edge_log2() >= LOG[level] => {
Kind::Uniform(self.tile_base + n.get())
}
(0, _) if self.t.has_upper_node(r) => Kind::Split,
(1, _) if self.t.has_lower_node(r) => Kind::Split,
_ => Kind::Uniform(self.empty_unit(level, r)),
}
}
fn run(&mut self) {
while let Some(w) = self.stack.pop() {
match w {
Work::Inside(r, l) => self.inside(r, l),
Work::Pair(a, b, l, e) => self.pair(a, b, l, e),
Work::Mixed(r, l, u, e) => match self.classify(r, l) {
Kind::Uniform(x) => self.units.join(x, u),
Kind::Split if l == 2 => {
let base = self.leaf_base(r);
for c in children(l).filter(|c| on_side(*c, e, per_axis(l))) {
self.units.join(Self::voxel(base, r, c), u);
}
}
Kind::Split => {
let n = per_axis(l);
for c in children(l).filter(|c| on_side(*c, e, n)) {
self.stack.push(Work::Mixed(child(r, l, c), l + 1, u, e));
}
}
},
}
}
}
fn kinds(&mut self, r: Coord, l: usize) -> Vec<Kind> {
if l == 2 {
let base = self.leaf_base(r);
return children(l).map(|c| Kind::Uniform(Self::voxel(base, r, c))).collect();
}
if l > 0 {
return children(l).map(|c| self.classify(child(r, l, c), l + 1)).collect();
}
let places = self.t.upper_places(r);
children(l)
.zip(places)
.map(|(c, p)| match p {
Place::Child => Kind::Split,
Place::Tile => self.classify(child(r, l, c), l + 1),
Place::Empty => Kind::Uniform(self.empty_unit(1, child(r, l, c))),
})
.collect()
}
fn inside(&mut self, r: Coord, l: usize) {
if matches!(self.classify(r, l), Kind::Uniform(_)) {
return;
}
let n = per_axis(l);
let kinds = self.kinds(r, l);
let at = |c: [i32; 3]| usize::try_from(c[0] + n * (c[1] + n * c[2])).unwrap_or(0);
let forward: Vec<[i32; 3]> = self
.offsets
.iter()
.copied()
.filter(|e| e.iter().find(|x| **x != 0).is_some_and(|x| *x > 0))
.collect();
for c in children(l) {
let kc = kinds[at(c)];
if matches!(kc, Kind::Split) {
self.stack.push(Work::Inside(child(r, l, c), l + 1));
}
for e in &forward {
let d = [c[0] + e[0], c[1] + e[1], c[2] + e[2]];
if d.iter().all(|x| (0..n).contains(x)) {
let (a, b) = (child(r, l, c), child(r, l, d));
match (kc, kinds[at(d)]) {
(Kind::Uniform(x), Kind::Uniform(y)) => self.units.join(x, y),
(Kind::Split, Kind::Uniform(y)) => {
self.stack.push(Work::Mixed(a, l + 1, y, *e))
}
(Kind::Uniform(x), Kind::Split) => {
self.stack.push(Work::Mixed(b, l + 1, x, e.map(|v| -v)))
}
(Kind::Split, Kind::Split) => self.stack.push(Work::Pair(a, b, l + 1, *e)),
}
}
}
}
}
fn pair(&mut self, a: Coord, b: Coord, l: usize, e: [i32; 3]) {
match (self.classify(a, l), self.classify(b, l)) {
(Kind::Uniform(x), Kind::Uniform(y)) => self.units.join(x, y),
(Kind::Split, Kind::Uniform(y)) => self.stack.push(Work::Mixed(a, l, y, e)),
(Kind::Uniform(x), Kind::Split) => self.stack.push(Work::Mixed(b, l, x, e.map(|c| -c))),
(Kind::Split, Kind::Split) if l == 2 => {
let (ba, bb, n) = (self.leaf_base(a), self.leaf_base(b), per_axis(l));
for c in children(l).filter(|c| on_side(*c, e, n)) {
for f in &self.offsets {
let t = across(c, *f, e, n);
if t.iter().all(|x| (0..n).contains(x)) {
self.units.join(Self::voxel(ba, a, c), Self::voxel(bb, b, t));
}
}
}
}
(Kind::Split, Kind::Split) => {
let n = per_axis(l);
for c in children(l).filter(|c| on_side(*c, e, n)) {
for f in &self.offsets {
let t = across(c, *f, e, n);
if t.iter().all(|x| (0..n).contains(x)) {
self.stack.push(Work::Pair(child(a, l, c), child(b, l, t), l + 1, *f));
}
}
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct Labels {
stamp: u64,
labels: Vec<u32>,
voxel_base: u32,
tile_base: u32,
empty: BTreeMap<(usize, Coord), u32>,
root: RootGrid,
root_units: BTreeMap<[usize; 3], u32>,
outside: u32,
count: u32,
}
impl Walk<'_> {
fn root_kind(
&mut self,
root: &RootGrid,
c: [usize; 3],
units: &mut BTreeMap<[usize; 3], u32>,
outside: u32,
) -> (Kind, Coord) {
if root.unbounded(c) {
return (Kind::Uniform(outside), [0; 3]);
}
if let Some(key) = root.key(c) {
return (self.classify(key, 0), key);
}
let u = *units.entry(c).or_insert_with(|| self.units.add(self.background));
(Kind::Uniform(u), [0; 3])
}
fn root(&mut self, root: &RootGrid, outside: u32) -> BTreeMap<[usize; 3], u32> {
let mut units = BTreeMap::new();
let [nx, ny, nz] = root.dims();
let cells =
(0..nz).flat_map(|z| (0..ny).flat_map(move |y| (0..nx).map(move |x| [x, y, z])));
let offsets = self.offsets.clone();
for c in cells {
let (kc, a) = self.root_kind(root, c, &mut units, outside);
if matches!(kc, Kind::Split) {
self.stack.push(Work::Inside(a, 0));
}
for e in offsets.iter().filter(|e| e.iter().find(|x| **x != 0).is_some_and(|x| *x > 0))
{
let d = [0, 1, 2].map(|k| c[k].checked_add_signed(e[k] as isize));
let [Some(dx), Some(dy), Some(dz)] = d else { continue };
if dx >= nx || dy >= ny || dz >= nz {
continue;
}
let (kd, b) = self.root_kind(root, [dx, dy, dz], &mut units, outside);
match (kc, kd) {
(Kind::Uniform(x), Kind::Uniform(y)) => self.units.join(x, y),
(Kind::Split, Kind::Uniform(y)) => self.stack.push(Work::Mixed(a, 0, y, *e)),
(Kind::Uniform(x), Kind::Split) => {
self.stack.push(Work::Mixed(b, 0, x, e.map(|v| -v)))
}
(Kind::Split, Kind::Split) => self.stack.push(Work::Pair(a, b, 0, *e)),
}
}
}
units
}
}
fn fits_units(leaves: usize, tiles: usize) -> Result<(), AnalysisError> {
let wide = |n: usize| u64::try_from(n).unwrap_or(u64::MAX);
let units = wide(leaves).saturating_mul(512).saturating_add(wide(tiles)).saturating_add(1);
if units > u64::from(u32::MAX) { Err(AnalysisError::TooLarge) } else { Ok(()) }
}
pub fn label<T: Value>(
t: &Topology,
f: &Field<T>,
meets: impl Fn(T) -> bool,
conn: Connectivity,
) -> Result<Labels, AnalysisError> {
f.belongs_to(t)?;
fits_units(t.leaf_count(), t.tile_count())?;
let root = RootGrid::new(t)?;
let background = meets(f.background());
let mut units = Units::default();
let outside = units.add(background);
let voxel_base = units.len();
for n in t.leaf_numbers() {
for v in f.leaf(t, n)? {
units.add(meets(*v));
}
}
let tile_base = units.len();
for n in t.tile_numbers() {
units.add(meets(f.tile(t, n)?));
}
let mut walk = Walk {
t,
units,
voxel_base,
tile_base,
empty: BTreeMap::new(),
background,
offsets: conn.offsets(),
stack: Vec::new(),
};
let root_units = walk.root(&root, outside);
walk.run();
if walk.units.overflow {
return Err(AnalysisError::TooLarge);
}
let (labels, count) = number(walk.units)?;
Ok(Labels {
stamp: t.stamp(),
labels,
voxel_base,
tile_base,
empty: walk.empty,
root,
root_units,
outside,
count,
})
}
const EXCLUDED: u32 = u32::MAX;
fn number(units: Units) -> Result<(Vec<u32>, u32), AnalysisError> {
let Units { parent: mut labels, meets, .. } = units;
let mut count = 0;
for (id, meets) in meets.into_iter().enumerate() {
let parent = labels[id] as usize;
labels[id] = if !meets {
EXCLUDED
} else if parent == id {
if count == EXCLUDED {
return Err(AnalysisError::TooLarge);
}
count += 1;
count - 1
} else {
labels[parent]
};
}
Ok((labels, count))
}
fn decode(stored: u32) -> Label {
if stored == EXCLUDED { Label::Excluded } else { Label::Component(stored) }
}
impl Labels {
pub fn count(&self) -> u32 {
self.count
}
fn unit_at(&self, t: &Topology, v: Coord) -> Option<u32> {
match t.locate(v) {
Location::Leaf(n) => Some(self.voxel_base + n.get() * 512 + leaf_place(v)),
Location::Tile(n, _) => Some(self.tile_base + n.get()),
Location::Background if t.has_lower_node(v) => self
.empty
.get(&(2, TileLevel::Upper.origin(v)))
.map(|b| b + place(TileLevel::Lower.origin(v), 2)),
Location::Background if t.has_upper_node(v) => self
.empty
.get(&(1, TileLevel::Root.origin(v)))
.map(|b| b + place(TileLevel::Upper.origin(v), 1)),
Location::Background => {
let c = self.root.cell_of(v);
if self.root.unbounded(c) {
Some(self.outside)
} else {
self.root_units.get(&c).copied()
}
}
}
}
pub fn label_at(&self, t: &Topology, v: Coord) -> Result<Label, AnalysisError> {
if t.stamp() != self.stamp {
return Err(AnalysisError::Field(FieldError::StaleField));
}
let u = self.unit_at(t, v).ok_or(AnalysisError::Unlabelled(v))?;
self.labels.get(u as usize).map(|x| decode(*x)).ok_or(AnalysisError::Unlabelled(v))
}
pub fn reaches_outside(&self, label: Label) -> bool {
label != Label::Excluded
&& self.labels.get(self.outside as usize).map(|x| decode(*x)) == Some(label)
}
}
#[derive(Debug, Clone)]
pub struct Flood {
labels: Labels,
label: Label,
}
impl Flood {
pub fn label(&self) -> Label {
self.label
}
pub fn contains(&self, t: &Topology, v: Coord) -> Result<bool, AnalysisError> {
let label = self.labels.label_at(t, v)?;
Ok(self.label != Label::Excluded && label == self.label)
}
pub fn reaches_outside(&self) -> bool {
self.labels.reaches_outside(self.label)
}
}
pub fn flood_fill<T: Value>(
t: &Topology,
f: &Field<T>,
meets: impl Fn(T) -> bool,
conn: Connectivity,
seed: Coord,
) -> Result<Flood, AnalysisError> {
let labels = label(t, f, meets, conn)?;
let label = labels.label_at(t, seed)?;
Ok(Flood { labels, label })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn more_units_than_a_u32_numbers_are_refused_before_any_is_made() {
let leaves = (1 << 23) - 1;
assert!(fits_units(leaves, 510).is_ok());
assert!(matches!(fits_units(leaves, 511), Err(AnalysisError::TooLarge)));
assert!(matches!(fits_units(1 << 23, 0), Err(AnalysisError::TooLarge)));
}
use crate::topology::TopologyEditor;
fn topology(edit: impl FnOnce(&mut TopologyEditor<'_>)) -> Topology {
let empty = Topology::new();
let mut e = empty.edit();
edit(&mut e);
e.commit().unwrap().0
}
fn positive(v: f32) -> bool {
v > 0.0
}
#[test]
fn background_enclosed_by_leaves_is_apart_from_the_outside() {
let t = topology(|e| {
for i in 0..27 {
let d = [i % 3 - 1, i / 3 % 3 - 1, i / 9 - 1];
if d != [0, 0, 0] {
e.activate(d.map(|c| c * 8)).unwrap();
}
}
});
let mut f = Field::new(&t, 1.0_f32);
for n in t.leaf_numbers().collect::<Vec<_>>() {
f.leaf_mut(&t, n).unwrap().fill(-1.0);
}
let l = label(&t, &f, positive, Connectivity::TwentySix).unwrap();
assert_eq!(l.count(), 2);
let inside = l.label_at(&t, [3, 3, 3]).unwrap();
let outside = l.label_at(&t, [100, 0, 0]).unwrap();
assert_ne!(inside, outside);
for v in [[200, 200, 200], [4000, 300, 2000]] {
assert_eq!(l.label_at(&t, v), Ok(outside), "{v:?}");
}
assert!(!l.reaches_outside(inside) && l.reaches_outside(outside));
assert_eq!(l.label_at(&t, [-8, 0, 0]).unwrap(), Label::Excluded);
}
#[test]
fn labelling_a_small_grid_ends_in_time() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let t = topology(|e| {
e.activate([0, 0, 0]).unwrap();
e.add_tile(TileLevel::Lower, [8, 0, 0]).unwrap();
});
let f = Field::new(&t, 1.0_f32);
tx.send(label(&t, &f, positive, Connectivity::Six).map(|l| l.count())).unwrap_or(());
});
assert_eq!(rx.recv_timeout(std::time::Duration::from_secs(60)), Ok(Ok(1)));
}
#[test]
fn six_and_twenty_six_neighbours_are_one_step_away() {
for (conn, n) in [(Connectivity::Six, 6), (Connectivity::TwentySix, 26)] {
let o = conn.offsets();
assert_eq!(o.len(), n, "{conn:?}");
assert!(o.iter().all(|d| *d != [0, 0, 0] && d.iter().all(|c| c.abs() <= 1)));
}
}
#[test]
fn an_upper_nodes_empty_places_are_the_outsides() {
let t = topology(|e| {
for x in 0..8 {
e.activate([x * 8, 0, 0]).unwrap();
}
});
let mut f = Field::new(&t, 1.0_f32);
for n in t.leaf_numbers().collect::<Vec<_>>() {
f.leaf_mut(&t, n).unwrap().fill(-1.0);
}
let l = label(&t, &f, positive, Connectivity::Six).unwrap();
let far = l.label_at(&t, [1_000_000, 0, 0]).unwrap();
assert_ne!(far, Label::Excluded);
for v in [[130, 0, 0], [500, 300, 200], [4095, 4095, 4095]] {
assert_eq!(l.label_at(&t, v), Ok(far), "{v:?}");
}
let fill = flood_fill(&t, &f, positive, Connectivity::Six, [500, 300, 200]).unwrap();
assert!(fill.reaches_outside());
assert!(fill.contains(&t, [64, 0, 0]).unwrap() && !fill.contains(&t, [63, 0, 0]).unwrap());
}
#[test]
fn a_corner_joins_under_26_and_not_under_6() {
let t = topology(|e| {
e.activate([0, 0, 0]).unwrap();
});
let mut f = Field::new(&t, -1.0_f32);
let n = t.leaf_numbers().next().unwrap();
let leaf = f.leaf_mut(&t, n).unwrap();
leaf.fill(-1.0);
leaf[0] = 1.0;
leaf[1 + 8 + 64] = 1.0;
assert_eq!(label(&t, &f, positive, Connectivity::Six).unwrap().count(), 2);
assert_eq!(label(&t, &f, positive, Connectivity::TwentySix).unwrap().count(), 1);
}
#[test]
fn root_tiles_can_enclose_a_root_region() {
let t = topology(|e| {
for i in 0..27 {
let d = [i % 3 - 1, i / 3 % 3 - 1, i / 9 - 1];
if d != [0, 0, 0] {
e.add_tile(TileLevel::Root, d.map(|c| c * 4096)).unwrap();
}
}
});
let f = Field::new(&t, 1.0_f32);
let mut f = f;
for n in t.tile_numbers().collect::<Vec<_>>() {
f.set_tile(&t, n, -1.0).unwrap();
}
let l = label(&t, &f, positive, Connectivity::Six).unwrap();
let centre = l.label_at(&t, [100, 100, 100]).unwrap();
let far = l.label_at(&t, [1_000_000, 0, 0]).unwrap();
assert_eq!(l.count(), 2);
assert!(!l.reaches_outside(centre) && l.reaches_outside(far));
}
#[test]
fn labelling_a_few_leaves_ends_in_time() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let t = topology(|e| {
for i in 0..16 {
e.activate([4096 * i, 0, 0]).unwrap();
}
});
let mut f = Field::new(&t, -1.0_f32);
for n in t.leaf_numbers() {
f.leaf_mut(&t, n).unwrap().fill(1.0);
}
tx.send(label(&t, &f, positive, Connectivity::Six).map(|l| l.count())).unwrap_or(());
});
let got = rx.recv_timeout(std::time::Duration::from_secs(20));
assert!(matches!(got, Ok(Ok(16))), "{got:?}");
}
#[test]
fn tiles_join_what_touches_them() {
let t = topology(|e| {
e.add_tile(TileLevel::Upper, [0, 0, 0]).unwrap();
e.activate([128, 0, 0]).unwrap();
e.add_tile(TileLevel::Lower, [200, 0, 0]).unwrap();
});
let mut f = Field::new(&t, -1.0_f32);
for n in t.tile_numbers().collect::<Vec<_>>() {
f.set_tile(&t, n, 1.0).unwrap();
}
let n = t.leaf_numbers().next().unwrap();
f.leaf_mut(&t, n).unwrap()[8 * 3 + 64 * 5] = 1.0;
let fill = flood_fill(&t, &f, positive, Connectivity::Six, [5, 3, 5]).unwrap();
assert!(fill.contains(&t, [128, 3, 5]).unwrap(), "the leaf voxel on the tile's face");
assert!(!fill.contains(&t, [129, 3, 5]).unwrap());
assert!(!fill.contains(&t, [200, 0, 0]).unwrap(), "the far tile is apart");
assert!(!fill.reaches_outside());
let none = flood_fill(&t, &f, positive, Connectivity::Six, [129, 3, 5]).unwrap();
assert_eq!(none.label(), Label::Excluded);
for v in [[129, 3, 5], [130, 0, 0], [5000, 0, 0]] {
assert!(!none.contains(&t, v).unwrap(), "{v:?}");
}
let (t2, _) = t.edit().commit().unwrap();
for flood in [&fill, &none] {
assert_eq!(
flood.contains(&t2, [0, 0, 0]),
Err(AnalysisError::Field(FieldError::StaleField))
);
}
}
}