use crate::descriptor::Subgrid;
use crate::library::Library;
use crate::map::Map;
use crate::placement::ir::Dir;
use crate::wfc::{E, N, S, W};
pub(crate) const SQUARE_EPSILON: f32 = 1e-3;
pub(crate) const EDGE_EPSILON: f32 = 1e-3;
pub fn quarter_turns(who: &str, yaw_deg: f32) -> Result<u8, String> {
if !yaw_deg.is_finite() {
return Err(format!("`{who}` has a yaw of {yaw_deg}, which is not an angle"));
}
let from_square = yaw_deg.rem_euclid(90.0);
let off = from_square.min(90.0 - from_square);
if off > SQUARE_EPSILON {
return Err(format!(
"`{who}` sits at yaw {yaw_deg}; a lattice face is only defined at multiples of 90, so \
its edge tokens cannot be matched against a neighbour"
));
}
Ok((yaw_deg / 90.0).round().rem_euclid(4.0) as u8)
}
pub fn face(g: &Subgrid, dir: Dir, div: (u32, u32, u32)) -> Vec<Option<&str>> {
let (dx, dy, dz) = div;
let mut out = Vec::new();
if dx == 0 || dy == 0 || dz == 0 {
return out;
}
let token = |at: (u32, u32, u32)| g.at(at).and_then(|c| c.edge.as_deref());
for y in 0..dy {
match dir {
E => (0..dz).for_each(|z| out.push(token((dx - 1, y, z)))),
W => (0..dz).for_each(|z| out.push(token((0, y, z)))),
N => (0..dx).for_each(|x| out.push(token((x, y, 0)))),
S => (0..dx).for_each(|x| out.push(token((x, y, dz - 1)))),
_ => return Vec::new(),
}
}
out
}
pub const EDGE_SOLID: &str = "solid";
pub const EDGE_OPEN: &str = "open";
pub fn derive_edges(g: &Subgrid, div: (u32, u32, u32)) -> Vec<((u32, u32, u32), &'static str)> {
let (dx, dy, dz) = div;
if dx == 0 || dy == 0 || dz == 0 {
return Vec::new();
}
let mut out: Vec<(u32, u32, u32)> = Vec::new();
for y in 0..dy {
for z in 0..dz {
out.push((dx - 1, y, z));
out.push((0, y, z));
}
for x in 0..dx {
out.push((x, y, 0));
out.push((x, y, dz - 1));
}
}
out.sort_unstable();
out.dedup();
out.into_iter()
.map(|at| {
let solid = g.at(at).is_some_and(|c| c.solid);
(at, if solid { EDGE_SOLID } else { EDGE_OPEN })
})
.collect()
}
#[derive(Clone, Copy, Debug)]
pub struct Placed3 {
pub x: (f32, f32),
pub y: (f32, f32),
pub z: (f32, f32),
pub div: (u32, u32, u32),
}
pub(crate) fn index(v: f32, (lo, hi): (f32, f32), n: u32) -> u32 {
if n == 0 || hi <= lo {
return 0;
}
(((v - lo) / (hi - lo) * n as f32) as i64).clamp(0, n as i64 - 1) as u32
}
type SeamTokens<'a> = (Vec<Option<&'a str>>, Vec<Option<&'a str>>);
pub fn seam<'a>(
a: &'a Subgrid,
a_at: Placed3,
b: &'a Subgrid,
b_at: Placed3,
dir: Dir,
subunit: f32,
) -> Option<SeamTokens<'a>> {
if !(subunit.is_finite() && subunit > 0.0) {
return None;
}
let y = (a_at.y.0.max(b_at.y.0), a_at.y.1.min(b_at.y.1));
if y.1 <= y.0 {
return None;
}
let lateral_is_z = dir == E || dir == W;
let (a_lat, b_lat) = if lateral_is_z {
(a_at.z, b_at.z)
} else {
(a_at.x, b_at.x)
};
let lat = (a_lat.0.max(b_lat.0), a_lat.1.min(b_lat.1));
if lat.1 <= lat.0 {
return None;
}
let last = |n: u32| n.saturating_sub(1);
let (a_face, b_face) = match dir {
E => (last(a_at.div.0), 0),
W => (0, last(b_at.div.0)),
N => (0, last(b_at.div.2)),
S => (last(a_at.div.2), 0),
_ => return None,
};
let steps = |span: (f32, f32)| (((span.1 - span.0) / subunit).round() as u32).max(1);
let (n_lat, n_y) = (steps(lat), steps(y));
let mut left = Vec::with_capacity((n_lat * n_y) as usize);
let mut right = Vec::with_capacity((n_lat * n_y) as usize);
for iy in 0..n_y {
let wy = y.0 + (iy as f32 + 0.5) * (y.1 - y.0) / n_y as f32;
for il in 0..n_lat {
let wl = lat.0 + (il as f32 + 0.5) * (lat.1 - lat.0) / n_lat as f32;
let cell = |at: Placed3, face: u32| -> (u32, u32, u32) {
let ay = index(wy, at.y, at.div.1);
if lateral_is_z {
(face, ay, index(wl, at.z, at.div.2))
} else {
(index(wl, at.x, at.div.0), ay, face)
}
};
left.push(a.at(cell(a_at, a_face)).and_then(|c| c.edge.as_deref()));
right.push(b.at(cell(b_at, b_face)).and_then(|c| c.edge.as_deref()));
}
}
Some((left, right))
}
#[derive(Clone, Debug, PartialEq)]
pub struct Fault {
pub a: String,
pub b: String,
pub dir: Dir,
pub a_face: Vec<Option<String>>,
pub b_face: Vec<Option<String>>,
pub message: String,
}
pub fn dir_name(dir: Dir) -> &'static str {
match dir {
N => "N",
E => "E",
S => "S",
W => "W",
_ => "?",
}
}
pub fn faults(map: &Map, library: &Library, divisions: u32) -> Vec<Fault> {
let mut out: Vec<Fault> = Vec::new();
let declared = |g: &Option<Subgrid>| {
g.as_ref()
.is_some_and(|g| g.cells.iter().any(|c| c.edge.is_some()))
};
let any_declared = library.descriptors.iter().any(|d| declared(&d.subgrid))
|| map
.placements
.iter()
.any(|p| p.patch.as_ref().is_some_and(|d| declared(&d.subgrid)));
if !any_declared {
return out;
}
let subunit = crate::grid::SNAP / divisions.max(1) as f32;
struct Tile<'a> {
id: &'a str,
x: (f32, f32),
y: (f32, f32),
z: (f32, f32),
turned: Result<(Subgrid, (u32, u32, u32)), String>,
declares: bool,
}
let Ok(ys) = crate::stack::resolve_y(map, library) else {
return out;
};
let mut placed: Vec<Tile> = Vec::new();
for (i, p) in map.placements.iter().enumerate() {
let Some(base) = library.get(&p.descriptor) else {
continue;
};
let patched;
let authored: &crate::descriptor::Descriptor = match &p.patch {
Some(patch) => {
patched = base.patched_with(patch);
&patched
}
None => base,
};
let Ok(div) = crate::descriptor::divisions(authored, divisions) else {
continue;
};
let grid = authored.subgrid.clone().unwrap_or_default();
let declares = grid.cells.iter().any(|c| c.edge.is_some());
let Some((fw, fd)) = crate::descriptor::placed_footprint(authored) else {
continue;
};
let turn = quarter_turns(&p.id, p.yaw);
let turned = match &turn {
Ok(q) => Ok((grid.rotated(*q, div), crate::descriptor::rotate_div(div, *q))),
Err(message) => Err(message.clone()),
};
let (fw, fd) = match &turn {
Ok(q) if q % 2 == 1 => (fd, fw),
Ok(_) => (fw, fd),
Err(_) => (fw, fd),
};
let base = ys.get(i).copied().unwrap_or(map.origin.1);
let h = crate::descriptor::placed_height(authored).unwrap_or(0.0);
placed.push(Tile {
id: p.id.as_str(),
x: (p.at.0 - fw * 0.5, p.at.0 + fw * 0.5),
y: (base, base + h),
z: (p.at.1 - fd * 0.5, p.at.1 + fd * 0.5),
declares,
turned,
});
}
let side = |a: &Tile, b: &Tile| -> Option<Dir> {
let meets = |p: f32, q: f32| (p - q).abs() <= EDGE_EPSILON;
let over = |a: (f32, f32), b: (f32, f32)| a.0.max(b.0) + EDGE_EPSILON < a.1.min(b.1);
if meets(a.x.1, b.x.0) && over(a.z, b.z) {
Some(E)
} else if meets(b.x.1, a.x.0) && over(a.z, b.z) {
Some(W)
} else if meets(a.z.1, b.z.0) && over(a.x, b.x) {
Some(S)
} else if meets(b.z.1, a.z.0) && over(a.x, b.x) {
Some(N)
} else {
None
}
};
let mut yaw_reported: Vec<&str> = Vec::new();
for a in &placed {
for b in &placed {
{
let Some(dir) = side(a, b) else {
continue;
};
if !a.declares && !b.declares {
continue;
}
if a.id >= b.id {
continue;
}
for t in [a, b] {
if let Err(message) = &t.turned
&& !yaw_reported.contains(&t.id)
{
yaw_reported.push(t.id);
out.push(Fault {
a: t.id.to_owned(),
b: String::new(),
dir: N,
a_face: Vec::new(),
b_face: Vec::new(),
message: message.clone(),
});
}
}
let (Ok((a_grid, a_div)), Ok((b_grid, b_div))) = (&a.turned, &b.turned) else {
continue;
};
let a_at = Placed3 { x: a.x, y: a.y, z: a.z, div: *a_div };
let b_at = Placed3 { x: b.x, y: b.y, z: b.z, div: *b_div };
let Some((a_face, b_face)) = seam(a_grid, a_at, b_grid, b_at, dir, subunit) else {
continue;
};
if a_face == b_face {
continue;
}
out.push(Fault {
a: a.id.to_owned(),
b: b.id.to_owned(),
dir,
a_face: a_face.iter().map(|t| t.map(str::to_owned)).collect(),
b_face: b_face.iter().map(|t| t.map(str::to_owned)).collect(),
message: format!(
"`{}` face {} is {} but `{}` presents {}",
a.id,
dir_name(dir),
say(&a_face),
b.id,
say(&b_face)
),
});
}
}
}
out.sort_by(|l, r| (&l.a, &l.b, l.dir).cmp(&(&r.a, &r.b, r.dir)));
out
}
#[derive(Clone, Debug, PartialEq)]
pub struct DeadEnd {
pub piece: String,
pub dir: Dir,
pub face: Vec<Option<String>>,
pub closest: Option<Closest>,
pub message: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Closest {
pub piece: String,
pub turn: u8,
pub misses: Vec<Miss>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Miss {
pub at: usize,
pub want: Option<String>,
pub found: Option<String>,
}
pub fn dead_ends(library: &Library, divisions: u32) -> Vec<DeadEnd> {
use crate::map::Placed;
type Usable<'a> = (&'a crate::descriptor::Descriptor, (f32, f32), (u32, u32, u32));
let usable: Vec<Usable> = library
.descriptors
.iter()
.filter_map(|d| {
let fp = crate::descriptor::placed_footprint(d)?;
let h = crate::descriptor::placed_height(d)?;
if h <= 0.0 {
return None;
}
let div = crate::descriptor::divisions(d, divisions).ok()?;
Some((d, fp, div))
})
.collect();
let mut out = Vec::new();
for (a, (aw, ad), a_div) in &usable {
let a_id = a.id.as_str();
let Some(a_grid) = &a.subgrid else {
continue;
};
for dir in [N, E, S, W] {
let a_face = face(a_grid, dir, *a_div);
if a_face.iter().all(Option::is_none) {
continue;
}
let mut met = false;
let mut best: Option<(usize, &str, u8, usize, Vec<Miss>)> = None;
'trials: for (b, (bw, bd), _) in &usable {
let b_id = b.id.as_str();
for q in 0..4u8 {
let (bw, bd) = if q % 2 == 1 { (*bd, *bw) } else { (*bw, *bd) };
let (normal, half_lat, b_half_lat) = match dir {
E => ((aw + bw) * 0.5, ad * 0.5, bd * 0.5),
W => (-(aw + bw) * 0.5, ad * 0.5, bd * 0.5),
S => ((ad + bd) * 0.5, aw * 0.5, bw * 0.5),
_ => (-(ad + bd) * 0.5, aw * 0.5, bw * 0.5),
};
let near = -half_lat + b_half_lat;
let far = half_lat - b_half_lat;
for (slide, lat) in [near, 0.0, far].into_iter().enumerate() {
let at = if dir == E || dir == W {
(normal, lat)
} else {
(lat, normal)
};
let map = Map {
placements: vec![
Placed {
id: "a".to_owned(),
descriptor: a_id.to_owned(),
..Placed::default()
},
Placed {
id: "b".to_owned(),
descriptor: b_id.to_owned(),
at,
yaw: f32::from(q) * 90.0,
..Placed::default()
},
],
..Map::default()
};
let Some((a_side, b_side)) = trial_seam(&map, library, divisions, dir) else {
continue;
};
if !a_side.iter().any(Option::is_some) {
continue;
}
if faults(&map, library, divisions).is_empty() {
met = true;
break 'trials;
}
let misses: Vec<Miss> = a_side
.iter()
.zip(&b_side)
.enumerate()
.filter(|(_, (w, f))| w != f)
.map(|(at, (w, f))| Miss { at, want: w.clone(), found: f.clone() })
.collect();
let key = (misses.len(), b_id, q, slide);
if best.as_ref().is_none_or(|(n, p, t, s, _)| key < (*n, *p, *t, *s)) {
best = Some((key.0, b_id, q, slide, misses));
}
}
}
}
if !met {
let closest = best.map(|(_, piece, turn, _, misses)| Closest {
piece: piece.to_owned(),
turn,
misses,
});
let near = match &closest {
Some(c) => format!(
"; closest is `{}` turned {}°, off in {} cell{}: {}",
c.piece,
u32::from(c.turn) * 90,
c.misses.len(),
if c.misses.len() == 1 { "" } else { "s" },
c.misses
.iter()
.map(|m| format!(
"wants {} finds {}",
m.want.as_deref().unwrap_or("-"),
m.found.as_deref().unwrap_or("-")
))
.collect::<Vec<_>>()
.join(", ")
),
None => String::new(),
};
out.push(DeadEnd {
piece: a_id.to_owned(),
dir,
face: a_face.iter().map(|t| t.map(str::to_owned)).collect(),
closest,
message: format!(
"`{a_id}` face {} is {} and no piece in the kit meets it at any turn{near}",
dir_name(dir),
say(&a_face)
),
});
}
}
}
out.sort_by(|l, r| (&l.piece, l.dir).cmp(&(&r.piece, r.dir)));
out
}
pub fn ring(library: &Library, divisions: u32) -> Option<Ring> {
let (cells, (w, d)) = block(library, divisions, 2, 2)?;
let corners = [("nw", (0.0, 0.0)), ("ne", (w, 0.0)), ("sw", (0.0, d)), ("se", (w, d))];
let map = Map {
placements: corners
.iter()
.zip(&cells)
.map(|(&(id, at), (piece, q))| block_placed(id, piece, *q, at))
.collect(),
..Map::default()
};
Some(Ring {
pieces: cells.try_into().ok()?,
map,
})
}
pub fn room(library: &Library, divisions: u32) -> Option<Room> {
const IDS: [[&str; 3]; 3] = [["nw", "n", "ne"], ["w", "middle", "e"], ["sw", "s", "se"]];
let (cells, (w, d)) = block(library, divisions, 3, 3)?;
let steps = [0.0, 1.0, 2.0];
let mut placements = Vec::with_capacity(9);
for ((ids, row), z) in IDS.iter().zip(cells.chunks(3)).zip(steps) {
for ((id, (piece, q)), x) in ids.iter().zip(row).zip(steps) {
placements.push(block_placed(id, piece, *q, (x * w, z * d)));
}
}
let [nw, n, ne, west, middle, east, sw, s, se]: [(String, u8); 9] = cells.try_into().ok()?;
Some(Room {
pieces: [[nw, n, ne], [west, middle, east], [sw, s, se]],
map: Map {
placements,
..Map::default()
},
})
}
fn block_placed(id: &str, piece: &str, q: u8, at: (f32, f32)) -> crate::map::Placed {
crate::map::Placed {
id: id.to_owned(),
descriptor: piece.to_owned(),
at,
yaw: f32::from(q) * 90.0,
..crate::map::Placed::default()
}
}
type Block = (Vec<(String, u8)>, (f32, f32));
fn block(library: &Library, divisions: u32, rows: usize, cols: usize) -> Option<Block> {
use std::collections::HashMap;
let mut opts: Vec<(&str, u8, (f32, f32))> = Vec::new();
let mut ids: Vec<&crate::descriptor::Descriptor> = library
.descriptors
.iter()
.filter(|d| d.subgrid.is_some())
.filter(|d| crate::descriptor::divisions(d, divisions).is_ok())
.filter(|d| crate::descriptor::placed_height(d).is_some_and(|h| h > 0.0))
.collect();
ids.sort_by(|l, r| l.id.cmp(&r.id));
for d in ids {
let Some((w, dp)) = crate::descriptor::placed_footprint(d) else {
continue;
};
for q in 0..4u8 {
opts.push((d.id.as_str(), q, if q % 2 == 1 { (dp, w) } else { (w, dp) }));
}
}
let same = |l: (f32, f32), r: (f32, f32)| (l.0 - r.0).abs() < 1e-4 && (l.1 - r.1).abs() < 1e-4;
let mut memo: HashMap<(usize, usize, Dir), bool> = HashMap::new();
let mut meets = |a: usize, b: usize, dir: Dir| -> bool {
*memo.entry((a, b, dir)).or_insert_with(|| {
let ((ap, aq, (w, d)), (bp, bq, _)) = (opts[a], opts[b]);
let at = if dir == E { (w, 0.0) } else { (0.0, d) };
let map = Map {
placements: vec![
block_placed("a", ap, aq, (0.0, 0.0)),
block_placed("b", bp, bq, at),
],
..Map::default()
};
trial_seam(&map, library, divisions, dir)
.is_some_and(|(near, _)| near.iter().any(Option::is_some))
&& faults(&map, library, divisions).is_empty()
})
};
fn consistent(
cand: &mut [Vec<usize>],
cols: usize,
meets: &mut dyn FnMut(usize, usize, Dir) -> bool,
) -> bool {
loop {
let mut changed = false;
for i in 0..cand.len() {
let east = (i + 1 < cand.len() && !(i + 1).is_multiple_of(cols)).then_some((i + 1, E));
let south = (i + cols < cand.len()).then_some((i + cols, S));
for (j, dir) in east.into_iter().chain(south) {
let (near, far) = (cand[i].clone(), cand[j].clone());
let before = (near.len(), far.len());
cand[i].retain(|&x| far.iter().any(|&y| meets(x, y, dir)));
cand[j].retain(|&y| near.iter().any(|&x| meets(x, y, dir)));
changed |= before != (cand[i].len(), cand[j].len());
}
}
if cand.iter().any(Vec::is_empty) {
return false;
}
if !changed {
return true;
}
}
}
fn fits(
cells: &[usize],
i: usize,
o: usize,
cols: usize,
meets: &mut dyn FnMut(usize, usize, Dir) -> bool,
) -> bool {
(i.is_multiple_of(cols) || meets(cells[i - 1], o, E))
&& (i < cols || meets(cells[i - cols], o, S))
}
fn fill(
cells: &mut Vec<usize>,
cand: &[Vec<usize>],
cols: usize,
meets: &mut dyn FnMut(usize, usize, Dir) -> bool,
) -> bool {
let i = cells.len();
if i == cand.len() {
return true;
}
for &o in &cand[i] {
if !fits(cells, i, o, cols, meets) {
continue;
}
cells.push(o);
let next = i + 1;
let east_open = next == cand.len()
|| next.is_multiple_of(cols)
|| cand[next].iter().any(|&y| fits(cells, next, y, cols, meets));
if east_open && fill(cells, cand, cols, meets) {
return true;
}
cells.pop();
}
false
}
type Candidates = Option<Vec<Vec<usize>>>;
let mut pruned: Vec<((f32, f32), Candidates)> = Vec::new();
for first in 0..opts.len() {
let fp = opts[first].2;
let k = match pruned.iter().position(|(f, _)| same(*f, fp)) {
Some(k) => k,
None => {
let fits: Vec<usize> = (0..opts.len()).filter(|&o| same(opts[o].2, fp)).collect();
let mut cand = vec![fits; rows * cols];
let ok = consistent(&mut cand, cols, &mut meets);
pruned.push((fp, ok.then_some(cand)));
pruned.len() - 1
}
};
let Some(cand) = &pruned[k].1 else {
continue;
};
if !cand[0].contains(&first) {
continue;
}
let mut cells = vec![first];
if fill(&mut cells, cand, cols, &mut meets) {
let picked = cells
.iter()
.map(|&o| (opts[o].0.to_owned(), opts[o].1))
.collect();
return Some((picked, fp));
}
}
None
}
#[derive(Clone, Debug, PartialEq)]
pub struct Ring {
pub pieces: [(String, u8); 4],
pub map: Map,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Room {
pub pieces: [[(String, u8); 3]; 3],
pub map: Map,
}
type OwnedSeam = (Vec<Option<String>>, Vec<Option<String>>);
fn trial_seam(map: &Map, library: &Library, divisions: u32, dir: Dir) -> Option<OwnedSeam> {
let (pa, pb) = (map.placements.first()?, map.placements.get(1)?);
let tile = |p: &crate::map::Placed| -> Option<(Subgrid, Placed3)> {
let d = library.get(&p.descriptor)?;
let div = crate::descriptor::divisions(d, divisions).ok()?;
let (fw, fd) = crate::descriptor::placed_footprint(d)?;
let q = quarter_turns(&p.id, p.yaw).ok()?;
let (fw, fd) = if q % 2 == 1 { (fd, fw) } else { (fw, fd) };
let h = crate::descriptor::placed_height(d)?;
let grid = d.subgrid.clone().unwrap_or_default().rotated(q, div);
Some((
grid,
Placed3 {
x: (p.at.0 - fw * 0.5, p.at.0 + fw * 0.5),
y: (0.0, h),
z: (p.at.1 - fd * 0.5, p.at.1 + fd * 0.5),
div: crate::descriptor::rotate_div(div, q),
},
))
};
let ((ag, a_at), (bg, b_at)) = (tile(pa)?, tile(pb)?);
let subunit = crate::grid::SNAP / divisions.max(1) as f32;
let (a_side, b_side) = seam(&ag, a_at, &bg, b_at, dir, subunit)?;
let own = |s: Vec<Option<&str>>| s.into_iter().map(|t| t.map(str::to_owned)).collect();
Some((own(a_side), own(b_side)))
}
fn say(face: &[Option<&str>]) -> String {
if face.is_empty() {
return "no face".to_owned();
}
let parts: Vec<&str> = face.iter().map(|t| t.unwrap_or("-")).collect();
format!("[{}]", parts.join(" "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::{Descriptor, SubCell};
use crate::map::{Map, Placed};
fn grid(cells: &[((u32, u32, u32), &str)]) -> Subgrid {
Subgrid {
cells: cells
.iter()
.map(|(at, token)| SubCell {
at: *at,
edge: Some((*token).to_owned()),
..SubCell::default()
})
.collect(),
}
}
fn solid_grid(cells: &[(u32, u32, u32)]) -> Subgrid {
Subgrid {
cells: cells
.iter()
.map(|at| SubCell {
at: *at,
solid: true,
..SubCell::default()
})
.collect(),
}
}
#[test]
fn a_derived_edge_reads_the_boundary_the_mesh_actually_fills() {
let div = (3, 1, 3);
let wall = solid_grid(&[(0, 0, 0), (1, 0, 0), (2, 0, 0)]);
let derived = derive_edges(&wall, div);
let token_at = |x, y, z| {
derived
.iter()
.find(|(at, _)| *at == (x, y, z))
.map(|(_, t)| *t)
};
assert_eq!(token_at(0, 0, 0), Some(EDGE_SOLID));
assert_eq!(token_at(1, 0, 0), Some(EDGE_SOLID));
assert_eq!(token_at(2, 0, 0), Some(EDGE_SOLID));
assert_eq!(token_at(0, 0, 2), Some(EDGE_OPEN));
assert_eq!(token_at(2, 0, 2), Some(EDGE_OPEN));
assert_eq!(token_at(1, 0, 1), None, "the interior cell must not be proposed");
}
#[test]
fn a_derivation_proposes_each_boundary_cell_exactly_once() {
let div = (3, 2, 3);
let derived = derive_edges(&solid_grid(&[]), div);
let cells: Vec<(u32, u32, u32)> = derived.iter().map(|(at, _)| *at).collect();
let mut sorted = cells.clone();
sorted.sort_unstable();
assert_eq!(cells, sorted, "ascending, so two machines agree");
let mut deduped = sorted.clone();
deduped.dedup();
assert_eq!(cells, deduped, "a corner belongs to two faces and appears once");
assert_eq!(cells.len(), 16, "every boundary cell of both layers: {cells:?}");
assert!(derived.iter().all(|(_, t)| *t == EDGE_OPEN));
}
#[test]
fn a_degenerate_lattice_has_nothing_to_derive() {
assert!(derive_edges(&solid_grid(&[]), (0, 1, 1)).is_empty());
assert!(derive_edges(&solid_grid(&[]), (1, 1, 0)).is_empty());
}
#[test]
fn what_is_derived_is_what_the_matcher_reads() {
let div = (2, 1, 2);
let mut g = solid_grid(&[(0, 0, 0), (1, 0, 0)]);
for (at, token) in derive_edges(&g, div) {
match g.cells.iter_mut().find(|c| c.at == at) {
Some(c) => c.edge = Some(token.to_owned()),
None => g.cells.push(SubCell {
at,
edge: Some(token.to_owned()),
..SubCell::default()
}),
}
}
assert_eq!(
face(&g, N, div),
vec![Some(EDGE_SOLID), Some(EDGE_SOLID)],
"the filled row reads as solid from the north"
);
assert_eq!(
face(&g, S, div),
vec![Some(EDGE_OPEN), Some(EDGE_OPEN)],
"and the empty row reads as open from the south"
);
}
#[test]
fn rotating_four_times_is_the_identity() {
let div = (3, 2, 4);
let g = grid(&[((0, 0, 0), "a"), ((2, 1, 3), "b"), ((1, 0, 2), "c")]);
let mut turned = g.clone();
let mut turned_div = div;
for _ in 0..4 {
turned = turned.rotated(1, turned_div);
turned_div = crate::descriptor::rotate_div(turned_div, 1);
}
assert_eq!(turned_div, div);
let mut want = g.cells.clone();
let mut got = turned.cells.clone();
want.sort_by_key(|c| c.at);
got.sort_by_key(|c| c.at);
assert_eq!(got, want);
}
#[test]
fn a_quarter_turn_moves_the_east_face_to_north() {
let div = (2, 1, 3);
let g = grid(&[((1, 0, 0), "seam"), ((1, 0, 1), "seam"), ((1, 0, 2), "seam")]);
let east = face(&g, E, div);
assert_eq!(east, vec![Some("seam"), Some("seam"), Some("seam")]);
let turned = g.rotated(1, div);
let turned_div = crate::descriptor::rotate_div(div, 1);
assert_eq!(turned_div, (3, 1, 2), "a quarter turn swaps the x and z divisions");
assert_eq!(face(&turned, N, turned_div), east, "the east face is now the north face");
}
#[test]
fn a_yaw_that_is_not_a_quarter_turn_is_refused_with_the_angle() {
assert_eq!(quarter_turns("a", 0.0), Ok(0));
assert_eq!(quarter_turns("a", 90.0), Ok(1));
assert_eq!(quarter_turns("a", 270.0), Ok(3));
assert_eq!(quarter_turns("a", -90.0), Ok(3));
let e = quarter_turns("chair_a", 15.0).unwrap_err();
assert!(e.contains("chair_a") && e.contains("15"), "{e}");
}
fn map_with(placements: Vec<Placed>) -> Map {
Map {
placements,
..Map::default()
}
}
fn library_with(entries: &[(&str, Subgrid)]) -> Library {
Library {
descriptors: entries
.iter()
.map(|(id, g)| Descriptor {
id: (*id).to_owned(),
extent: crate::descriptor::Extent {
footprint: Some((crate::grid::SNAP, crate::grid::SNAP)),
height: Some(crate::grid::SNAP),
},
subgrid: Some(g.clone()),
..Descriptor::default()
})
.collect(),
..Library::default()
}
}
const ONE_PER_TILE: u32 = 1;
fn placed(id: &str, descriptor: &str, at: (f32, f32), yaw: f32) -> Placed {
Placed {
id: id.to_owned(),
descriptor: descriptor.to_owned(),
at,
yaw,
..Placed::default()
}
}
fn sized_library(entries: &[(&str, (f32, f32), Subgrid)]) -> Library {
Library {
descriptors: entries
.iter()
.map(|(id, fp, g)| Descriptor {
id: (*id).to_owned(),
extent: crate::descriptor::Extent {
footprint: Some(*fp),
height: Some(crate::grid::SNAP),
},
subgrid: Some(g.clone()),
..Descriptor::default()
})
.collect(),
..Library::default()
}
}
#[test]
fn an_unauthored_map_short_circuits_however_large_it_is() {
let lib = sized_library(&[("crate_a", (0.5, 0.5), Subgrid::default())]);
let many: Vec<Placed> = (0..4000)
.map(|i| {
placed(
&format!("p{i}"),
"crate_a",
((i % 64) as f32 * 0.5, (i / 64) as f32 * 0.5),
0.0,
)
})
.collect();
assert_eq!(
faults(&map_with(many), &lib, ONE_PER_TILE),
Vec::new()
);
}
#[test]
fn a_single_declared_token_re_enables_the_whole_check() {
let lib = sized_library(&[
("wall", (0.5, 0.5), grid(&[((0, 0, 0), "stone")])),
("gap", (0.5, 0.5), Subgrid::default()),
]);
let map = map_with(vec![
placed("g1", "gap", (0.25, 0.25), 0.0),
placed("w1", "wall", (0.75, 0.25), 0.0),
]);
assert_eq!(faults(&map, &lib, ONE_PER_TILE).len(), 1);
}
#[test]
fn two_wide_pieces_side_by_side_are_compared() {
let lib = sized_library(&[
("wall", (3.0, 0.5), grid(&[((5, 0, 0), "stone")])),
("gap", (3.0, 0.5), Subgrid::default()),
]);
let map = map_with(vec![
placed("w1", "wall", (1.5, 0.25), 0.0),
placed("g1", "gap", (4.5, 0.25), 0.0),
]);
let found = faults(&map, &lib, ONE_PER_TILE);
assert_eq!(found.len(), 1, "the seam between two touching walls: {found:#?}");
assert_eq!((found[0].a.as_str(), found[0].b.as_str()), ("g1", "w1"));
}
#[test]
fn pieces_with_a_gap_between_them_are_not_compared() {
let lib = sized_library(&[
("wall", (0.5, 0.5), grid(&[((0, 0, 0), "stone")])),
("gap", (0.5, 0.5), Subgrid::default()),
]);
let map = map_with(vec![
placed("w1", "wall", (0.25, 0.25), 0.0),
placed("g1", "gap", (1.25, 0.25), 0.0),
]);
assert_eq!(faults(&map, &lib, ONE_PER_TILE), Vec::new());
}
#[test]
fn an_odd_map_bound_does_not_unpair_every_seam() {
let lib = library_with(&[
("wall", grid(&[((0, 0, 0), "stone")])),
("gap", Subgrid::default()),
]);
let touching = vec![
placed("w1", "wall", (0.5, 0.5), 0.0),
placed("g1", "gap", (1.0, 0.5), 0.0),
];
for width in [33.0, 7.0, 1.0, 12.5] {
let map = Map {
bounds: (width, 4.0, width),
placements: touching.clone(),
..Map::default()
};
let found = faults(&map, &lib, ONE_PER_TILE);
assert_eq!(
found.len(),
1,
"a {width} m map must find the same one seam: {found:#?}"
);
}
}
#[test]
fn every_row_of_a_face_is_read_exactly_once() {
let rows = |odd: &str| {
let mut cells: Vec<_> = (0..5)
.map(|y| SubCell {
at: (0, y, 0),
edge: Some("wall".to_owned()),
..SubCell::default()
})
.collect();
cells[2].edge = Some(odd.to_owned());
Subgrid { cells }
};
let wall = |id: &str, g: Subgrid| Descriptor {
id: id.to_owned(),
extent: crate::descriptor::Extent {
footprint: Some((crate::grid::SNAP, crate::grid::SNAP)),
height: Some(2.4),
},
subgrid: Some(g),
..Descriptor::default()
};
let lib = Library {
descriptors: vec![wall("plain", rows("wall")), wall("odd", rows("glass"))],
..Library::default()
};
let map = map_with(vec![
placed("a", "plain", (0.5, 0.5), 0.0),
placed("b", "odd", (1.0, 0.5), 0.0),
]);
let found = faults(&map, &lib, ONE_PER_TILE);
assert_eq!(found.len(), 1, "row 2 disagrees: {found:#?}");
assert_eq!(found[0].a_face.len(), 5, "five rows, five samples: {found:#?}");
assert!(found[0].message.contains("glass"), "{}", found[0].message);
}
#[test]
fn pieces_touching_only_at_a_corner_are_not_compared() {
let lib = sized_library(&[
("wall", (0.5, 0.5), grid(&[((0, 0, 0), "stone")])),
("gap", (0.5, 0.5), Subgrid::default()),
]);
let map = map_with(vec![
placed("w1", "wall", (0.25, 0.25), 0.0),
placed("g1", "gap", (0.75, 0.75), 0.0),
]);
assert_eq!(faults(&map, &lib, ONE_PER_TILE), Vec::new());
}
#[test]
fn a_turned_piece_abuts_on_the_footprint_it_actually_has() {
let lib = sized_library(&[
("wall", (3.0, 0.5), grid(&[((0, 0, 0), "stone")])),
("gap", (0.5, 0.5), Subgrid::default()),
]);
let map = map_with(vec![
placed("w1", "wall", (0.25, 1.5), 90.0),
placed("g1", "gap", (0.75, 2.75), 0.0),
]);
let found = faults(&map, &lib, ONE_PER_TILE);
assert_eq!(found.len(), 1, "the turned wall's long face: {found:#?}");
let elsewhere = map_with(vec![
placed("w1", "wall", (0.25, 1.5), 90.0),
placed("g1", "gap", (0.75, 0.25), 0.0),
]);
assert_eq!(faults(&elsewhere, &lib, ONE_PER_TILE), Vec::new());
}
#[test]
fn a_map_of_unauthored_tiles_has_no_faults() {
let lib = library_with(&[("crate_a", Subgrid::default())]);
let map = map_with(vec![
placed("one", "crate_a", (0.5, 0.5), 0.0),
placed("two", "crate_a", (1.0, 0.5), 0.0),
]);
assert_eq!(faults(&map, &lib, ONE_PER_TILE), Vec::new());
}
#[test]
fn a_mismatched_pair_is_reported_and_names_both_faces() {
let lib = library_with(&[
("wall", grid(&[((0, 0, 0), "stone")])),
("gap", Subgrid::default()),
]);
let map = map_with(vec![
placed("w1", "wall", (0.5, 0.5), 0.0),
placed("g1", "gap", (1.0, 0.5), 0.0),
]);
let found = faults(&map, &lib, ONE_PER_TILE);
assert_eq!(found.len(), 1, "one seam is one fault, not two: {found:#?}");
assert!(found[0].message.contains("stone"), "{}", found[0].message);
assert_eq!((found[0].a.as_str(), found[0].b.as_str()), ("g1", "w1"));
}
#[test]
fn fault_order_does_not_depend_on_placement_order() {
let lib = library_with(&[
("wall", grid(&[((0, 0, 0), "stone")])),
("gap", Subgrid::default()),
]);
let forward = faults(
&map_with(vec![
placed("aaa", "wall", (0.5, 0.5), 0.0),
placed("bbb", "gap", (1.0, 0.5), 0.0),
]),
&lib,
ONE_PER_TILE,
);
let backward = faults(
&map_with(vec![
placed("bbb", "gap", (1.0, 0.5), 0.0),
placed("aaa", "wall", (0.5, 0.5), 0.0),
]),
&lib,
ONE_PER_TILE,
);
assert_eq!(forward, backward);
}
#[test]
fn an_off_square_yaw_is_a_fault_not_a_dead_check() {
let lib = library_with(&[("wall", grid(&[((0, 0, 0), "stone")]))]);
let map = map_with(vec![
placed("tilted", "wall", (0.5, 0.5), 15.0),
placed("square", "wall", (1.0, 0.5), 0.0),
]);
let found = faults(&map, &lib, ONE_PER_TILE);
assert_eq!(
found.iter().filter(|f| f.a == "tilted" && f.b.is_empty()).count(),
1,
"one sentence per tilted piece, not one per neighbour: {found:#?}"
);
assert!(found.iter().any(|f| f.message.contains("tilted")));
}
#[test]
fn an_undeclared_tile_at_an_odd_yaw_is_not_a_fault() {
let lib = library_with(&[("armchair", Subgrid::default()), ("table", Subgrid::default())]);
let map = map_with(vec![
placed("chair", "armchair", (0.5, 0.5), 240.0),
placed("table", "table", (1.0, 0.5), 0.0),
]);
assert_eq!(faults(&map, &lib, ONE_PER_TILE), Vec::new());
}
#[test]
fn an_odd_yaw_matters_as_soon_as_a_neighbour_declares_an_edge() {
let lib = library_with(&[
("armchair", Subgrid::default()),
("wall", grid(&[((0, 0, 0), "stone")])),
]);
let map = map_with(vec![
placed("chair", "armchair", (0.5, 0.5), 240.0),
placed("w1", "wall", (1.0, 0.5), 0.0),
]);
let found = faults(&map, &lib, ONE_PER_TILE);
assert!(
found.iter().any(|f| f.a == "chair" && f.message.contains("240")),
"{found:#?}"
);
}
}
#[cfg(test)]
mod seam_tests {
use super::*;
use crate::descriptor::{Descriptor, Extent, SubCell};
use crate::map::Placed;
fn all_wall(div: (u32, u32, u32)) -> Subgrid {
let (dx, dy, dz) = div;
Subgrid {
cells: (0..dx)
.flat_map(|x| (0..dy).flat_map(move |y| (0..dz).map(move |z| (x, y, z))))
.map(|at| SubCell {
at,
edge: Some("wall".into()),
..SubCell::default()
})
.collect(),
}
}
fn piece(id: &str, w: f32, h: f32, d: f32, div: (u32, u32, u32)) -> Descriptor {
Descriptor {
id: id.into(),
extent: Extent {
footprint: Some((w, d)),
height: Some(h),
},
mount: Some(crate::descriptor::Mount::OnFloor),
subgrid: Some(all_wall(div)),
..Descriptor::default()
}
}
fn map_of(ps: Vec<Placed>) -> Map {
Map {
name: "t".into(),
placements: ps,
..Map::default()
}
}
fn at(id: &str, d: &str, x: f32, z: f32) -> Placed {
Placed {
id: id.into(),
descriptor: d.into(),
at: (x, z),
yaw: 0.0,
..Placed::default()
}
}
#[test]
fn a_wall_and_a_shorter_doorway_agree_over_the_rows_they_share() {
let lib = Library {
descriptors: vec![
piece("wall", 0.1, 2.40, 1.0, (1, 5, 2)),
piece("door", 0.1, 2.01, 1.0, (1, 4, 2)),
],
..Library::default()
};
let map = map_of(vec![at("w1", "wall", 0.25, 0.5), at("d1", "door", 0.25, 1.5)]);
let found = faults(&map, &lib, 1);
assert!(found.is_empty(), "{found:#?}");
}
#[test]
fn a_header_agrees_with_the_wall_over_the_band_it_occupies() {
let mut header = piece("header", 0.1, 0.40, 1.0, (1, 1, 2));
header.mount = Some(crate::descriptor::Mount::OnWall { height: 2.0 });
let lib = Library {
descriptors: vec![piece("wall", 0.1, 2.40, 1.0, (1, 5, 2)), header],
..Library::default()
};
let map = map_of(vec![at("w1", "wall", 0.25, 0.5), at("h1", "header", 0.25, 1.5)]);
let found = faults(&map, &lib, 1);
assert!(found.is_empty(), "{found:#?}");
}
#[test]
fn a_different_token_on_the_shared_rows_is_still_reported() {
let mut door = piece("door", 0.1, 2.01, 1.0, (1, 4, 2));
door.subgrid = Some(Subgrid {
cells: (0..4)
.flat_map(|y| [(0, y, 0), (0, y, 1)])
.map(|at| SubCell {
at,
edge: Some("glass".into()),
..SubCell::default()
})
.collect(),
});
let lib = Library {
descriptors: vec![piece("wall", 0.1, 2.40, 1.0, (1, 5, 2)), door],
..Library::default()
};
let map = map_of(vec![at("w1", "wall", 0.25, 0.5), at("d1", "door", 0.25, 1.5)]);
let found = faults(&map, &lib, 1);
assert_eq!(found.len(), 1, "{found:#?}");
assert!(found[0].message.contains("glass"), "{}", found[0].message);
}
#[test]
fn a_wider_corner_agrees_with_its_wall_over_the_width_they_share() {
let lib = Library {
descriptors: vec![
piece("wall", 0.5, 2.40, 1.0, (1, 5, 2)),
piece("corner", 1.0, 2.40, 1.0, (2, 5, 2)),
],
..Library::default()
};
let map = map_of(vec![at("w1", "wall", 0.25, 0.5), at("c1", "corner", 0.5, 1.5)]);
let found = faults(&map, &lib, 1);
assert!(found.is_empty(), "{found:#?}");
}
#[test]
fn pieces_that_do_not_overlap_in_height_are_not_a_seam() {
let mut high = piece("high", 0.1, 0.40, 1.0, (1, 1, 2));
high.mount = Some(crate::descriptor::Mount::OnWall { height: 3.0 });
high.subgrid = Some(Subgrid {
cells: vec![SubCell {
at: (0, 0, 0),
edge: Some("glass".into()),
..SubCell::default()
}],
});
let lib = Library {
descriptors: vec![piece("wall", 0.1, 2.40, 1.0, (1, 5, 2)), high],
..Library::default()
};
let map = map_of(vec![at("w1", "wall", 0.25, 0.5), at("h1", "high", 0.25, 1.5)]);
assert_eq!(faults(&map, &lib, 1), Vec::new());
}
}