mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-30 05:46:50 -06:00
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>
140 lines
5.6 KiB
Rust
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()
|
|
}
|