grid.rs

4.8 kB · rust · 140 lines

1use crate::board::{Board, Frame};2use mrlycore::Color;3use mrlymath::two::Cell2d;45// GRID67/// A lattice of cells laid over a frame, each cell drawn inside its own gap.8#[derive(Clone, Copy, Debug, PartialEq)]9pub struct Grid {10    /// The frame the lattice covers.11    pub frame: Frame,12    /// The number of columns.13    pub cols: usize,14    /// The number of rows.15    pub rows: usize,16    /// The share of a cell left empty between one cell and the next.17    pub gap: f64,18}1920impl Grid {21    /// Lays a lattice of cols by rows over the frame, keeping a gap of that fraction of a cell.22    pub fn new(frame: Frame, cols: usize, rows: usize, gap: f64) -> Grid {23        Grid {24            frame,25            cols: cols.max(1),26            rows: rows.max(1),27            gap,28        }29    }30    /// Returns the drawn box of one cell as its corner and its size; at gap zero the edges snap to whole pixels so neighbours meet without a seam.31    pub fn cell(&self, col: usize, row: usize) -> (f64, f64, f64, f64) {32        let w = self.frame.w / self.cols as f64;33        let h = self.frame.h / self.rows as f64;34        if self.gap <= 0.0 {35            let x0 = (self.frame.x + col as f64 * w).round();36            let x1 = (self.frame.x + (col + 1) as f64 * w).round();37            let y0 = (self.frame.y + row as f64 * h).round();38            let y1 = (self.frame.y + (row + 1) as f64 * h).round();39            return (x0, y0, x1 - x0, y1 - y0);40        }41        let pad = self.gap * w.min(h) / 2.0;42        (43            self.frame.x + col as f64 * w + pad,44            self.frame.y + row as f64 * h + pad,45            w - 2.0 * pad,46            h - 2.0 * pad,47        )48    }49    /// Fills one cell.50    pub fn fill(&self, board: &mut Board, col: usize, row: usize, color: Color) {51        let (x, y, w, h) = self.cell(col, row);52        board.rect(x, y, w, h, color);53    }54    /// Fills every cell of a flat design whose type byte the ink maps to a color.55    pub fn paint(&self, board: &mut Board, cells: &Cell2d, ink: impl Fn(u8) -> Option<Color>) {56        let types = cells.types();57        let (height, width) = (cells.height(), cells.width());58        for row in 0..self.rows.min(height) {59            for col in 0..self.cols.min(width) {60                if let Some(color) = ink(types.get(&[row, col])) {61                    self.fill(board, col, row, color);62                }63            }64        }65    }66    /// Fills every true cell of a mask.67    pub fn carpet(&self, board: &mut Board, mask: &[Vec<bool>], color: Color) {68        for (row, line) in mask.iter().enumerate().take(self.rows) {69            for (col, on) in line.iter().enumerate().take(self.cols) {70                if *on {71                    self.fill(board, col, row, color);72                }73            }74        }75    }76}7778// MASKS7980/// The MrlyProd logo, the five by five seed the mark grows from.81pub const LOGO: [&str; 5] = ["11111", "10101", "11111", "10101", "11111"];8283/// Grows a 0/1 string mask by Kronecker substitution, level one being the seed itself.84pub fn mask(rows: &[&str], level: usize) -> Vec<Vec<bool>> {85    let seed: Vec<Vec<bool>> = rows86        .iter()87        .map(|row| row.chars().map(|c| c == '1').collect())88        .collect();89    let mut out = seed.clone();90    for _ in 1..level.max(1) {91        let width = seed.first().map_or(0, |row| row.len());92        let mut next = Vec::with_capacity(out.len() * seed.len());93        for row in &out {94            for inner in &seed {95                let mut line = Vec::with_capacity(row.len() * width);96                for on in row {97                    if *on {98                        line.extend_from_slice(inner);99                    } else {100                        line.extend(std::iter::repeat_n(false, width));101                    }102                }103                next.push(line);104            }105        }106        out = next;107    }108    out109}110111/// Fills every true cell of a mask laid over a frame.112pub fn carpet(board: &mut Board, frame: Frame, mask: &[Vec<bool>], gap: f64, color: Color) {113    let rows = mask.len();114    let cols = mask.first().map_or(0, |row| row.len());115    if rows == 0 || cols == 0 {116        return;117    }118    Grid::new(frame, cols, rows, gap).carpet(board, mask, color);119}120121#[cfg(test)]122mod tests {123    use super::*;124    #[test]125    fn the_logo_at_level_two_is_twenty_five_wide_and_squares_its_ones() {126        let rows = mask(&LOGO, 2);127        let ones: usize = rows.iter().flatten().filter(|on| **on).count();128        assert_eq!(rows.len(), 25);129        assert_eq!(rows[0].len(), 25);130        assert_eq!(ones, 21 * 21);131    }132    #[test]133    fn at_gap_zero_neighbouring_cells_meet_on_a_whole_pixel() {134        let lattice = Grid::new(Frame::new(3.3, 0.0, 389.12, 389.12), 5, 5, 0.0);135        let (x0, _, w0, _) = lattice.cell(0, 0);136        let (x1, _, _, _) = lattice.cell(1, 0);137        assert_eq!(x0 + w0, x1);138        assert_eq!(x1, x1.round());139    }140}