renderer.rs

6.9 kB · rust · 229 lines

1use super::geometry::orientation;2use super::models::Cell6d;3use super::painter::paint;4use super::Orientation;5use crate::core::colors::Color;6use crate::core::enums::Mode;7use crate::core::errors::{value_error, Result};89/// A screen triangle: three grid points and an RGBA color.10pub type Triangle = ([(i64, i64); 3], [u8; 4]);1112#[derive(Clone, Debug)]13struct Rect {14    triangles: Vec<Triangle>,15    origin: (i64, i64),16    size: (usize, usize),17}1819fn north(x: i64, y: i64) -> [(i64, i64); 3] {20    [(x, 2 * y + 2), (x + 1, 2 * y), (x + 2, 2 * y + 2)]21}2223fn south(x: i64, y: i64) -> [(i64, i64); 3] {24    [(x, 2 * y), (x + 1, 2 * y + 2), (x + 2, 2 * y)]25}2627fn east(x: i64, y: i64) -> [(i64, i64); 3] {28    [(2 * x, y), (2 * x, y + 2), (2 * x + 2, y + 1)]29}3031fn west(x: i64, y: i64) -> [(i64, i64); 3] {32    [(2 * x + 2, y), (2 * x + 2, y + 2), (2 * x, y + 1)]33}3435fn painted(cell: &Cell6d) -> Vec<[u8; 4]> {36    match &cell.cell.cell.colors {37        Some(colors) => colors.clone(),38        None => paint(cell.clone(), None, Some(Mode::Type))39            .cell40            .cell41            .colors42            .unwrap(),43    }44}4546/// Folds a cell into colored screen triangles, dropping the transparent ones.47pub fn triangles(cell: &Cell6d) -> Result<Vec<Triangle>> {48    let inner = &cell.cell;49    let (height, width) = (inner.height(), inner.width());50    let colors = painted(cell);51    let orient = orientation(width, height)?;52    let start = cell.start as i64;53    let mut out = Vec::new();54    for y in 0..height {55        for x in 0..width {56            let rgba = colors[y * width + x];57            if rgba[3] == 0 {58                continue;59            }60            let flip = (x as i64 + y as i64 + start).rem_euclid(2);61            let points = match orient {62                Orientation::Horizontal => {63                    if flip == 0 {64                        north(x as i64, y as i64)65                    } else {66                        south(x as i64, y as i64)67                    }68                }69                Orientation::Vertical => {70                    if flip == 0 {71                        east(x as i64, y as i64)72                    } else {73                        west(x as i64, y as i64)74                    }75                }76            };77            out.push((points, rgba));78        }79    }80    Ok(out)81}8283fn bounds(tris: &[Triangle]) -> (i64, i64, i64, i64) {84    let xs = tris.iter().flat_map(|(p, _)| p.iter().map(|q| q.0));85    let ys = tris.iter().flat_map(|(p, _)| p.iter().map(|q| q.1));86    let min_x = xs.clone().min().unwrap();87    let max_x = xs.max().unwrap();88    let min_y = ys.clone().min().unwrap();89    let max_y = ys.max().unwrap();90    (min_x, max_x, min_y, max_y)91}9293fn window(cell: &Cell6d, padding: i64) -> Result<Rect> {94    let triangles = triangles(cell)?;95    if triangles.is_empty() {96        return value_error("nothing to render.");97    }98    let (min_x, max_x, min_y, max_y) = bounds(&triangles);99    Ok(Rect {100        triangles,101        origin: (min_x - padding, min_y - padding),102        size: (103            (max_x - min_x + 2 * padding) as usize,104            (max_y - min_y + 2 * padding) as usize,105        ),106    })107}108109fn stroke_of(outline: Option<Color>, width: usize) -> String {110    match outline {111        Some(c) => format!("stroke=\"{}\" stroke-width=\"{width}\"", c.to_hex()),112        None => "stroke=\"none\"".to_string(),113    }114}115116fn polygon(117    points: &[(i64, i64); 3],118    rgba: [u8; 4],119    origin: (i64, i64),120    scale: usize,121    stroke: &str,122) -> String {123    let [r, g, b, a] = rgba;124    let fill = Color::rgba(r, g, b, a).to_hex();125    let pts: Vec<String> = points126        .iter()127        .map(|(x, y)| {128            format!(129                "{},{}",130                (x - origin.0) * scale as i64,131                (y - origin.1) * scale as i64132            )133        })134        .collect();135    format!(136        "<polygon points=\"{}\" fill=\"{fill}\" {stroke}/>",137        pts.join(" ")138    )139}140141fn sheet(view: &Rect, scale: usize, stroke: &str) -> String {142    let (img_w, img_h) = (view.size.0 * scale, view.size.1 * scale);143    let mut out = vec![format!(144        "<svg width=\"{img_w}\" height=\"{img_h}\" viewBox=\"0 0 {img_w} {img_h}\" xmlns=\"http://www.w3.org/2000/svg\">"145    )];146    for (points, rgba) in &view.triangles {147        out.push(polygon(points, *rgba, view.origin, scale, stroke));148    }149    out.push("</svg>".to_string());150    out.join("\n")151}152153/// Renders a cell's triangles to an SVG string at the given scale, stroked and padded when an outline is given, or an error when nothing renders.154pub fn svg(cell: &Cell6d, scale: usize, outline: Option<Color>, width: usize) -> Result<String> {155    let padding = if outline.is_some() { width as i64 } else { 0 };156    Ok(sheet(157        &window(cell, padding)?,158        scale,159        &stroke_of(outline, width),160    ))161}162163#[cfg(test)]164mod tests {165    use super::*;166    use crate::math::six::designs::iso_design;167    use crate::math::six::geometry::blank;168    use crate::math::six::models::Cell6d;169    use crate::math::six::{Orientation, Projection};170    #[test]171    fn triangle_geometry() {172        assert_eq!(north(0, 0), [(0, 2), (1, 0), (2, 2)]);173        assert_eq!(south(0, 0), [(0, 0), (1, 2), (2, 0)]);174        assert_eq!(east(1, 1), [(2, 1), (2, 3), (4, 2)]);175    }176    #[test]177    fn iso_renders_triangles() {178        let i = iso_design(23, 3, 1, 2).unwrap();179        let tris = triangles(&i).unwrap();180        assert!(!tris.is_empty());181        let s = svg(&i, 10, None, 1).unwrap();182        assert!(s.contains("<polygon"));183        assert!(s.contains("stroke=\"none\""));184    }185    #[test]186    fn outline_strokes_and_pads_the_svg() {187        let i = iso_design(23, 3, 1, 2).unwrap();188        let plain = svg(&i, 4, None, 1).unwrap();189        let lined = svg(&i, 4, Some(Color::rgba(255, 0, 0, 255)), 2).unwrap();190        assert!(lined.contains("stroke-width=\"2\""));191        assert!(lined.contains(&Color::rgba(255, 0, 0, 255).to_hex()));192        let size = |svg: &str| -> usize {193            svg.split("width=\"")194                .nth(1)195                .unwrap()196                .split('"')197                .next()198                .unwrap()199                .parse()200                .unwrap()201        };202        assert_eq!(size(&lined), size(&plain) + 4 * 4);203    }204    #[test]205    fn hexagon_renders() {206        let hex = Cell6d::new(207            blank(3, Orientation::Horizontal, 1, 0),208            Projection::Cut,209            Orientation::Horizontal,210            0,211        );212        let tris = triangles(&hex).unwrap();213        assert!(!tris.is_empty());214    }215    #[test]216    fn nothing_to_render_errors_in_both_doors() {217        let bare = Cell6d::new(218            crate::math::two::Cell2d::new(crate::core::Tensor::full(219                vec![2, 3],220                crate::math::six::GRID,221            )),222            Projection::Cut,223            Orientation::Horizontal,224            0,225        );226        assert!(triangles(&bare).unwrap().is_empty());227        assert!(svg(&bare, 4, None, 1).is_err());228    }229}