raster.rs
4.4 kB · rust · 131 lines
1use super::geometry::orientation;2use super::models::Cell6d;3use super::{Orientation, FILL, GRID};4use mrlycore::errors::{value_error, Result};56const ROW: f64 = 0.866_025_403_784_438_6;78/// Rasterizes a hex cell's fills on a square of the side at the true hex aspect, one for a fill triangle and zero elsewhere.9pub fn raster(cell: &Cell6d, size: usize) -> Result<Vec<f32>> {10 if size == 0 {11 return value_error("size must be at least 1.");12 }13 let (width, height) = (cell.width(), cell.height());14 let types = cell.cell.cell.types.bytes();15 let flipped = orientation(width, height)? == Orientation::Vertical;16 let (cols, rows) = if flipped {17 (height, width)18 } else {19 (width, height)20 };21 let start = cell.start as i64 + flipped as i64;22 let at = |col: i64, row: i64| -> Option<u8> {23 if col < 0 || row < 0 || col >= cols as i64 || row >= rows as i64 {24 return None;25 }26 let (col, row) = (col as usize, row as usize);27 let index = if flipped {28 col * width + row29 } else {30 row * width + col31 };32 Some(types[index])33 };34 let mut bounds: Option<(usize, usize, usize, usize)> = None;35 for row in 0..rows {36 for col in 0..cols {37 let value = at(col as i64, row as i64).unwrap();38 if value == GRID {39 continue;40 }41 bounds = Some(match bounds {42 None => (col, col, row, row),43 Some((a, b, c, d)) => (a.min(col), b.max(col), c.min(row), d.max(row)),44 });45 }46 }47 let Some((min_col, max_col, min_row, max_row)) = bounds else {48 return value_error("nothing to rasterize.");49 };50 let (left, right) = (min_col as f64, max_col as f64 + 2.0);51 let (top, bottom) = (52 2.0 * min_row as f64 * ROW,53 (2.0 * max_row as f64 + 2.0) * ROW,54 );55 let (wide, tall) = (right - left, bottom - top);56 let unit = wide.max(tall) / size as f64;57 let (x0, y0) = (58 left - (size as f64 * unit - wide) / 2.0,59 top - (size as f64 * unit - tall) / 2.0,60 );61 let mut out = Vec::with_capacity(size * size);62 for i in 0..size {63 for j in 0..size {64 let px = x0 + (j as f64 + 0.5) * unit;65 let py = (y0 + (i as f64 + 0.5) * unit) / ROW;66 let row = (py / 2.0).floor();67 let t = py / 2.0 - row;68 let base = px.floor() as i64;69 let row = row as i64;70 let mut hit = 0.0;71 for col in [base - 1, base] {72 let north = (col + row + start).rem_euclid(2) == 0;73 let (lo, hi) = if north {74 (col as f64 + 1.0 - t, col as f64 + 1.0 + t)75 } else {76 (col as f64 + t, col as f64 + 2.0 - t)77 };78 if px >= lo && px < hi {79 if at(col, row) == Some(FILL) {80 hit = 1.0;81 }82 break;83 }84 }85 out.push(hit);86 }87 }88 Ok(out)89}9091#[cfg(test)]92mod tests {93 use super::*;94 use crate::six::{census, cut_design};9596 fn fraction(cell: &Cell6d, size: usize) -> f64 {97 let pixels = raster(cell, size).unwrap();98 pixels.iter().map(|&v| v as f64).sum::<f64>() / (size * size) as f6499 }100101 fn hexagon_share(cell: &Cell6d) -> f64 {102 let tally = census::census(cell, false);103 let n = ((tally.triangles as f64) / 6.0).sqrt();104 let wide = 4.0 * n;105 let area = (tally.fills as f64) * 3f64.sqrt();106 area / (wide * wide)107 }108109 #[test]110 fn the_solid_cut_fills_its_hexagon() {111 let cell = cut_design(255, 3, 1, 2).unwrap();112 let expect = hexagon_share(&cell);113 assert!((fraction(&cell, 400) - expect).abs() < 0.01, "{expect}");114 let centre = raster(&cell, 101).unwrap()[50 * 101 + 50];115 assert_eq!(centre, 1.0);116 }117118 #[test]119 fn the_carpet_cut_is_pierced_at_the_centre() {120 let cell = cut_design(23, 3, 1, 2).unwrap();121 let expect = hexagon_share(&cell);122 assert!((fraction(&cell, 400) - expect).abs() < 0.01, "{expect}");123 let centre = raster(&cell, 101).unwrap()[50 * 101 + 50];124 assert_eq!(centre, 0.0);125 }126127 #[test]128 fn an_empty_size_is_refused() {129 assert!(raster(&cut_design(23, 3, 1, 2).unwrap(), 0).is_err());130 }131}