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}