Files
cadcraft/crates/render/src/raster.rs
T
Brandon ThomasandClaude Opus 5.5 d9ab00c53c M0: CADCraft skeleton and vertical slice
Workspace (geom, dxf, color, doc, fonts, render, io, engine, ui-egui, mcp; apps cadcraft,
cadcraft-cli, cadcraft-web; xtask), AutoCAD-for-Mac style UI with code-drawn icons, command line
with prompts/keywords/AutoComplete, draw and modify commands, object snaps, polar/ortho,
selection, undo, layers and properties palettes, DXF read/write, SVG/PNG export, MCP server,
JSON control channel, CLI, wasm build, README, ROADMAP, AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:22:25 -04:00

140 lines
5.6 KiB
Rust

//! CPU rasteriser: draws a display list with tiny-skia (PNG export, plot preview, tests).
use cadcraft_color::{Rgb, display_rgb};
use cadcraft_geom::{Bounds2, Vec2};
use tiny_skia::{FillRule, LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform};
use crate::{DisplayList, Kind};
/// A 2D view: world point at the image centre and pixels per drawing unit.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct View {
pub center: Vec2,
pub scale: f64,
pub width: u32,
pub height: u32,
}
impl View {
pub fn to_screen(&self, p: Vec2) -> (f32, f32) {
let x = (p.x - self.center.x) * self.scale + f64::from(self.width) / 2.0;
let y = f64::from(self.height) / 2.0 - (p.y - self.center.y) * self.scale;
(x as f32, y as f32)
}
/// Fit `b` with a margin fraction.
pub fn fit(b: &Bounds2, width: u32, height: u32, margin: f64) -> View {
if b.is_empty() {
return View { center: Vec2::ZERO, scale: 1.0, width, height };
}
let w = b.width().max(1e-9);
let h = b.height().max(1e-9);
let s = (f64::from(width) / w).min(f64::from(height) / h) * (1.0 - margin);
View { center: b.center(), scale: if s.is_finite() && s > 0.0 { s } else { 1.0 }, width, height }
}
pub fn world_bounds(&self) -> Bounds2 {
let hw = f64::from(self.width) / 2.0 / self.scale;
let hh = f64::from(self.height) / 2.0 / self.scale;
Bounds2::new(self.center - Vec2::new(hw, hh), self.center + Vec2::new(hw, hh))
}
}
#[derive(Clone, Copy, Debug)]
pub struct RasterOptions {
pub background: Rgb,
/// Line width in pixels for zero-lineweight lines.
pub hairline: f32,
/// Pixels per mm for lineweights (0 = ignore lineweights).
pub px_per_mm: f32,
pub antialias: bool,
}
impl Default for RasterOptions {
fn default() -> Self {
RasterOptions { background: Rgb(33, 40, 48), hairline: 1.0, px_per_mm: 0.0, antialias: true }
}
}
/// Render to a pixmap. Returns `None` for zero or absurd sizes.
pub fn render(list: &DisplayList, view: &View, o: &RasterOptions) -> Option<Pixmap> {
if view.width == 0 || view.height == 0 || view.width > 16384 || view.height > 16384 {
return None;
}
let mut pm = Pixmap::new(view.width, view.height)?;
pm.fill(tiny_skia::Color::from_rgba8(o.background.0, o.background.1, o.background.2, 255));
let vis = view.world_bounds();
for p in &list.prims {
let c = display_rgb(p.color, o.background);
let mut paint = Paint::default();
paint.set_color_rgba8(c.0, c.1, c.2, 255);
paint.anti_alias = o.antialias;
let pts = list.points(p);
match p.kind {
Kind::Polyline => {
let bb = Bounds2::from_points(pts.iter().copied());
if !bb.intersects(&vis) {
continue;
}
let mut pb = PathBuilder::new();
for (i, q) in pts.iter().enumerate() {
let (x, y) = view.to_screen(*q);
if i == 0 {
pb.move_to(x, y);
} else {
pb.line_to(x, y);
}
}
if let Some(path) = pb.finish() {
let w = if o.px_per_mm > 0.0 && p.lw > 0.0 { (p.lw * o.px_per_mm).max(o.hairline) } else { o.hairline };
let stroke = Stroke { width: w, line_cap: LineCap::Round, line_join: LineJoin::Round, ..Default::default() };
pm.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
}
Kind::Tris => {
let mut pb = PathBuilder::new();
for t in pts.chunks(3) {
if let [a, b, c] = t {
let (ax, ay) = view.to_screen(*a);
let (bx, by) = view.to_screen(*b);
let (cx, cy) = view.to_screen(*c);
pb.move_to(ax, ay);
pb.line_to(bx, by);
pb.line_to(cx, cy);
pb.close();
}
}
if let Some(path) = pb.finish() {
pm.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None);
}
}
Kind::Point => {
if let Some(q) = pts.first() {
let (x, y) = view.to_screen(*q);
if let Some(r) = tiny_skia::Rect::from_xywh(x - 0.5, y - 0.5, 1.5, 1.5) {
pm.fill_rect(r, &paint, Transform::identity(), None);
}
}
}
Kind::Infinite { ray } => {
let (Some(base), Some(dir)) = (pts.first(), pts.get(1)) else { continue };
let far = (vis.width() + vis.height()) * 2.0 + base.dist(vis.center());
let a = if ray { *base } else { *base - *dir * far };
let b = *base + *dir * far;
let mut pb = PathBuilder::new();
let (ax, ay) = view.to_screen(a);
let (bx, by) = view.to_screen(b);
pb.move_to(ax, ay);
pb.line_to(bx, by);
if let Some(path) = pb.finish() {
pm.stroke_path(&path, &paint, &Stroke { width: o.hairline, ..Default::default() }, Transform::identity(), None);
}
}
}
}
Some(pm)
}
/// Render and encode as PNG.
pub fn render_png(list: &DisplayList, view: &View, o: &RasterOptions) -> Option<Vec<u8>> {
render(list, view, o)?.encode_png().ok()
}