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}