hex.rs

7.4 kB · rust · 219 lines

1use crate::board::{Board, Frame};2use mrlycore::errors::Result;3use mrlycore::Color;4use mrlymath::six::geometry::orientation;5use mrlymath::six::Cell6d;6use mrlymath::six::Orientation;78// GEOMETRY910const RATIO: f64 = 0.866_025_403_784_438_6;1112fn north(x: i64, y: i64) -> [(f64, f64); 3] {13    let (x, y) = (x as f64, y as f64);14    [15        (x, 2.0 * y + 2.0),16        (x + 1.0, 2.0 * y),17        (x + 2.0, 2.0 * y + 2.0),18    ]19}2021fn south(x: i64, y: i64) -> [(f64, f64); 3] {22    let (x, y) = (x as f64, y as f64);23    [(x, 2.0 * y), (x + 1.0, 2.0 * y + 2.0), (x + 2.0, 2.0 * y)]24}2526fn east(x: i64, y: i64) -> [(f64, f64); 3] {27    let (x, y) = (x as f64, y as f64);28    [(2.0 * x, y), (2.0 * x, y + 2.0), (2.0 * x + 2.0, y + 1.0)]29}3031fn west(x: i64, y: i64) -> [(f64, f64); 3] {32    let (x, y) = (x as f64, y as f64);33    [34        (2.0 * x + 2.0, y),35        (2.0 * x + 2.0, y + 2.0),36        (2.0 * x, y + 1.0),37    ]38}3940fn shrink(pts: &[(f64, f64); 3], gap: f64) -> [(f64, f64); 3] {41    let cx = (pts[0].0 + pts[1].0 + pts[2].0) / 3.0;42    let cy = (pts[0].1 + pts[1].1 + pts[2].1) / 3.0;43    let side = |a: (f64, f64), b: (f64, f64)| ((b.0 - a.0).powi(2) + (b.1 - a.1).powi(2)).sqrt();44    let perimeter = side(pts[0], pts[1]) + side(pts[1], pts[2]) + side(pts[2], pts[0]);45    let area = ((pts[1].0 - pts[0].0) * (pts[2].1 - pts[0].1)46        - (pts[2].0 - pts[0].0) * (pts[1].1 - pts[0].1))47        .abs()48        / 2.0;49    let inradius = 2.0 * area / perimeter;50    let k = if inradius > 0.0 {51        ((inradius - gap) / inradius).max(0.0)52    } else {53        0.054    };55    let pull = |p: (f64, f64)| (cx + (p.0 - cx) * k, cy + (p.1 - cy) * k);56    [pull(pts[0]), pull(pts[1]), pull(pts[2])]57}5859fn fit(mesh: &[[(f64, f64); 3]], frame: Frame) -> impl Fn((f64, f64)) -> (f64, f64) {60    let mut lo = (f64::MAX, f64::MAX);61    let mut hi = (f64::MIN, f64::MIN);62    for tri in mesh {63        for p in tri {64            lo.0 = lo.0.min(p.0);65            lo.1 = lo.1.min(p.1);66            hi.0 = hi.0.max(p.0);67            hi.1 = hi.1.max(p.1);68        }69    }70    let (span_x, span_y) = ((hi.0 - lo.0).max(1e-9), (hi.1 - lo.1).max(1e-9));71    let scale = (frame.w / span_x).min(frame.h / span_y);72    let (ox, oy) = (73        frame.x + (frame.w - span_x * scale) / 2.0,74        frame.y + (frame.h - span_y * scale) / 2.0,75    );76    move |p: (f64, f64)| (ox + (p.0 - lo.0) * scale, oy + (p.1 - lo.1) * scale)77}7879// DRAWING8081/// Draws the triangle mesh of a hex slice into the frame, centred and equilateral.82///83/// The mesh is read straight from the cell's triangle grid: site (row, column) becomes one84/// unit triangle whose parity alternates from the cell's start, and the ink maps its type85/// byte to a color or to nothing. The gap is the number of pixels each triangle is pulled86/// back from its own edges.87pub fn draw(88    board: &mut Board,89    frame: Frame,90    cell: &Cell6d,91    gap: f64,92    ink: impl Fn(u8) -> Option<Color>,93) -> Result<()> {94    let (width, height) = (cell.width(), cell.height());95    let orient = orientation(width, height)?;96    let types = cell.cell.types();97    let start = cell.start as i64;98    let mut mesh = Vec::new();99    let mut paint = Vec::new();100    for y in 0..height {101        for x in 0..width {102            let color = match ink(types.get(&[y, x])) {103                Some(color) => color,104                None => continue,105            };106            let flip = (x as i64 + y as i64 + start).rem_euclid(2);107            let points = match (orient, flip) {108                (Orientation::Horizontal, 0) => north(x as i64, y as i64),109                (Orientation::Horizontal, _) => south(x as i64, y as i64),110                (Orientation::Vertical, 0) => east(x as i64, y as i64),111                (Orientation::Vertical, _) => west(x as i64, y as i64),112            };113            let squash = |p: (f64, f64)| match orient {114                Orientation::Horizontal => (p.0, p.1 * RATIO),115                Orientation::Vertical => (p.0 * RATIO, p.1),116            };117            mesh.push([squash(points[0]), squash(points[1]), squash(points[2])]);118            paint.push(color);119        }120    }121    if mesh.is_empty() {122        return Ok(());123    }124    let place = fit(&mesh, frame);125    for (tri, color) in mesh.iter().zip(paint) {126        let screen = [place(tri[0]), place(tri[1]), place(tri[2])];127        let small = shrink(&screen, gap);128        board.triangle(small[0], small[1], small[2], color);129    }130    Ok(())131}132133/// Returns the number of unit triangles in a plain hexagon of side n, which is six n squared.134pub fn count(n: usize) -> usize {135    6 * n * n136}137138/// Returns the number of triangles in one row of a plain hexagon of side n, rows counted from the top.139pub fn row_len(n: usize, row: usize) -> usize {140    let reach = if row < n { row } else { 2 * n - 1 - row };141    2 * (n + reach) + 1142}143144/// Draws a plain hexagon of side n cells, six n squared unit triangles, centred in the frame.145///146/// A triangle is addressed by its row from the top, its column from the left of that row, and147/// one for a triangle pointing up or zero for one pointing down. The gap is the number of148/// pixels each triangle is pulled back from its own edges.149pub fn hexagon(150    board: &mut Board,151    frame: Frame,152    n: usize,153    gap: f64,154    ink: impl Fn(usize, usize, usize) -> Option<Color>,155) {156    if n == 0 {157        return;158    }159    let side = (frame.w / (2 * n) as f64).min(frame.h / (n as f64 * 2.0 * RATIO));160    let rise = side * RATIO;161    let (cx, cy) = frame.center();162    let left = cx - side * n as f64;163    let top = cy - rise * n as f64;164    for row in 0..2 * n {165        let reach = if row < n { row } else { 2 * n - 1 - row };166        let up = row < n;167        let (long, short) = (n + reach + 1, n + reach);168        let (top_len, bot_len) = if up { (short, long) } else { (long, short) };169        let tx = left + (2 * n - top_len) as f64 * side / 2.0;170        let bx = left + (2 * n - bot_len) as f64 * side / 2.0;171        let (y0, y1) = (top + row as f64 * rise, top + (row + 1) as f64 * rise);172        for col in 0..row_len(n, row) {173            let points = if (col % 2 == 0) == up {174                let j = (col / 2) as f64;175                [176                    (bx + j * side, y1),177                    (bx + (j + 1.0) * side, y1),178                    (bx + (j + 0.5) * side, y0),179                ]180            } else {181                let i = (col / 2) as f64;182                [183                    (tx + i * side, y0),184                    (tx + (i + 1.0) * side, y0),185                    (tx + (i + 0.5) * side, y1),186                ]187            };188            let parity = usize::from((col % 2 == 0) == up);189            if let Some(color) = ink(row, col, parity) {190                let small = shrink(&points, gap);191                board.triangle(small[0], small[1], small[2], color);192            }193        }194    }195}196197#[cfg(test)]198mod tests {199    use super::*;200    use crate::ink;201    #[test]202    fn a_hexagon_of_side_s_has_six_s_squared_triangles() {203        for n in 1..8 {204            let rows: usize = (0..2 * n).map(|row| row_len(n, row)).sum();205            assert_eq!(rows, count(n));206        }207    }208    #[test]209    fn every_triangle_of_a_hexagon_is_offered_to_the_ink() {210        let mut board = Board::new(128, 128, ink::ground());211        let frame = board.frame(0.1);212        let seen = std::cell::Cell::new(0usize);213        hexagon(&mut board, frame, 3, 0.0, |_, _, _| {214            seen.set(seen.get() + 1);215            None216        });217        assert_eq!(seen.get(), count(3));218    }219}