use crate::{Color, Pixmap, Scene};
use oxideav_raster::Renderer;
use stipple_geometry::{PhysicalSize, Rect, ScaleFactor};
#[derive(Debug, Default)]
pub struct SoftwareRenderer {
background: Color,
}
impl SoftwareRenderer {
pub fn new() -> Self {
Self::default()
}
pub fn with_background(mut self, color: Color) -> Self {
self.background = color;
self
}
pub fn render(&self, scene: Scene, scale: ScaleFactor) -> Pixmap {
let physical = scale.to_physical(scene.logical_size());
self.render_at(scene, physical)
}
pub fn render_at(&self, scene: Scene, physical: PhysicalSize) -> Pixmap {
let frame = scene.into_vector_frame();
self.rasterize_frame(frame, physical)
}
pub fn render_region(&self, scene: Scene, view: Rect, physical: PhysicalSize) -> Pixmap {
let frame = scene.into_vector_frame_region(view);
self.rasterize_frame(frame, physical)
}
fn rasterize_frame(&self, frame: oxideav_core::VectorFrame, physical: PhysicalSize) -> Pixmap {
let (w, h) = (physical.width.max(1), physical.height.max(1));
let mut renderer = Renderer::new(w, h);
renderer.background = self.background.to_oxideav();
let video = renderer.render(&frame);
let plane = &video.planes[0];
let dst_stride = w as usize * 4;
let total = dst_stride * h as usize;
let mut data = vec![0u8; total];
if plane.stride == dst_stride {
data.copy_from_slice(&plane.data[..total]);
} else {
for y in 0..h as usize {
let src = &plane.data[y * plane.stride..y * plane.stride + dst_stride];
data[y * dst_stride..(y + 1) * dst_stride].copy_from_slice(src);
}
}
Pixmap::from_rgba8(PhysicalSize::new(w, h), data)
}
}
#[cfg(test)]
mod tests {
use super::*;
use stipple_geometry::{Rect, Size};
#[test]
fn fills_pixel_at_expected_location() {
let mut scene = Scene::new(Size::new(100.0, 100.0));
scene.fill_rect(
Rect::from_xywh(0.0, 0.0, 100.0, 100.0),
Color::rgb(255, 0, 0),
);
let pm = SoftwareRenderer::new().render(scene, ScaleFactor::IDENTITY);
assert_eq!(pm.size(), PhysicalSize::new(100, 100));
let [r, g, b, a] = pm.pixel(50, 50).unwrap();
assert_eq!((r, g, b, a), (255, 0, 0, 255));
}
#[test]
fn hidpi_scale_doubles_resolution() {
let scene = Scene::new(Size::new(100.0, 80.0));
let pm = SoftwareRenderer::new().render(scene, ScaleFactor::new(2.0));
assert_eq!(pm.size(), PhysicalSize::new(200, 160));
}
#[test]
fn render_region_matches_full_render_within_the_region() {
let make = || {
let mut s = Scene::new(Size::new(100.0, 100.0));
s.fill_rect(
Rect::from_xywh(10.0, 10.0, 20.0, 20.0),
Color::rgb(255, 0, 0),
);
s.fill_rect(
Rect::from_xywh(70.0, 70.0, 20.0, 20.0),
Color::rgb(0, 0, 255),
);
s
};
let r = SoftwareRenderer::new().with_background(Color::rgb(0, 0, 0));
let full = r.render(make(), ScaleFactor::IDENTITY);
let view = Rect::from_xywh(10.0, 10.0, 20.0, 20.0);
let region = r.render_region(make(), view, PhysicalSize::new(20, 20));
assert_eq!(region.size(), PhysicalSize::new(20, 20));
for y in 0..20u32 {
for x in 0..20u32 {
assert_eq!(
region.pixel(x, y),
full.pixel(10 + x, 10 + y),
"mismatch at region ({x},{y})"
);
}
}
assert_eq!(region.pixel(10, 10), Some([255, 0, 0, 255]));
}
}