geometry.rs
27.5 kB · rust · 776 lines
1use super::models::Cell6d;2use super::{Orientation, Projection, FILL, GRID, LEFT, RIGHT, UP, VOID};3use crate::three::Cell3d;4use crate::two::Cell2d;5use mrlycore::cell::{remap, Cell};6use mrlycore::errors::{value_error, Result};7use mrlycore::tensor::Tensor;89fn backed(front: &Cell2d, back: &Cell2d, tag: u8) -> Cell {10 let (count, spare) = (front.cell.size(), back.cell.size());11 let mut types = Tensor::typed(vec![count + spare], front.types().dtype());12 for i in 0..count {13 types.put(i, front.types().at(i));14 }15 for i in 0..spare {16 types.put(count + i, back.types().at(i));17 }18 Cell {19 types,20 colors: front.cell.colors.as_ref().map(|colors| {21 let mut out = colors.clone();22 out.resize(count + spare, [0u8; 4]);23 out24 }),25 tags: front.cell.tags.as_ref().map(|tags| {26 let mut out = Tensor::filled(vec![count + spare], tag as i64, tags.dtype());27 for i in 0..count {28 out.put(i, tags.at(i));29 }30 out31 }),32 }33}3435/// Returns whether the cell's three sides are equal.36pub fn is_cube(cell: &Cell3d) -> bool {37 let s = &cell.types().shape;38 s[0] == s[1] && s[1] == s[2]39}4041/// Returns whether the cell's width, height and parity frame a hexagon.42pub fn is_hex(cell: &Cell2d) -> bool {43 let (h, w) = (cell.height(), cell.width());44 if w > h {45 if w.is_multiple_of(2) {46 return false;47 }48 let dx = (3 * (w + 1)) / 4;49 let row_shift = h / 2;50 (dx + row_shift).is_multiple_of(2)51 } else if h > w {52 let dy = (3 * (h + 1)) / 4;53 let row_shift = w / 2;54 (dy + row_shift).is_multiple_of(2)55 } else {56 false57 }58}5960/// Returns the orientation a hexagon's width and height imply, or an error when they are equal.61pub fn orientation(width: usize, height: usize) -> Result<Orientation> {62 if width > height {63 return Ok(Orientation::Horizontal);64 }65 if height > width {66 return Ok(Orientation::Vertical);67 }68 value_error("Cell must be a hexagon.")69}7071/// Builds a hexagon of the given radius, fill inside and void outside.72///73/// ```74/// use mrlymath::six::{blank, Orientation};75/// let hex = blank(2, Orientation::Horizontal, 1, 0);76/// assert_eq!(hex.types().shape, vec![4, 7]);77/// ```78pub fn blank(radius: usize, orient: Orientation, fill: u8, void: u8) -> Cell2d {79 let n = radius;80 let (height, width) = match orient {81 Orientation::Horizontal => (2 * n, 4 * n - 1),82 Orientation::Vertical => {83 let width = 2 * n;84 let mut height = (7 * n - 1) / 2;85 let row_shift = width / 2;86 while !((3 * (height + 1)) / 4 + row_shift).is_multiple_of(2) {87 height += 1;88 }89 (height, width)90 }91 };92 let mut types = Tensor::full(vec![height, width], fill);93 for r in 0..height {94 let p = match orient {95 Orientation::Horizontal => {96 [0isize, n as isize - 1 - r as isize, r as isize - n as isize]97 }98 Orientation::Vertical => [99 0isize,100 n as isize - 1 - r as isize,101 r as isize - (height - n) as isize,102 ],103 }104 .into_iter()105 .max()106 .unwrap() as usize;107 if p > 0 {108 for c in 0..p {109 types.set(&[r, c], void);110 types.set(&[r, width - 1 - c], void);111 }112 }113 }114 Cell2d::new(types)115}116117/// Wraps a hexagonal cell in k rings of the given value, carrying colors and tags along.118pub fn pad(cell: &Cell6d, k: usize, value: u8) -> Result<Cell6d> {119 if k < 1 {120 return Ok(cell.clone());121 }122 let inner = &cell.cell;123 if !is_hex(inner) {124 return value_error("Cell must be a hexagon.");125 }126 let orient = orientation(inner.width(), inner.height())?;127 let n = match orient {128 Orientation::Horizontal => inner.height() / 2,129 Orientation::Vertical => inner.width() / 2,130 };131 let base = blank(n + k, orient, value, GRID);132 let (base_h, base_w) = (base.height(), base.width());133 let (tile_h, tile_w) = (inner.height(), inner.width());134 let y_off = (base_h - tile_h) / 2;135 let x_off = (base_w - tile_w) / 2;136 let mut front = inner.clone();137 for v in front.cell.types.bytes_mut().iter_mut() {138 if *v == GRID {139 *v = value;140 }141 }142 let count = front.cell.size();143 let map: Vec<usize> = (0..base_h * base_w)144 .map(|flat| {145 let (y, x) = (flat / base_w, flat % base_w);146 let inside = y >= y_off && y < y_off + tile_h && x >= x_off && x < x_off + tile_w;147 match inside {148 true => (y - y_off) * tile_w + x - x_off,149 false => count + flat,150 }151 })152 .collect();153 Ok(Cell6d::new(154 Cell2d {155 cell: remap(&backed(&front, &base, value), &map, &[base_h, base_w]),156 },157 cell.projection,158 orient,159 cell.start,160 ))161}162163/// Projects a cube into the isometric hexagon of top, left and right faces.164pub fn iso(cell: &Cell3d) -> Result<Cell6d> {165 if !is_cube(cell) {166 return value_error("Cell must be a cube.");167 }168 let grid = cell.types();169 let n = grid.shape[0];170 let width = 2 * n;171 let height = 4 * n - 1;172 let mut types = Tensor::full(vec![height, width], GRID);173 for z in 0..n {174 for y in 0..n {175 for x in 0..n {176 if grid.get(&[x, y, z]) == 0 {177 continue;178 }179 let gx = x as isize - y as isize + (n as isize - 1);180 let gy = x as isize + y as isize - 2 * z as isize + (2 * n as isize - 2);181 if gx >= 0 && gx < width as isize - 1 && gy >= 0 && gy < height as isize - 2 {182 let (gx, gy) = (gx as usize, gy as usize);183 types.set(&[gy, gx], UP);184 types.set(&[gy, gx + 1], UP);185 types.set(&[gy + 1, gx], LEFT);186 types.set(&[gy + 1, gx + 1], RIGHT);187 types.set(&[gy + 2, gx], LEFT);188 types.set(&[gy + 2, gx + 1], RIGHT);189 }190 }191 }192 }193 Ok(Cell6d::new(194 Cell2d::new(types),195 Projection::Iso,196 Orientation::Vertical,197 1,198 ))199}200201/// Projects a cube's three facing sides into a hexagon of fills and voids.202pub fn pro(cell: &Cell3d) -> Result<Cell6d> {203 if !is_cube(cell) {204 return value_error("Cell must be a cube.");205 }206 let grid = cell.types();207 let n = grid.shape[0];208 let width = 2 * n;209 let height = 4 * n - 1;210 let mut types = Tensor::full(vec![height, width], GRID);211 let place = |x: usize, y: usize, z: usize, face: u8, types: &mut Tensor| {212 let val = if grid.get(&[x, y, z]) == 1 {213 FILL214 } else {215 VOID216 };217 let gx = x as isize - y as isize + (n as isize - 1);218 let gy = x as isize + y as isize - 2 * z as isize + (2 * n as isize - 2);219 if gx >= 0 && gx < width as isize - 1 && gy >= 0 && gy < height as isize - 2 {220 let (gx, gy) = (gx as usize, gy as usize);221 match face {222 0 => {223 types.set(&[gy + 1, gx], val);224 types.set(&[gy + 2, gx], val);225 }226 1 => {227 types.set(&[gy + 1, gx + 1], val);228 types.set(&[gy + 2, gx + 1], val);229 }230 _ => {231 types.set(&[gy, gx], val);232 types.set(&[gy, gx + 1], val);233 }234 }235 }236 };237 let y = n - 1;238 for z in 0..n {239 for x in 0..n {240 place(x, y, z, 0, &mut types);241 }242 }243 let x = n - 1;244 for z in 0..n {245 for y in 0..n {246 place(x, y, z, 1, &mut types);247 }248 }249 let z = n - 1;250 for y in 0..n {251 for x in 0..n {252 place(x, y, z, 2, &mut types);253 }254 }255 Ok(Cell6d::new(256 Cell2d::new(types),257 Projection::Pro,258 Orientation::Vertical,259 1,260 ))261}262263/// Slices a cube through its center across the main diagonal into a hexagon.264pub fn cut(cell: &Cell3d) -> Result<Cell6d> {265 if !is_cube(cell) {266 return value_error("Cell must be a cube.");267 }268 let scale = 4usize;269 let block = Tensor::full(vec![scale, scale, scale], 1);270 let grid = cell.types().kron(&block);271 let size = grid.shape[0];272 let k = (3 * (size - 1)) / 2;273 let mut rows: Vec<Vec<u8>> = Vec::new();274 for z in (0..size).step_by(2) {275 let target = k - z;276 let min_x = target.saturating_sub(size - 1);277 let max_x = (size - 1).min(target);278 if min_x > max_x {279 continue;280 }281 let mut row = Vec::new();282 for x in min_x..=max_x {283 let y = target - x;284 row.push(grid.get(&[x, y, z]));285 }286 rows.push(row);287 }288 if rows.is_empty() {289 return Ok(Cell6d::new(290 Cell2d::new(Tensor::new(vec![1, 1])),291 Projection::Cut,292 Orientation::Horizontal,293 0,294 ));295 }296 let width = rows.iter().map(|r| r.len()).max().unwrap();297 let height = rows.len();298 let mut types = Tensor::full(vec![height, width], GRID);299 for (r, row) in rows.iter().enumerate() {300 let offset = (width - row.len()) / 2;301 for (c, &v) in row.iter().enumerate() {302 types.set(&[r, c + offset], if v == 1 { FILL } else { VOID });303 }304 }305 Ok(Cell6d::new(306 Cell2d::new(types),307 Projection::Cut,308 Orientation::Horizontal,309 0,310 ))311}312313/// Stamps a hexagonal cell at every set mask entry into one interlocking sheet, colors and tags included.314pub fn tessellate(cell: &Cell6d, mask: &Tensor) -> Result<Cell2d> {315 let inner = &cell.cell;316 if !is_hex(inner) {317 return value_error("Cell must be a hexagon.");318 }319 let orient = orientation(inner.width(), inner.height())?;320 let (tile_h, tile_w) = (inner.height(), inner.width());321 let (dx, dy, row_shift) = match orient {322 Orientation::Horizontal => ((3 * (tile_w + 1)) / 4, tile_h, tile_h / 2),323 Orientation::Vertical => (tile_w, (3 * (tile_h + 1)) / 4, tile_w / 2),324 };325 let mut positions = Vec::new();326 for r in 0..mask.shape[0] {327 for c in 0..mask.shape[1] {328 if mask.get(&[r, c]) == 0 {329 continue;330 }331 let (mut px, mut py) = (c * dx, r * dy);332 match orient {333 Orientation::Horizontal => {334 if !c.is_multiple_of(2) {335 py += row_shift;336 }337 }338 Orientation::Vertical => {339 if !r.is_multiple_of(2) {340 px += row_shift;341 }342 }343 }344 positions.push((px, py));345 }346 }347 if positions.is_empty() {348 return Ok(Cell2d::new(Tensor::new(vec![1, 1])));349 }350 let min_x = positions.iter().map(|p| p.0).min().unwrap();351 let min_y = positions.iter().map(|p| p.1).min().unwrap();352 let max_x = positions.iter().map(|p| p.0 + tile_w).max().unwrap();353 let max_y = positions.iter().map(|p| p.1 + tile_h).max().unwrap();354 let (final_w, final_h) = (max_x - min_x, max_y - min_y);355 let count = inner.cell.size();356 let mut map = vec![count; final_h * final_w];357 for &(px, py) in &positions {358 let (dest_x, dest_y) = (px - min_x, py - min_y);359 for y in 0..tile_h {360 for x in 0..tile_w {361 if inner.types().get(&[y, x]) != GRID {362 map[(dest_y + y) * final_w + dest_x + x] = y * tile_w + x;363 }364 }365 }366 }367 let back = Cell2d::new(Tensor::full(vec![1, 1], GRID));368 Ok(Cell2d {369 cell: remap(&backed(inner, &back, 0), &map, &[final_h, final_w]),370 })371}372373/// Tessellates a hexagonal cell over a full width-by-height mask.374pub fn tile(cell: &Cell6d, width: usize, height: usize) -> Result<Cell2d> {375 tessellate(cell, &Tensor::full(vec![height, width], 1))376}377378/// Returns the interlocking step, in triangle columns and rows, that a sheet of hexagons of the given width and height loses off each side when cropped.379///380/// ```381/// assert_eq!(mrlymath::six::tile_step((11, 6)).unwrap(), (2, 3));382/// ```383pub fn tile_step(size: (usize, usize)) -> Result<(usize, usize)> {384 let (w, h) = size;385 Ok(match orientation(w, h)? {386 Orientation::Horizontal => ((w - 1) / 4, h / 2),387 Orientation::Vertical => (w / 2, (h - 1) / 4),388 })389}390391/// Crops one interlocking step off each side of a sheet tiled at the given size.392pub fn tile_crop(cell: &Cell2d, size: (usize, usize)) -> Result<Cell2d> {393 let (crop_x, crop_y) = tile_step(size)?;394 crop(cell, crop_x, crop_y)395}396397/// Tessellates a hexagon over a full width-by-height mask and returns the sheet as a projected cell, cropped to the interlocking rectangle on request.398///399/// The sheet keeps the tile's projection and orientation. A crop slides the triangle grid by the interlocking step on both axes, so the start parity flips whenever that step is odd; without the flip every triangle in the sheet points the wrong way.400pub fn tile_cell(cell: &Cell6d, width: usize, height: usize, crop: bool) -> Result<Cell6d> {401 let size = (cell.width(), cell.height());402 let orient = orientation(size.0, size.1)?;403 let sheet = tile(cell, width, height)?;404 let (sheet, start) = match crop {405 true => {406 let (step_x, step_y) = tile_step(size)?;407 (408 tile_crop(&sheet, size)?,409 (cell.start as usize + step_x + step_y) % 2,410 )411 }412 false => (sheet, cell.start as usize),413 };414 Ok(Cell6d::new(sheet, cell.projection, orient, start as u8))415}416417/// Recodes an isometric projection's top, left and right faces as plain fills, so a census reads its visible skin as one figure.418///419/// The other two projections already speak in fills and voids and come back untouched.420pub fn skin(cell: &Cell6d) -> Cell6d {421 let mut out = cell.clone();422 for v in out.cell.cell.types.bytes_mut().iter_mut() {423 if [UP, LEFT, RIGHT].contains(v) {424 *v = FILL;425 }426 }427 out428}429430/// Backs a cell onto a backdrop whose longer axis matches its orientation, leaving every triangle where it stood.431///432/// A renderer reads a sheet's orientation off its frame, so a tall sheet of wide hexagons would draw every triangle on its side; the spare columns or rows are backdrop and reach neither the census nor the picture.433pub fn framed(cell: &Cell6d) -> Cell6d {434 let (h, w) = (cell.height(), cell.width());435 let (width, height) = match cell.orientation {436 Orientation::Horizontal if w <= h => (h + 1, h),437 Orientation::Vertical if h <= w => (w, w + 1),438 _ => return cell.clone(),439 };440 let mut types = Tensor::filled(vec![height, width], GRID as i64, cell.cell.types().dtype());441 for y in 0..h {442 for x in 0..w {443 types.set(&[y, x], cell.cell.types().get(&[y, x]));444 }445 }446 Cell6d::new(447 Cell2d::new(types),448 cell.projection,449 cell.orientation,450 cell.start,451 )452}453454fn crop(cell: &Cell2d, crop_x: usize, crop_y: usize) -> Result<Cell2d> {455 let (current_h, current_w) = (cell.height(), cell.width());456 if crop_y * 2 >= current_h || crop_x * 2 >= current_w {457 return Ok(Cell2d::new(Tensor::new(vec![1, 1])));458 }459 let (new_h, new_w) = (current_h - 2 * crop_y, current_w - 2 * crop_x);460 let map: Vec<usize> = (0..new_h * new_w)461 .map(|flat| (flat / new_w + crop_y) * current_w + flat % new_w + crop_x)462 .collect();463 Ok(Cell2d {464 cell: remap(&cell.cell, &map, &[new_h, new_w]),465 })466}467468/// Builds the disc mask of cells within hex distance radius of the center.469pub fn radial_mask(radius: usize, orient: Orientation) -> Tensor {470 if radius < 1 {471 return Tensor::new(vec![1, 1]);472 }473 let size = 2 * radius - 1;474 let center = radius - 1;475 let mut mask = Tensor::new(vec![size, size]);476 let (c_q, c_r) = match orient {477 Orientation::Horizontal => (478 center as isize,479 center as isize - ((center - (center & 1)) / 2) as isize,480 ),481 Orientation::Vertical => (482 center as isize - ((center - (center & 1)) / 2) as isize,483 center as isize,484 ),485 };486 for r in 0..size {487 for c in 0..size {488 let (q, r_axial) = match orient {489 Orientation::Horizontal => (c as isize, r as isize - ((c - (c & 1)) / 2) as isize),490 Orientation::Vertical => (c as isize - ((r - (r & 1)) / 2) as isize, r as isize),491 };492 let dq = q - c_q;493 let dr = r_axial - c_r;494 if (dq.abs() + dr.abs() + (dq + dr).abs()) / 2 < radius as isize {495 mask.set(&[r, c], 1);496 }497 }498 }499 mask500}501502/// Tessellates a hexagonal cell over the disc mask of the given radius.503pub fn radial(cell: &Cell6d, radius: usize) -> Result<Cell2d> {504 let inner = &cell.cell;505 if !is_hex(inner) {506 return value_error("Cell must be a hexagon.");507 }508 let orient = orientation(inner.width(), inner.height())?;509 tessellate(cell, &radial_mask(radius, orient))510}511512/// Crops the interlocking overhang off a disc tiled at the given radius and tile size.513pub fn radial_crop(cell: &Cell2d, radius: usize, size: (usize, usize)) -> Result<Cell2d> {514 let (w, h) = size;515 let orient = orientation(w, h)?;516 let rings = radius.saturating_sub(1);517 let (crop_x, crop_y) = match orient {518 Orientation::Horizontal => (h / 2, rings * (h / 2)),519 Orientation::Vertical => (rings * (w / 2), w / 2),520 };521 crop(cell, crop_x, crop_y)522}523524#[cfg(test)]525mod tests {526 use super::*;527 use crate::three;528 #[test]529 fn blank_frames_both_orientations() {530 let b = blank(2, Orientation::Horizontal, 1, 0);531 assert_eq!(b.types().shape, vec![4, 7]);532 assert_eq!(533 b.types().bytes(),534 vec![535 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0536 ]537 );538 let v = blank(2, Orientation::Vertical, 1, 0);539 assert_eq!(v.types().shape, vec![7, 4]);540 assert!(is_hex(&b));541 assert!(is_hex(&v));542 }543 #[test]544 fn radial_mask_is_the_hex_disc() {545 let m = radial_mask(2, Orientation::Horizontal);546 assert_eq!(m.bytes(), vec![0, 1, 0, 1, 1, 1, 1, 1, 1]);547 }548 #[test]549 fn radial_crop_trims_the_overhang() {550 let hex = Cell6d::new(551 blank(2, Orientation::Horizontal, FILL, GRID),552 Projection::Cut,553 Orientation::Horizontal,554 0,555 );556 let (w, h) = (hex.width(), hex.height());557 let disc = radial(&hex, 2).unwrap();558 let cropped = radial_crop(&disc, 2, (w, h)).unwrap();559 assert_eq!(cropped.height(), disc.height() - h);560 assert_eq!(cropped.width(), disc.width() - h);561 let tight = radial_crop(&disc, 9, (w, h)).unwrap();562 assert_eq!(tight.types().shape, vec![1, 1]);563 }564 #[test]565 fn radial_crop_shrinks_the_two_axes_apart() {566 let radius = 3;567 let rings = radius - 1;568 for orient in [Orientation::Horizontal, Orientation::Vertical] {569 let hex = Cell6d::new(blank(2, orient, FILL, GRID), Projection::Cut, orient, 0);570 let (w, h) = (hex.width(), hex.height());571 let disc = radial(&hex, radius).unwrap();572 let cropped = radial_crop(&disc, radius, (w, h)).unwrap();573 let (lost_x, lost_y) = match orient {574 Orientation::Horizontal => (h, rings * h),575 Orientation::Vertical => (rings * w, w),576 };577 assert_ne!(lost_x, lost_y, "{orient:?}");578 assert_eq!(cropped.width(), disc.width() - lost_x, "{orient:?}");579 assert_eq!(cropped.height(), disc.height() - lost_y, "{orient:?}");580 }581 }582 #[test]583 fn tessellate_and_crop_carry_colors_and_tags() {584 let painted = crate::six::paint(585 Cell6d::new(586 blank(2, Orientation::Horizontal, FILL, GRID),587 Projection::Cut,588 Orientation::Horizontal,589 0,590 ),591 None,592 None,593 );594 assert!(painted.cell.cell.colors.is_some());595 let sheet = tile(&painted, 2, 2).unwrap();596 let colors = sheet.cell.colors.as_ref().unwrap();597 assert_eq!(colors.len(), sheet.width() * sheet.height());598 let opaque = colors.iter().filter(|c| c[3] > 0).count();599 assert_eq!(600 opaque,601 sheet.types().bytes().iter().filter(|&&v| v != GRID).count()602 );603 let cropped = tile_crop(&sheet, (painted.width(), painted.height())).unwrap();604 assert_eq!(605 cropped.cell.colors.as_ref().unwrap().len(),606 cropped.width() * cropped.height()607 );608 }609 #[test]610 fn pad_carries_colors_across_the_ring() {611 let painted = crate::six::paint(612 Cell6d::new(613 blank(2, Orientation::Horizontal, FILL, VOID),614 Projection::Cut,615 Orientation::Horizontal,616 0,617 ),618 None,619 None,620 );621 let source = painted.cell.cell.colors.clone().unwrap();622 let wider = pad(&painted, 1, GRID).unwrap();623 let grown = wider.cell.cell.colors.as_ref().unwrap();624 let y_off = (wider.height() - painted.height()) / 2;625 let x_off = (wider.width() - painted.width()) / 2;626 for y in 0..painted.height() {627 for x in 0..painted.width() {628 assert_eq!(629 grown[(y + y_off) * wider.width() + x + x_off],630 source[y * painted.width() + x]631 );632 }633 }634 assert_eq!(grown[0], [0, 0, 0, 0]);635 }636 #[test]637 fn the_sheet_keeps_the_tile_pointing_the_same_way() {638 let hex = crate::six::cut_design(23, 3, 1, 2).unwrap();639 assert_eq!((hex.width(), hex.height()), (11, 6));640 for (wide, high) in [(5usize, 5usize), (3, 9)] {641 for crop in [false, true] {642 let sheet = tile_cell(&hex, wide, high, crop).unwrap();643 assert_eq!(sheet.orientation, Orientation::Horizontal);644 let shown = framed(&sheet);645 assert!(shown.width() > shown.height(), "{wide}x{high} {crop}");646 assert_eq!(647 orientation(shown.width(), shown.height()).unwrap(),648 Orientation::Horizontal649 );650 let whole = crate::six::census(&shown, false);651 assert_eq!(whole.euler, 1, "{wide}x{high} {crop}");652 let triangles = whole.fills + whole.voids;653 match crop {654 false => assert_eq!(triangles, wide * high * 54, "{wide}x{high}"),655 true => assert!(triangles < wide * high * 54, "{wide}x{high}"),656 }657 }658 }659 }660 fn filled_corners(cell: &Cell6d) -> std::collections::BTreeSet<[(i64, i64); 3]> {661 let mut out = std::collections::BTreeSet::new();662 for y in 0..cell.height() {663 for x in 0..cell.width() {664 if cell.cell.types().get(&[y, x]) == FILL {665 out.insert(crate::six::census::corners(666 x as i64,667 y as i64,668 cell.start as i64,669 ));670 }671 }672 }673 out674 }675 fn shifted(676 set: &std::collections::BTreeSet<[(i64, i64); 3]>,677 dx: i64,678 dy: i64,679 ) -> std::collections::BTreeSet<[(i64, i64); 3]> {680 set.iter()681 .map(|c| c.map(|(x, y)| (x + dx, y + dy)))682 .collect()683 }684 #[test]685 fn the_sheet_lays_every_copy_on_the_triangle_lattice() {686 let hex = crate::six::cut_design(23, 3, 1, 2).unwrap();687 let one = filled_corners(&hex);688 let (dx, dy, shift) = (9i64, 6i64, 3i64);689 for (wide, high) in [(5usize, 5usize), (3usize, 9usize)] {690 let sheet = tile_cell(&hex, wide, high, false).unwrap();691 let mut want = std::collections::BTreeSet::new();692 for r in 0..high as i64 {693 for c in 0..wide as i64 {694 let py = dy * r + if c % 2 == 1 { shift } else { 0 };695 want.extend(shifted(&one, dx * c, 2 * py));696 }697 }698 assert_eq!(filled_corners(&sheet), want, "{wide}x{high}");699 assert_eq!(want.len(), wide * high * one.len(), "{wide}x{high}");700 }701 }702 #[test]703 fn the_crop_flips_the_start_parity_when_its_step_is_odd() {704 let hex = crate::six::cut_design(23, 3, 1, 2).unwrap();705 let (step_x, step_y) = tile_step((hex.width(), hex.height())).unwrap();706 assert_eq!((step_x, step_y), (2, 3));707 let plain = tile_cell(&hex, 5, 5, false).unwrap();708 let cropped = tile_cell(&hex, 5, 5, true).unwrap();709 assert_eq!(plain.start, 0);710 assert_eq!(cropped.start, 1);711 assert_eq!(712 (cropped.width(), cropped.height()),713 (plain.width() - 2 * step_x, plain.height() - 2 * step_y)714 );715 let whole = filled_corners(&plain);716 let back = shifted(&filled_corners(&cropped), step_x as i64, 2 * step_y as i64);717 assert!(back.is_subset(&whole));718 let wrong = Cell6d::new(719 cropped.cell.clone(),720 cropped.projection,721 cropped.orientation,722 plain.start,723 );724 assert!(725 !shifted(&filled_corners(&wrong), step_x as i64, 2 * step_y as i64).is_subset(&whole)726 );727 }728 #[test]729 fn skin_turns_the_iso_faces_into_one_figure() {730 let iso = crate::six::iso_design(23, 3, 1, 2).unwrap();731 let bare = crate::six::census(&iso, false);732 assert_eq!((bare.fills, bare.voids), (0, 0));733 let painted = iso734 .cell735 .types()736 .bytes()737 .iter()738 .filter(|&&v| [UP, LEFT, RIGHT].contains(&v))739 .count();740 let read = crate::six::census(&skin(&iso), false);741 assert_eq!(read.voids, 0);742 assert_eq!(read.fills, painted);743 assert_eq!(read.triangles, painted);744 assert_eq!(read.grids, bare.grids);745 assert!(painted > 0);746 let sliced = crate::six::cut_design(23, 3, 1, 2).unwrap();747 assert_eq!(skin(&sliced).cell, sliced.cell);748 }749 #[test]750 fn framed_leaves_a_sheet_that_already_points_right_alone() {751 let hex = crate::six::cut_design(23, 3, 1, 2).unwrap();752 let wide = tile_cell(&hex, 5, 5, false).unwrap();753 assert_eq!(framed(&wide).cell, wide.cell);754 let tall = tile_cell(&hex, 3, 9, false).unwrap();755 assert!(tall.width() < tall.height());756 assert_eq!(framed(&tall).height(), tall.height());757 assert_eq!(framed(&tall).width(), tall.height() + 1);758 assert_eq!(759 crate::six::census(&framed(&tall), false).fills,760 crate::six::census(&tall, false).fills761 );762 }763 #[test]764 fn projections_have_expected_frames() {765 let c = three::carpet(3, 1).unwrap();766 let i = iso(&c).unwrap();767 assert_eq!(i.cell.types().shape, vec![11, 6]);768 assert_eq!(i.start, 1);769 let p = pro(&c).unwrap();770 assert_eq!(p.cell.types().shape, vec![11, 6]);771 let q = cut(&c).unwrap();772 assert_eq!(q.orientation, Orientation::Horizontal);773 assert_eq!(q.start, 0);774 assert!(iso(&three::Cell3d::new(Tensor::new(vec![2, 3, 2]))).is_err());775 }776}