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}