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}