geometry.rs
5.9 kB · rust · 171 lines
1use super::models::Cell2d;2use crate::three::Cell3d;3use mrlycore::cell::{remap, Cell};4use mrlycore::errors::{value_error, Result};5use mrlycore::tensor::{Dtype, Tensor};67pub use crate::dim::geometry::{magic, mosaic, perforate};89fn widest(cells: &[Cell2d]) -> Option<Dtype> {10 cells11 .iter()12 .filter_map(|c| c.cell.tags.as_ref())13 .map(|t| t.dtype())14 .max_by_key(|d| d.max())15}1617fn stacked(cells: &[Cell2d]) -> Cell {18 let inner = cells[0].types().size();19 let shape = vec![cells.len() * inner];20 let mut types = Tensor::typed(shape.clone(), cells[0].types().dtype());21 let mut colors = vec![[0u8; 4]; cells.len() * inner];22 let mut tags = widest(cells).map(|dtype| Tensor::typed(shape, dtype));23 for (block, cell) in cells.iter().enumerate() {24 let base = block * inner;25 for local in 0..inner {26 types.put(base + local, cell.types().at(local));27 if let Some(source) = &cell.cell.colors {28 colors[base + local] = source[local];29 }30 if let (Some(layer), Some(source)) = (tags.as_mut(), &cell.cell.tags) {31 layer.put(base + local, source.at(local));32 }33 }34 }35 Cell {36 types,37 colors: cells38 .iter()39 .any(|c| c.cell.colors.is_some())40 .then_some(colors),41 tags,42 }43}4445/// Merges same-shaped cells into one block of the given width and height in cells, colors and tags kept.46pub fn merge(cells: &[Cell2d], width: usize, height: usize) -> Result<Cell2d> {47 if cells.is_empty() {48 return value_error("Cannot merge an empty list of cells.");49 }50 let count = width * height;51 if cells.len() != count {52 return value_error(format!("Expected {count} cells, got {}", cells.len()));53 }54 let (inner_w, inner_h) = (cells[0].width(), cells[0].height());55 if cells56 .iter()57 .any(|c| c.types().shape != cells[0].types().shape)58 {59 return value_error("All cells in a merge operation must have the same dimensions.");60 }61 let shape = vec![inner_h * height, inner_w * width];62 let inner = inner_w * inner_h;63 let mut map = Vec::with_capacity(shape[0] * shape[1]);64 for y in 0..shape[0] {65 for x in 0..shape[1] {66 let block = (y / inner_h) * width + x / inner_w;67 map.push(block * inner + (y % inner_h) * inner_w + x % inner_w);68 }69 }70 Ok(Cell2d {71 cell: remap(&stacked(cells), &map, &shape),72 })73}7475/// Tiles quarter-turned copies of the cell as the 2d mask directs, or an error at a rotation past 3.76pub fn special(mask: &Tensor, cell: &Cell2d) -> Result<Cell2d> {77 if mask.shape.len() != 2 {78 return value_error("special mask must be 2d.");79 }80 if mask.bytes().iter().any(|&v| v > 3) {81 return value_error("Invalid rotation value. Must be 0, 1, 2, or 3.");82 }83 let rotated: Vec<Cell2d> = mask84 .bytes()85 .iter()86 .map(|&k| cell.clone().rotate(k as usize))87 .collect();88 merge(&rotated, mask.shape[1], mask.shape[0])89}9091/// Lifts the flat cell into a cube one site deep, colors and tags with it.92///93/// ```94/// let flat = mrlymath::two::carpet(3, 2).unwrap();95/// let solid = mrlymath::two::to_3d(&flat);96/// assert_eq!(mrlymath::three::slice(&solid, 2, 0).unwrap(), flat);97/// ```98pub fn to_3d(cell: &Cell2d) -> Cell3d {99 crate::three::extrude(cell, 2, 1).expect("a one-deep lift on the last axis always holds")100}101102#[cfg(test)]103mod tests {104 use super::*;105 use crate::two::designs;106 use mrlycore::cell::mapping;107 use mrlycore::enums::Mode;108 #[test]109 fn special_rotations_preserve_sum() {110 let tree = designs::htree(3, 1).unwrap();111 let mask = Tensor::of(vec![0, 1, 3, 2], vec![2, 2]);112 let s = special(&mask, &tree).unwrap();113 assert_eq!(s.width(), 6);114 assert_eq!(s.types().sum(), 4 * tree.types().sum());115 assert!(special(&Tensor::of(vec![4], vec![1, 1]), &tree).is_err());116 }117 #[test]118 fn special_identity_mask_is_tile() {119 let c = designs::carpet(3, 1).unwrap();120 let mask = Tensor::new(vec![2, 3]);121 let s = special(&mask, &c).unwrap();122 assert_eq!(s, c.clone().tile(3, 2));123 }124 #[test]125 fn merge_carries_colors_and_tags() {126 let painted = designs::carpet(3, 1)127 .unwrap()128 .layers()129 .paint(&mapping(), Mode::Type);130 let plain = designs::void(3, 1).unwrap();131 let block = merge(132 &[painted.clone(), plain, painted.clone(), painted.clone()],133 2,134 2,135 )136 .unwrap();137 assert_eq!(block.width(), 6);138 let colors = block.cell.colors.as_ref().unwrap();139 let source = painted.cell.colors.as_ref().unwrap();140 assert_eq!(colors.len(), 36);141 assert_eq!(colors[0], source[0]);142 assert_eq!(colors[3], [0, 0, 0, 0]);143 assert_eq!(colors[18], source[0]);144 assert_eq!(colors[21], source[0]);145 let tags = block.cell.tags.as_ref().unwrap();146 let seed = painted.cell.tags.as_ref().unwrap();147 assert_eq!(tags.shape, vec![6, 6]);148 assert_eq!(tags.at(7), seed.at(4));149 assert_eq!(tags.at(10), 0);150 }151 #[test]152 fn merge_without_paint_stays_bare() {153 let c = designs::carpet(3, 1).unwrap();154 let block = merge(&[c.clone(), c.clone(), c.clone(), c], 2, 2).unwrap();155 assert!(block.cell.colors.is_none());156 assert!(block.cell.tags.is_none());157 assert!(merge(&[], 1, 1).is_err());158 }159 #[test]160 fn to_3d_is_a_one_deep_lift() {161 let flat = designs::htree(5, 1)162 .unwrap()163 .layers()164 .paint(&mapping(), Mode::Index);165 let solid = to_3d(&flat);166 assert_eq!(solid.types().shape, vec![5, 5, 1]);167 assert_eq!(solid.types().sum(), flat.types().sum());168 assert_eq!(solid.cell.colors, flat.cell.colors);169 assert_eq!(crate::three::slice(&solid, 2, 0).unwrap(), flat);170 }171}