//! A drawable scene (coloured triangles, lines and points) and a CPU rasterizer for headless //! snapshots: z-buffered, lit triangles and depth-tested lines, rendered at 2× and downsampled //! for anti-aliasing; a scene's [`Scene::on_top`] lines are drawn over everything. The GPU //! viewport draws the same [`Scene`]. //! //! Adapted from SolidCraft's `crates/render/src/raster.rs` (MIT OR Apache-2.0), see //! ATTRIBUTION.md. use crate::pick::Projector; use crate::{Camera, Vec3, font}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Rgb(pub u8, pub u8, pub u8); impl Rgb { fn f(self) -> [f64; 3] { [self.0 as f64 / 255.0, self.1 as f64 / 255.0, self.2 as f64 / 255.0] } } #[derive(Clone, Copy, Debug, PartialEq)] pub struct SceneTri { pub p: [Vec3; 3], /// Unit normal; lighting is two-sided. pub normal: Vec3, pub color: Rgb, } /// A translucent triangle (a datum plane's fill): drawn after the opaque faces, depth-tested /// against them without hiding what is behind, blended at `alpha` (0–1). #[derive(Clone, Copy, Debug, PartialEq)] pub struct SceneFill { pub tri: SceneTri, pub alpha: f64, } #[derive(Clone, Copy, Debug, PartialEq)] pub struct SceneLine { pub a: Vec3, pub b: Vec3, pub color: Rgb, /// Width in output pixels. pub width: f64, } /// A node marker: a square `size` output pixels across. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScenePoint { pub p: Vec3, pub color: Rgb, pub size: f64, } /// Text beside a point (a reference point's name), drawn over everything else. The GPU viewport /// draws labels with the interface's font; the CPU rasterizer with [`crate::font`]. #[derive(Clone, Debug, PartialEq)] pub struct SceneLabel { pub p: Vec3, pub text: String, pub color: Rgb, } #[derive(Clone, Debug, Default, PartialEq)] pub struct Scene { pub tris: Vec, /// Translucent triangles ([`SceneFill`]). pub fills: Vec, pub lines: Vec, pub points: Vec, pub labels: Vec, /// Lines drawn over everything, after the depth-tested layers (a concentrated load at a /// point inside the part stays visible from any side). A zero-length one is a round dot. pub on_top: Vec, /// Background gradient top → bottom. pub background: Option<(Rgb, Rgb)>, /// Scene radius around the camera target, for the clipping planes. pub radius: f64, } impl Scene { /// Add `other`'s primitives (not its background or radius) to this scene. pub fn append(&mut self, other: Scene) { self.tris.extend(other.tris); self.fills.extend(other.fills); self.lines.extend(other.lines); self.points.extend(other.points); self.labels.extend(other.labels); self.on_top.extend(other.on_top); } /// Line widths and point sizes (given in points) in pixels at `pixels_per_point`, as the GPU /// viewport draws them: a CPU render at device resolution then matches the window. pub fn scale_marks(&mut self, pixels_per_point: f64) { if !(pixels_per_point.is_finite() && pixels_per_point > 0.0) { return; } for l in self.lines.iter_mut().chain(self.on_top.iter_mut()) { l.width *= pixels_per_point; } for p in &mut self.points { p.size *= pixels_per_point; } } } /// The lit colour of a surface with normal `n` seen along `view_dir` (toward the eye). The GPU /// shader computes the same. pub fn shade(n: Vec3, view_dir: Vec3, base: [f64; 3]) -> [f64; 3] { let n = if n.dot(view_dir) < 0.0 { -n } else { n }; let key = (view_dir * 0.8 + Vec3::new(-0.3, 0.2, 0.6)).normalized().unwrap_or(view_dir); let diff = n.dot(key).max(0.0); let fill = n.dot(view_dir).max(0.0); let h = (key + view_dir).normalized().unwrap_or(view_dir); let spec = n.dot(h).max(0.0).powi(40) * 0.25; let k = 0.42 + 0.38 * diff + 0.25 * fill; [(base[0] * k + spec).min(1.0), (base[1] * k + spec).min(1.0), (base[2] * k + spec).min(1.0)] } /// RGBA pixels plus depth. pub struct Canvas { pub w: usize, pub h: usize, pub rgba: Vec, depth: Vec, } const SS: usize = 2; /// Largest output side in pixels. pub const MAX_DIM: usize = 8192; /// Most output pixels (`w × h`): 16 MP, drawn at 2× into 64 MP of colour and depth (~512 MB). pub const MAX_PIXELS: usize = 4096 * 4096; /// Most primitives (triangles, lines and points together) one render draws. pub const MAX_PRIMITIVES: usize = 20_000_000; /// Why a render was refused. #[derive(Clone, Debug, PartialEq, Eq)] pub enum RenderError { /// The image size is zero or over [`MAX_DIM`] per side or [`MAX_PIXELS`] in all. Size { w: usize, h: usize }, /// The scene has more than [`MAX_PRIMITIVES`] primitives. SceneTooLarge { primitives: usize }, /// The image buffers could not be allocated. OutOfMemory, /// The PNG encoder failed. Encode, } impl std::fmt::Display for RenderError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RenderError::Size { w, h } => { write!(f, "image size {w} x {h} is out of range (1..={MAX_DIM} per side, at most {MAX_PIXELS} pixels)") } RenderError::SceneTooLarge { primitives } => { write!(f, "the scene has {primitives} primitives; the CPU renderer draws at most {MAX_PRIMITIVES}") } RenderError::OutOfMemory => f.write_str("not enough memory for the image"), RenderError::Encode => f.write_str("PNG encoding failed"), } } } impl std::error::Error for RenderError {} /// Whether `w × h` is an image size [`render`] accepts. fn size_ok(w: usize, h: usize) -> bool { (1..=MAX_DIM).contains(&w) && (1..=MAX_DIM).contains(&h) && w.checked_mul(h).is_some_and(|n| n <= MAX_PIXELS) } /// The largest size with the aspect of `w × h` that [`render`] accepts (for drawing a window of /// any size: the image is scaled up on screen). pub fn fit_size(w: usize, h: usize) -> (usize, usize) { let (w, h) = (w.max(1), h.max(1)); if size_ok(w, h) { return (w, h); } let by_side = MAX_DIM as f64 / w.max(h) as f64; let by_area = (MAX_PIXELS as f64 / (w as f64 * h as f64)).sqrt(); let k = by_side.min(by_area); let fit = |v: usize| ((v as f64 * k).floor() as usize).clamp(1, MAX_DIM); (fit(w), fit(h)) } /// Largest half-width of a line or point, in supersampled pixels. const MAX_MARK: f64 = 64.0; /// Most labels drawn, and most characters drawn of one. const MAX_LABELS: usize = 200; const MAX_LABEL_CHARS: usize = 48; /// Output pixels per font pixel, and a label's offset from its point (output pixels). const LABEL_SCALE: usize = 2; pub const LABEL_OFFSET: f64 = 8.0; /// Most samples drawn along one line segment (a segment from far off screen is bounded). const MAX_LINE_STEPS: usize = 20_000; impl Canvas { /// A white canvas; the buffers are reserved fallibly, so a size the machine cannot hold is /// an error rather than an abort. fn new(w: usize, h: usize) -> Result { let n = w.checked_mul(h).ok_or(RenderError::Size { w, h })?; let bytes = n.checked_mul(4).ok_or(RenderError::Size { w, h })?; let mut rgba = Vec::new(); rgba.try_reserve_exact(bytes).map_err(|_| RenderError::OutOfMemory)?; rgba.resize(bytes, 255); let mut depth = Vec::new(); depth.try_reserve_exact(n).map_err(|_| RenderError::OutOfMemory)?; depth.resize(n, f32::INFINITY); Ok(Canvas { w, h, rgba, depth }) } fn put(&mut self, x: usize, y: usize, c: [f64; 3], a: f64) { let i = (y * self.w + x) * 4; if let Some(px) = self.rgba.get_mut(i..i + 3) { for (k, v) in px.iter_mut().enumerate() { let old = *v as f64 / 255.0; *v = ((old * (1.0 - a) + c[k] * a).clamp(0.0, 1.0) * 255.0).round() as u8; } } } } /// Render a scene with the camera into an RGBA canvas of `w × h` pixels. Sizes outside /// 1..=[`MAX_DIM`] per side or over [`MAX_PIXELS`], and scenes over [`MAX_PRIMITIVES`], are /// refused ([`fit_size`] gives an accepted size). pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Result { render_layers(scene, &[], cam, w, h) } /// Depth (of the 0…1 range) an overlay face may lie behind the scene's and still be drawn: it /// covers the faces it repeats. const OVERLAY_DEPTH: f32 = 2e-4; /// The depth bias of lines and points over faces: of the 0…1 range in orthographic views, and /// times eight of the distance to the eye in perspective (the GPU shader's `screen.z`). const DEPTH_BIAS: f64 = 2e-4; /// [`render`] with overlays drawn over the scene (the setup's symbols, a selection highlight): /// their faces over the scene's faces they coincide with (a highlight repeats the faces it /// marks), then every layer's translucent faces blended over them, then every layer's lines and points depth-tested, then every layer's /// [`Scene::on_top`] lines and labels over everything. The overlays' background and radius are /// not used. pub fn render_layers(scene: &Scene, overlays: &[&Scene], cam: &Camera, w: usize, h: usize) -> Result { if !size_ok(w, h) { return Err(RenderError::Size { w, h }); } let count = |s: &Scene| { s.tris .len() .saturating_add(s.fills.len()) .saturating_add(s.lines.len()) .saturating_add(s.points.len()) .saturating_add(s.labels.len()) .saturating_add(s.on_top.len()) }; let primitives = overlays.iter().fold(count(scene), |n, o| n.saturating_add(count(o))); if primitives > MAX_PRIMITIVES { return Err(RenderError::SceneTooLarge { primitives }); } let (sw, sh) = (w * SS, h * SS); let mut c = Canvas::new(sw, sh)?; let (top, bot) = scene.background.unwrap_or((Rgb(236, 240, 245), Rgb(205, 212, 222))); let (a, b) = (top.f(), bot.f()); for y in 0..sh { let t = y as f64 / sh.max(1) as f64; let col = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]; for x in 0..sw { c.put(x, y, col, 1.0); } } let proj = Projector::new(cam, sw as f64, sh as f64, scene.radius); let project = |p: Vec3| proj.screen(p); // Edges, nodes and overlay faces win the depth test against the faces they lie on by a bias // toward the eye, `s` biases of it. Orthographic depth is linear, so a fixed depth offset // is the same everywhere; perspective depth is not (a fixed offset spans a large distance far // from the near plane, which let hidden edges show through, #282), so the point is moved // toward the eye by a fraction of its distance instead, as the GPU shader does. let eye = cam.eye(); let biased = |p: Vec3, s: f64| -> Option<[f64; 3]> { let mut q = project(p)?; if cam.fov > 0.0 { q[2] = project(p + (eye - p) * (DEPTH_BIAS * 8.0 * s))?[2] + DEPTH_BIAS; } Some(q) }; let view_dir = cam.back(); for (t, overlay) in scene.tris.iter().map(|t| (t, false)).chain(overlays.iter().flat_map(|o| &o.tris).map(|t| (t, true))) { let corner = |p: Vec3| if overlay { biased(p, 0.5) } else { project(p) }; let (Some(a), Some(b), Some(cc)) = (corner(t.p[0]), corner(t.p[1]), corner(t.p[2])) else { continue }; let allowance = if overlay { OVERLAY_DEPTH } else { 0.0 }; let col = shade(t.normal, view_dir, t.color.f()); raster_tri(a, b, cc, sw, sh, |x, y, z| { if let Some(d) = c.depth.get_mut(y * sw + x) && z < *d + allowance { *d = d.min(z); c.put(x, y, col, 1.0); } }); } let layers = || std::iter::once(scene).chain(overlays.iter().copied()); // Translucent faces over the opaque ones, without writing depth: what is behind shows. for f in layers().flat_map(|s| &s.fills) { let t = &f.tri; let (Some(a), Some(b), Some(cc)) = (project(t.p[0]), project(t.p[1]), project(t.p[2])) else { continue }; let alpha = if f.alpha.is_finite() { f.alpha.clamp(0.0, 1.0) } else { 0.0 }; let col = shade(t.normal, view_dir, t.color.f()); raster_tri(a, b, cc, sw, sh, |x, y, z| { if c.depth.get(y * sw + x).is_some_and(|d| z < *d) { c.put(x, y, col, alpha); } }); } let line = |c: &mut Canvas, l: &SceneLine, depth_test: bool| { let (Some(a), Some(b)) = (biased(l.a, 1.0), biased(l.b, 1.0)) else { return }; let len = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt(); let steps = if len.is_finite() { (len.ceil() as usize).clamp(1, MAX_LINE_STEPS) } else { return }; let col = l.color.f(); for k in 0..=steps { let t = k as f64 / steps as f64; let p = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]; dot(c, p, l.width * SS as f64, col, true, depth_test); } }; for l in layers().flat_map(|s| &s.lines) { line(&mut c, l, true); } for p in layers().flat_map(|s| &s.points) { if let Some(q) = biased(p.p, 1.0) { dot(&mut c, q, p.size * SS as f64, p.color.f(), false, true); } } for l in layers().flat_map(|s| &s.on_top) { line(&mut c, l, false); } for l in layers().flat_map(|s| &s.labels).take(MAX_LABELS) { if let Some(q) = project(l.p) { label(&mut c, q, &l.text, l.color.f()); } } downsample(&c, w, h) } /// Cover a triangle with screen corners `a`, `b`, `c` (x, y, depth) on a `w × h` pixel grid: /// `pixel(x, y, depth)` for every pixel whose centre is inside it (its edges included), the depth /// interpolated. Degenerate and off-grid triangles cover nothing. The renderer and the picking /// depth map ([`crate::pick::DepthMap`]) share it, so a box selects what is drawn. pub(crate) fn raster_tri(a: [f64; 3], b: [f64; 3], c: [f64; 3], w: usize, h: usize, mut pixel: impl FnMut(usize, usize, f32)) { let (x0, x1) = (a[0].min(b[0]).min(c[0]).floor().max(0.0), a[0].max(b[0]).max(c[0]).ceil().min(w as f64 - 1.0)); let (y0, y1) = (a[1].min(b[1]).min(c[1]).floor().max(0.0), a[1].max(b[1]).max(c[1]).ceil().min(h as f64 - 1.0)); if !(x0 <= x1 && y0 <= y1) { return; } let area = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); if !(area.abs() >= 1e-12) { return; } for y in y0 as usize..=y1 as usize { for x in x0 as usize..=x1 as usize { let (px, py) = (x as f64 + 0.5, y as f64 + 0.5); let w0 = ((b[0] - px) * (c[1] - py) - (b[1] - py) * (c[0] - px)) / area; let w1 = ((c[0] - px) * (a[1] - py) - (c[1] - py) * (a[0] - px)) / area; let w2 = 1.0 - w0 - w1; if w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9 { continue; } pixel(x, y, (w0 * a[2] + w1 * b[2] + w2 * c[2]) as f32); } } } /// Text above and right of screen point `p`, over everything, with a halo of the opposite /// lightness so it reads on any background. fn label(c: &mut Canvas, p: [f64; 3], text: &str, col: [f64; 3]) { if !(p[0].is_finite() && p[1].is_finite()) { return; } let px = LABEL_SCALE * SS; let (x0, y0) = (p[0] + LABEL_OFFSET * SS as f64, p[1] - LABEL_OFFSET * SS as f64 - (font::H * px) as f64); let (sw, sh) = (c.w as f64, c.h as f64); if x0 > sw || y0 > sh || x0 < -sw || y0 < -sh { return; } let luma = 0.3 * col[0] + 0.6 * col[1] + 0.1 * col[2]; let halo = if luma > 0.5 { [0.0; 3] } else { [1.0; 3] }; let (x0, y0) = (x0.floor() as i64, y0.floor() as i64); let chars: Vec = text.chars().take(MAX_LABEL_CHARS).collect(); // The halo, spread a supersampled pixel pair around each font pixel, then the text. for (grow, color, a) in [(SS as i64, halo, 0.7), (0, col, 1.0)] { for (i, ch) in chars.iter().enumerate() { for fy in 0..font::H { for fx in (0..font::W).filter(|&fx| font::on(*ch, fx, fy)) { let (bx, by) = (x0 + ((i * font::ADVANCE + fx) * px) as i64, y0 + (fy * px) as i64); for y in by - grow..by + px as i64 + grow { for x in bx - grow..bx + px as i64 + grow { if x >= 0 && y >= 0 && (x as usize) < c.w && (y as usize) < c.h { c.put(x as usize, y as usize, color, a); } } } } } } } } /// A disc (`round`) or square of diameter `size` at screen point `p` (x, y, depth), depth-tested /// unless `depth_test` is off. fn dot(c: &mut Canvas, p: [f64; 3], size: f64, col: [f64; 3], round: bool, depth_test: bool) { // Widths are capped so a hostile width cannot make the loop below unbounded. let half = if size.is_finite() { (size * 0.5).clamp(0.5, MAX_MARK) } else { 0.5 }; let r = half.ceil() as i64; let (sw, sh) = (c.w as i64, c.h as i64); if !(p[0].is_finite() && p[1].is_finite()) || p[0] < -half || p[1] < -half || p[0] > sw as f64 + half || p[1] > sh as f64 + half { return; } for dy in -r..=r { for dx in -r..=r { let (ix, iy) = (p[0].floor() as i64 + dx, p[1].floor() as i64 + dy); if ix < 0 || iy < 0 || ix >= sw || iy >= sh { continue; } let (ox, oy) = (ix as f64 + 0.5 - p[0], iy as f64 + 0.5 - p[1]); let d = if round { (ox * ox + oy * oy).sqrt() } else { ox.abs().max(oy.abs()) }; let cov = (half + 0.5 - d).clamp(0.0, 1.0); let i = iy as usize * c.w + ix as usize; // Lines and points sit on faces: a small depth allowance keeps them in front. if cov > 0.0 && (!depth_test || c.depth.get(i).is_some_and(|dd| (p[2] as f32) <= *dd + 2e-4)) { c.put(ix as usize, iy as usize, col, cov); } } } } fn downsample(c: &Canvas, w: usize, h: usize) -> Result { let mut out = Canvas::new(w, h)?; for y in 0..h { for x in 0..w { let mut acc = [0u32; 4]; for sy in 0..SS { for sx in 0..SS { let i = ((y * SS + sy) * c.w + x * SS + sx) * 4; for (k, a) in acc.iter_mut().enumerate() { *a += c.rgba.get(i + k).copied().unwrap_or(0) as u32; } } } let o = (y * w + x) * 4; for (k, a) in acc.iter().enumerate() { if let Some(v) = out.rgba.get_mut(o + k) { *v = (*a / (SS * SS) as u32) as u8; } } } } Ok(out) } /// Render to PNG bytes. pub fn render_png(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Result, RenderError> { render_layers_png(scene, &[], cam, w, h) } /// [`render_layers`] as PNG bytes. pub fn render_layers_png(scene: &Scene, overlays: &[&Scene], cam: &Camera, w: usize, h: usize) -> Result, RenderError> { let c = render_layers(scene, overlays, cam, w, h)?; png(c.w, c.h, &c.rgba).ok_or(RenderError::Encode) } /// RGBA pixels (`w × h`, 8 bits per channel) to PNG bytes; `None` when the sizes disagree. pub fn png(w: usize, h: usize, rgba: &[u8]) -> Option> { if w.checked_mul(h)?.checked_mul(4)? != rgba.len() || w == 0 || h == 0 { return None; } let mut buf = Vec::new(); { let mut enc = png::Encoder::new(&mut buf, u32::try_from(w).ok()?, u32::try_from(h).ok()?); enc.set_color(png::ColorType::Rgba); enc.set_depth(png::BitDepth::Eight); let mut writer = enc.write_header().ok()?; writer.write_image_data(rgba).ok()?; writer.finish().ok()?; } Some(buf) } #[cfg(test)] mod tests { use super::*; use crate::StandardView; fn red_triangle() -> Scene { Scene { tris: vec![SceneTri { p: [Vec3::new(-10.0, 0.0, -10.0), Vec3::new(10.0, 0.0, -10.0), Vec3::new(0.0, 0.0, 10.0)], normal: Vec3::new(0.0, -1.0, 0.0), color: Rgb(200, 30, 30), }], radius: 20.0, ..Default::default() } } #[test] fn renders_a_triangle_lines_and_points() { let mut scene = red_triangle(); let mut cam = Camera { distance: 50.0, ..Default::default() }; cam.set_view(StandardView::Front); let c = render(&scene, &cam, 64, 64).unwrap(); let px = |c: &Canvas, x: usize, y: usize| c.rgba[(y * c.w + x) * 4..(y * c.w + x) * 4 + 4].to_vec(); let centre = px(&c, 32, 32); assert!(centre[0] > centre[1] + 50, "centre pixel is red: {centre:?}"); // A black line across the middle, in front of the triangle, and a point. scene.lines.push(SceneLine { a: Vec3::new(-10.0, -0.1, 0.0), b: Vec3::new(10.0, -0.1, 0.0), color: Rgb(0, 0, 0), width: 3.0 }); scene.points.push(ScenePoint { p: Vec3::new(0.0, -0.1, 5.0), color: Rgb(0, 0, 255), size: 5.0 }); let c = render(&scene, &cam, 64, 64).unwrap(); assert!(px(&c, 32, 32)[0] < 40, "{:?}", px(&c, 32, 32)); let (x, y, _) = cam.to_screen(Vec3::new(0.0, 0.0, 5.0), 64.0, 64.0, 20.0).unwrap(); let p = px(&c, x as usize, y as usize); assert!(p[2] > 200 && p[0] < 60, "{p:?}"); } /// A translucent face tints what is behind it and hides nothing; one behind an opaque face /// does not show (#302). #[test] fn translucent_faces_blend_over_what_is_in_front_only() { let mut cam = Camera { distance: 50.0, ..Default::default() }; cam.set_view(StandardView::Front); let px = |c: &Canvas| c.rgba[(32 * c.w + 32) * 4..(32 * c.w + 32) * 4 + 3].to_vec(); let red = red_triangle(); let plain = px(&render(&red, &cam, 64, 64).unwrap()); let fill = |y: f64| SceneFill { tri: SceneTri { p: red.tris[0].p.map(|p| Vec3::new(p.x, y, p.z)), color: Rgb(0, 0, 255), ..red.tris[0] }, alpha: 0.25, }; let mut front = red.clone(); front.fills.push(fill(-1.0)); let tinted = px(&render(&front, &cam, 64, 64).unwrap()); assert!(tinted[2] > plain[2] + 20 && tinted[0] > 120, "red still shows, tinted blue: {tinted:?} over {plain:?}"); let mut back = red.clone(); back.fills.push(fill(1.0)); assert_eq!(px(&render(&back, &cam, 64, 64).unwrap()), plain); } /// In perspective, close to the part (the near plane at its smallest), a line a little /// behind a face stays hidden and one on it shows (#282). #[test] fn perspective_hides_lines_behind_faces() { let mut scene = red_triangle(); scene.radius = 40.0; let mut cam = Camera { distance: 30.0, fov: 0.6, ..Default::default() }; cam.set_view(StandardView::Front); let px = |c: &Canvas| c.rgba[(32 * c.w + 32) * 4..(32 * c.w + 32) * 4 + 3].to_vec(); // Front view looks along +y: y = 1 is behind the face, 3 % of the distance. scene.lines.push(SceneLine { a: Vec3::new(-10.0, 1.0, 0.0), b: Vec3::new(10.0, 1.0, 0.0), color: Rgb(0, 0, 0), width: 3.0 }); let behind = render(&scene, &cam, 64, 64).unwrap(); assert!(px(&behind)[0] > 150, "hidden: {:?}", px(&behind)); scene.lines[0].a.y = 0.0; scene.lines[0].b.y = 0.0; let on = render(&scene, &cam, 64, 64).unwrap(); assert!(px(&on)[0] < 60, "on the face: {:?}", px(&on)); } /// An overlay face repeating a scene face covers it; one behind another face does not show. #[test] fn an_overlay_covers_the_faces_it_repeats() { let scene = red_triangle(); let mut cam = Camera { distance: 50.0, ..Default::default() }; cam.set_view(StandardView::Front); let px = |c: &Canvas| c.rgba[(32 * c.w + 32) * 4..(32 * c.w + 32) * 4 + 3].to_vec(); let green = |s: &Scene| Scene { tris: s.tris.iter().map(|t| SceneTri { color: Rgb(0, 255, 0), ..*t }).collect(), ..Scene::default() }; let over = render_layers(&scene, &[&green(&scene)], &cam, 64, 64).unwrap(); assert!(px(&over)[1] > px(&over)[0] + 50, "{:?}", px(&over)); // The same face moved behind the red one (front view looks along +y) stays hidden. let mut behind = green(&scene); for t in &mut behind.tris { t.p = t.p.map(|p| p + Vec3::Y * 5.0); } let hidden = render_layers(&scene, &[&behind], &cam, 64, 64).unwrap(); assert_eq!(px(&hidden), px(&render(&scene, &cam, 64, 64).unwrap())); } /// A line behind a face is hidden; the same line on top is drawn over it. #[test] fn on_top_lines_draw_over_the_faces_in_front_of_them() { let scene = red_triangle(); let mut cam = Camera { distance: 50.0, ..Default::default() }; cam.set_view(StandardView::Front); let px = |c: &Canvas| c.rgba[(32 * c.w + 32) * 4..(32 * c.w + 32) * 4 + 3].to_vec(); // Behind the triangle (the front view looks along +y). let line = SceneLine { a: Vec3::new(-10.0, 5.0, 0.0), b: Vec3::new(10.0, 5.0, 0.0), color: Rgb(0, 0, 255), width: 4.0 }; let behind = Scene { lines: vec![line], ..Scene::default() }; let hidden = render_layers(&scene, &[&behind], &cam, 64, 64).unwrap(); assert_eq!(px(&hidden), px(&render(&scene, &cam, 64, 64).unwrap())); let over = Scene { on_top: vec![line], ..Scene::default() }; let shown = render_layers(&scene, &[&over], &cam, 64, 64).unwrap(); assert!(px(&shown)[2] > 200 && px(&shown)[0] < 60, "{:?}", px(&shown)); } #[test] fn png_bytes_and_hostile_sizes() { let scene = red_triangle(); let cam = Camera::default(); assert!(render_png(&scene, &cam, 16, 16).unwrap().starts_with(&[0x89, b'P', b'N', b'G'])); // Out-of-range sizes are refused before anything is allocated. for (w, h) in [(0, 16), (16, 0), (0, usize::MAX), (MAX_DIM + 1, 16), (100_000, 100_000), (usize::MAX, usize::MAX), (8192, 8192)] { assert_eq!(render(&scene, &cam, w, h).err(), Some(RenderError::Size { w, h })); } assert!(render(&scene, &cam, MAX_DIM, MAX_PIXELS / MAX_DIM).is_ok_and(|c| c.w == MAX_DIM)); assert!(png(2, 2, &[0; 3]).is_none()); assert!(png(usize::MAX, 2, &[]).is_none()); // Far-away and non-finite geometry is skipped or bounded, never a panic. let wild = Scene { lines: vec![SceneLine { a: Vec3::new(-1e12, 0.0, 0.0), b: Vec3::new(1e12, 0.0, 0.0), color: Rgb(0, 0, 0), width: 1e6 }], points: vec![ScenePoint { p: Vec3::new(f64::NAN, 0.0, 0.0), color: Rgb(0, 0, 0), size: 4.0 }], radius: f64::NAN, ..scene }; let _ = render(&wild, &cam, 32, 32); } #[test] fn labels_draw_over_everything_beside_their_point() { let mut scene = red_triangle(); let mut cam = Camera { distance: 50.0, ..Default::default() }; cam.set_view(StandardView::Front); let plain = render(&scene, &cam, 64, 64).unwrap(); scene.labels.push(SceneLabel { p: Vec3::new(0.0, 0.0, 0.0), text: "RP".into(), color: Rgb(0, 0, 0) }); let c = render(&scene, &cam, 64, 64).unwrap(); // Up and right of the centre, some pixels turned dark; left of it nothing changed. let dark = |c: &Canvas, x0: usize, x1: usize| { (x0..x1).flat_map(|x| (0..32).map(move |y| (x, y))).filter(|&(x, y)| c.rgba[(y * 64 + x) * 4] < 40).count() }; assert!(dark(&c, 32, 64) > 10 && dark(&plain, 32, 64) == 0, "{}", dark(&c, 32, 64)); assert_eq!(dark(&c, 0, 32), 0); // Labels far off screen, empty or very long, or at a non-finite point, are bounded. scene.labels = vec![ SceneLabel { p: Vec3::new(1e9, 0.0, 0.0), text: "far".into(), color: Rgb(0, 0, 0) }, SceneLabel { p: Vec3::new(f64::NAN, 0.0, 0.0), text: "nan".into(), color: Rgb(0, 0, 0) }, SceneLabel { p: Vec3::ZERO, text: String::new(), color: Rgb(255, 255, 255) }, SceneLabel { p: Vec3::ZERO, text: "x".repeat(100_000), color: Rgb(255, 255, 255) }, ]; assert!(render(&scene, &cam, 64, 64).is_ok()); } #[test] fn huge_scenes_are_refused() { let line = SceneLine { a: Vec3::ZERO, b: Vec3::X, color: Rgb(0, 0, 0), width: 1.0 }; let scene = Scene { lines: vec![line; MAX_PRIMITIVES + 1], radius: 1.0, ..Default::default() }; let r = render_png(&scene, &Camera::default(), 16, 16); assert_eq!(r, Err(RenderError::SceneTooLarge { primitives: MAX_PRIMITIVES + 1 })); } #[test] fn fit_size_keeps_the_aspect_within_the_limits() { assert_eq!(fit_size(1280, 800), (1280, 800)); assert_eq!(fit_size(0, 0), (1, 1)); for (w, h) in [(7680, 4320), (100_000, 100_000), (usize::MAX, 3), (20_000, 10)] { let (fw, fh) = fit_size(w, h); assert!(size_ok(fw, fh), "{w}x{h} -> {fw}x{fh}"); } let (fw, fh) = fit_size(7680, 4320); assert!(((fw as f64 / fh as f64) - 16.0 / 9.0).abs() < 1e-2, "{fw}x{fh}"); } }