geometry.rs
11.1 kB · rust · 299 lines
1use super::models::Cell3d;2use crate::dim::geometry;3use crate::dim::models::{dtype_for, Cell2d};4use mrlycore::cell::remap;5use mrlycore::errors::{value_error, Result};6use mrlycore::tensor::Tensor;7use std::sync::OnceLock;89pub use crate::dim::geometry::{magic, mosaic, perforate};1011/// Returns the 24 rotation triples that reach each distinct cube orientation.12pub fn orientations() -> &'static Vec<(usize, usize, usize)> {13 static TABLE: OnceLock<Vec<(usize, usize, usize)>> = OnceLock::new();14 TABLE.get_or_init(|| {15 let mut probe = Tensor::new(vec![3, 3, 3]);16 for (flat, item) in probe.bytes_mut().iter_mut().enumerate() {17 *item = flat as u8;18 }19 let mut seen: Vec<Vec<u8>> = Vec::new();20 let mut table = Vec::new();21 for a in 0..4 {22 for b in 0..4 {23 for c in 0..4 {24 let image = probe.rot90(a, (1, 2)).rot90(b, (0, 2)).rot90(c, (0, 1));25 if !seen.contains(&image.bytes().to_vec()) {26 seen.push(image.bytes().to_vec());27 table.push((a, b, c));28 }29 }30 }31 }32 table33 })34}3536/// Merges the cells into one cube arranged width by height by depth.37pub fn merge(cells: &[Cell3d], width: usize, height: usize, depth: usize) -> Result<Cell3d> {38 geometry::merge_reps(cells, &[height, width, depth])39}4041/// Orients a copy of the cell by each mask value and merges them in the mask's shape.42pub fn special(mask: &Tensor, cell: &Cell3d) -> Result<Cell3d> {43 if mask.shape.len() != 3 {44 return value_error("special mask must be 3d.");45 }46 if mask.bytes().iter().any(|&v| v > 23) {47 return value_error("Invalid orientation value. Must be 0..23.");48 }49 let oriented: Result<Vec<Cell3d>> = mask50 .bytes()51 .iter()52 .map(|&k| cell.clone().orient(k as usize))53 .collect();54 let oriented = oriented?;55 merge(&oriented, mask.shape[1], mask.shape[0], mask.shape[2])56}5758fn slice_map(cube: &[usize], axis: usize, index: usize) -> (Vec<usize>, Vec<usize>) {59 let strides = [cube[1] * cube[2], cube[2], 1];60 let kept: Vec<usize> = (0..3).filter(|&a| a != axis).collect();61 let shape = vec![cube[kept[0]], cube[kept[1]]];62 let mut map = Vec::with_capacity(shape[0] * shape[1]);63 for row in 0..shape[0] {64 for col in 0..shape[1] {65 let mut at = [0usize; 3];66 at[axis] = index;67 at[kept[0]] = row;68 at[kept[1]] = col;69 map.push(at[0] * strides[0] + at[1] * strides[1] + at[2]);70 }71 }72 (shape, map)73}7475/// Takes the flat cell left when one axis of the cube is fixed at an index, colors and tags with it.76///77/// ```78/// let sponge = mrlymath::three::carpet(3, 2).unwrap();79/// let front = mrlymath::three::slice(&sponge, 2, 0).unwrap();80/// assert_eq!(front, mrlymath::two::carpet(3, 2).unwrap());81/// ```82pub fn slice(cell: &Cell3d, axis: usize, index: usize) -> Result<Cell2d> {83 if axis > 2 {84 return value_error("slice axis is past the cube's rank.");85 }86 if index >= cell.types().shape[axis] {87 return value_error("slice index is past the axis.");88 }89 let (shape, map) = slice_map(&cell.types().shape, axis, index);90 Ok(Cell2d {91 cell: remap(&cell.cell, &map, &shape),92 })93}9495// EXTRUDE9697fn lift_map(shape: &[usize], axis: usize, depth: usize) -> (Vec<usize>, Vec<usize>) {98 let mut lifted = shape.to_vec();99 lifted.insert(axis, depth);100 let strides = [lifted[1] * lifted[2], lifted[2], 1];101 let (height, width) = (shape[0], shape[1]);102 let mut map = vec![0usize; lifted.iter().product()];103 for plane in 0..depth {104 for y in 0..height {105 for x in 0..width {106 let mut at = vec![y, x];107 at.insert(axis, plane);108 map[at[0] * strides[0] + at[1] * strides[1] + at[2]] = y * width + x;109 }110 }111 }112 (lifted, map)113}114115/// Lifts a flat cell into a cube by repeating it depth times along a new axis, colors and tags with it.116///117/// This is the inverse of slice: every slice of the lift on that axis is the flat cell again.118///119/// ```120/// let flat = mrlymath::two::carpet(3, 2).unwrap();121/// let cube = mrlymath::three::extrude(&flat, 2, 4).unwrap();122/// assert_eq!(cube.depth(), 4);123/// assert_eq!(mrlymath::three::slice(&cube, 2, 3).unwrap(), flat);124/// ```125pub fn extrude(cell: &Cell2d, axis: usize, depth: usize) -> Result<Cell3d> {126 if axis > 2 {127 return value_error("extrude axis must be 0, 1 or 2.");128 }129 if depth == 0 {130 return value_error("extrude depth must be at least 1.");131 }132 let (shape, map) = lift_map(&cell.types().shape, axis, depth);133 Ok(Cell3d {134 cell: remap(&cell.cell, &map, &shape),135 })136}137138// LAYERS139140/// Tags every site with its Manhattan distance from the cube's center, the diamond shells.141///142/// The cell's own layers count Chebyshev shells, which are boxes; these are octahedra.143pub fn manhattan_layers(mut cell: Cell3d) -> Cell3d {144 let shape = cell.types().shape.clone();145 let strides = [shape[1] * shape[2], shape[2], 1];146 let mut rings = vec![0i64; shape.iter().product()];147 for i in 0..shape[0] {148 for j in 0..shape[1] {149 for k in 0..shape[2] {150 let at = [i, j, k];151 let reach: f64 = (0..3)152 .map(|axis| (at[axis] as f64 - (shape[axis] as f64 - 1.0) / 2.0).abs())153 .sum();154 rings[i * strides[0] + j * strides[1] + k] = reach.floor() as i64;155 }156 }157 }158 let peak = rings.iter().copied().max().unwrap_or(0);159 let mut tags = Tensor::typed(shape, dtype_for(peak));160 for (flat, &ring) in rings.iter().enumerate() {161 tags.put(flat, ring);162 }163 cell.cell.tags = Some(tags);164 cell165}166167#[cfg(test)]168mod tests {169 use super::*;170 use crate::three::designs;171 use crate::two;172 #[test]173 fn exactly_24_orientations() {174 let table = orientations();175 assert_eq!(table.len(), 24);176 assert_eq!(table[0], (0, 0, 0));177 }178 #[test]179 fn orientations_preserve_sum_and_shape() {180 let c = designs::carpet(3, 1).unwrap();181 for i in 0..24 {182 let o = c.clone().orient(i).unwrap();183 assert_eq!(o.types().sum(), c.types().sum());184 assert_eq!(o.types().shape, c.types().shape);185 }186 assert!(c.clone().orient(24).is_err());187 }188 #[test]189 fn orientations_are_distinct_on_chiral_design() {190 let tree = designs::xtree(3, 1).unwrap();191 let images: Vec<Vec<u8>> = (0..24)192 .map(|i| tree.clone().orient(i).unwrap().types().bytes().to_vec())193 .collect();194 let mut unique = images.clone();195 unique.sort();196 unique.dedup();197 assert!(unique.len() >= 3);198 }199 #[test]200 fn special_identity_is_tile() {201 let c = designs::carpet(3, 1).unwrap();202 let mask = Tensor::new(vec![2, 2, 2]);203 let s = special(&mask, &c).unwrap();204 assert_eq!(s, c.clone().tile(2, 2, 2));205 }206 #[test]207 fn only_the_carpet_and_two_trees_face_a_flat_name() {208 for n in [3, 5] {209 for level in [1, 2] {210 let carpet = slice(&designs::carpet(n, level).unwrap(), 2, 0).unwrap();211 let net = slice(&designs::net(n, level).unwrap(), 2, 0).unwrap();212 let xtree = slice(&designs::xtree(n, level).unwrap(), 2, 0).unwrap();213 let ytree = slice(&designs::ytree(n, level).unwrap(), 2, 0).unwrap();214 let ztree = slice(&designs::ztree(n, level).unwrap(), 2, 0).unwrap();215 let void = slice(&designs::void(n, level).unwrap(), 2, 0).unwrap();216 assert_eq!(carpet, two::carpet(n, level).unwrap());217 assert_eq!(xtree, two::vtree(n, level).unwrap());218 assert_eq!(ytree, two::htree(n, level).unwrap());219 assert_eq!(void, ztree);220 assert_ne!(net, two::net(n, level).unwrap());221 assert_ne!(void, two::void(n, level).unwrap());222 assert_ne!(ztree, two::htree(n, level).unwrap());223 assert_ne!(ztree, two::vtree(n, level).unwrap());224 }225 }226 }227 #[test]228 fn extrude_undoes_slice_on_every_axis() {229 use mrlycore::cell::mapping;230 use mrlycore::enums::Mode;231 let flat = two::carpet(3, 2)232 .unwrap()233 .layers()234 .paint(&mapping(), Mode::Index);235 for axis in 0..3 {236 let cube = extrude(&flat, axis, 4).unwrap();237 assert_eq!(cube.types().shape[axis], 4);238 for index in 0..4 {239 assert_eq!(slice(&cube, axis, index).unwrap(), flat);240 }241 assert_eq!(cube.types().sum(), 4 * flat.types().sum());242 }243 assert!(extrude(&flat, 3, 2).is_err());244 assert!(extrude(&flat, 0, 0).is_err());245 assert!(slice(&extrude(&flat, 2, 1).unwrap(), 3, 0).is_err());246 assert!(slice(&extrude(&flat, 2, 1).unwrap(), 2, 1).is_err());247 }248 #[test]249 fn extrude_lifts_a_flat_face_of_a_cube_back() {250 let sponge = designs::carpet(3, 2).unwrap();251 let face = slice(&sponge, 2, 0).unwrap();252 let column = extrude(&face, 2, sponge.depth()).unwrap();253 assert_eq!(column.types().shape, sponge.types().shape);254 assert_eq!(slice(&column, 2, 5).unwrap(), face);255 }256 #[test]257 fn extrude_carries_colors_and_tags() {258 use mrlycore::cell::mapping;259 use mrlycore::enums::Mode;260 let flat = two::carpet(3, 1)261 .unwrap()262 .layers()263 .paint(&mapping(), Mode::Type);264 let cube = extrude(&flat, 0, 3).unwrap();265 let colors = cube.cell.colors.as_ref().unwrap();266 let tags = cube.cell.tags.as_ref().unwrap();267 let flat_colors = flat.cell.colors.as_ref().unwrap();268 let flat_tags = flat.cell.tags.as_ref().unwrap();269 assert_eq!(colors.len(), 27);270 assert_eq!(tags.shape, vec![3, 3, 3]);271 for plane in 0..3 {272 for site in 0..9 {273 assert_eq!(colors[plane * 9 + site], flat_colors[site]);274 assert_eq!(tags.at(plane * 9 + site), flat_tags.at(site));275 }276 }277 }278 #[test]279 fn manhattan_layers_are_diamond_shells() {280 let cube = manhattan_layers(designs::ones(3, 1).unwrap());281 let tags = cube.cell.tags.as_ref().unwrap();282 assert_eq!(tags.get(&[1, 1, 1]), 0);283 assert_eq!(tags.get(&[0, 1, 1]), 1);284 assert_eq!(tags.get(&[0, 0, 1]), 2);285 assert_eq!(tags.get(&[0, 0, 0]), 3);286 let boxes = designs::ones(3, 1).unwrap().layers();287 assert_eq!(boxes.cell.tags.as_ref().unwrap().get(&[0, 0, 0]), 1);288 }289 #[test]290 fn manhattan_layers_widen_past_a_byte() {291 use mrlycore::tensor::Dtype;292 let small = manhattan_layers(designs::ones(3, 1).unwrap());293 assert_eq!(small.cell.tags.as_ref().unwrap().dtype(), Dtype::U8);294 let long = manhattan_layers(Cell3d::new(Tensor::full(vec![1, 1, 600], 1)));295 let tags = long.cell.tags.as_ref().unwrap();296 assert_eq!(tags.dtype(), Dtype::U16);297 assert_eq!(tags.at(0), 299);298 }299}