Merge main into land/1230

This commit is contained in:
Tanish Manku
2026-11-03 17:11:25 -07:00
37 changed files with 2582 additions and 21 deletions
+52
View File
@@ -22,6 +22,51 @@ pub enum Kind {
Point,
/// An infinite line or ray: `verts[start]` = base, `verts[start+1]` = unit direction.
Infinite { ray: bool },
/// A raster image, [`DisplayList::images`]`[index]`; `verts[start..start+4]` are its corners
/// (top left, top right, bottom right, bottom left of the pixel grid).
Image { index: u32 },
}
/// A raster image placed in the world.
#[derive(Clone, Debug, PartialEq)]
pub struct DImage {
pub raster: std::sync::Arc<cadcraft_doc::Raster>,
/// Where the top-left corner of the top-left pixel is.
pub origin: Vec2,
/// One pixel to the right and one pixel down, in world units.
pub right: Vec2,
pub down: Vec2,
/// Closed outlines the image is cut to (all of them: its own clip boundary, then the
/// viewports it is seen through). Empty: the whole image.
pub clips: Vec<Vec<Vec2>>,
/// Fade towards the background, percent (0..=100).
pub fade: u8,
}
impl DImage {
/// The world position of pixel-grid point (`x`, `y`), `y` down from the top.
pub fn at(&self, x: f64, y: f64) -> Vec2 {
self.origin + self.right * x + self.down * y
}
/// The pixel-grid point at world position `q` (the inverse of [`DImage::at`]); `None` for an
/// image squashed to a line or a point.
pub fn pixel_of(&self, q: Vec2) -> Option<Vec2> {
let det = self.right.x * self.down.y - self.right.y * self.down.x;
if !(det.is_finite() && det.abs() > 0.0) {
return None;
}
let d = q - self.origin;
Some(Vec2::new((d.x * self.down.y - d.y * self.down.x) / det, (self.right.x * d.y - self.right.y * d.x) / det))
}
/// How opaque the image is drawn, 0..=1: the object's opacity `alpha` (0..=255) less the fade.
pub fn opacity(&self, alpha: u8) -> f32 {
f32::from(alpha) / 255.0 * (1.0 - f32::from(self.fade.min(100)) / 100.0)
}
/// The corners: top left, top right, bottom right, bottom left.
pub fn corners(&self) -> [Vec2; 4] {
let (w, h) = (f64::from(self.raster.width()), f64::from(self.raster.height()));
[self.at(0.0, 0.0), self.at(w, 0.0), self.at(w, h), self.at(0.0, h)]
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -46,6 +91,8 @@ pub struct DisplayList {
pub prims: Vec<DPrim>,
pub verts: Vec<Vec2>,
pub tris: Vec<Vec2>,
/// The raster images drawn by [`Kind::Image`] primitives.
pub images: Vec<DImage>,
/// Plots ([`build_plot`]): for each primitive, the index in [`Drawing::layers`] of the layer
/// it is drawn on (block contents on layer 0: the block reference's; [`NO_LAYER`] when the
/// layer isn't in the table), for PDF layers. Empty for other lists.
@@ -135,14 +182,19 @@ impl DisplayList {
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn append(dst: &mut DisplayList, src: DisplayList) {
let (verts, tris, prims) = (dst.verts.len() as u32, dst.tris.len() as u32, dst.prims.len() as u32);
let images = dst.images.len() as u32;
// Pens are parallel to the primitives, with missing entries meaning "use object".
if !src.pens.is_empty() {
dst.pens.resize(dst.prims.len(), Pen::default());
}
dst.prims.extend(src.prims.into_iter().map(|mut p| {
p.start += if matches!(p.kind, Kind::Tris | Kind::Mask) { tris } else { verts };
if let Kind::Image { index } = &mut p.kind {
*index = index.saturating_add(images);
}
p
}));
dst.images.extend(src.images);
dst.verts.extend(src.verts);
dst.tris.extend(src.tris);
dst.prim_layers.extend(src.prim_layers);
+18 -8
View File
@@ -193,14 +193,7 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
}
}
EntityKind::Wipeout(w) => draw_wipeout(b, ctx, w, rgb, lw),
EntityKind::Image(i) => {
let o = i.insert.xy();
let u = i.u.xy() * i.size.x;
let v = i.v.xy() * i.size.y;
b.polyline(ctx, rgb, lw, &[o, o + u, o + u + v, o + v, o]);
b.polyline(ctx, rgb, lw, &[o, o + u + v]);
b.polyline(ctx, rgb, lw, &[o + u, o + v]);
}
EntityKind::Image(i) => draw_image(b, ctx, i, rgb, lw),
EntityKind::Table(t) => table(b, ctx, t, rgb, lw),
EntityKind::Tolerance(t) => tolerance::draw(b, ctx, e, t, lw, tol),
EntityKind::Leader(l) => {
@@ -289,6 +282,23 @@ fn draw_mleader(b: &mut Builder, ctx: &Ctx, e: &Entity, m: &cadcraft_doc::MLeade
}
}
fn draw_image(b: &mut Builder, ctx: &Ctx, i: &cadcraft_doc::Image, rgb: Ink, lw: f32) {
let o = i.insert.xy();
let u = i.u.xy() * i.size.x;
let v = i.v.xy() * i.size.y;
// Display flag 1: show the image. Without pixels (file not found, not loaded yet, hidden)
// the rectangle is crossed out.
let raster = if i.display & 1 != 0 { ctx.d.rasters.get(&i.path) } else { None };
if let Some(r) = raster {
b.image(ctx, rgb, i, r);
}
b.polyline(ctx, rgb, lw, &[o, o + u, o + u + v, o + v, o]);
if raster.is_none() {
b.polyline(ctx, rgb, lw, &[o, o + u + v]);
b.polyline(ctx, rgb, lw, &[o + u, o + v]);
}
}
fn draw_wipeout(b: &mut Builder, ctx: &Ctx, w: &cadcraft_doc::Wipeout, rgb: Ink, lw: f32) {
// Hide what was drawn before it (draw order), then the frame per WIPEOUTFRAME:
// 0 = none, 1 = shown and plotted, 2 = shown but not plotted.
+57 -1
View File
@@ -186,6 +186,45 @@ impl Builder<'_> {
len: pts.len() as u32,
});
}
/// The pixels of image `i` over its rectangle, cut to its clip boundary.
fn image(&mut self, ctx: &Ctx, color: Ink, i: &cadcraft_doc::Image, raster: &std::sync::Arc<cadcraft_doc::Raster>) {
// Where the entity puts its picture: the top-left corner, and one of its pixels to the
// right and down.
let top_left = ctx.xf.apply(i.insert.xy() + i.v.xy() * i.size.y);
let right = ctx.xf.apply_vec(i.u.xy());
let down = ctx.xf.apply_vec(i.v.xy() * -1.0);
let clip = image_clip(i, |q| top_left + right * q.x + down * q.y);
// The file found can have another pixel count than the entity records: its pixels are
// stretched over the same rectangle.
let img = DImage {
raster: raster.clone(),
origin: top_left,
right: right * (i.size.x / f64::from(raster.width())),
down: down * (i.size.y / f64::from(raster.height())),
clips: clip.into_iter().collect(),
fade: i.fade.min(100),
};
let corners = img.corners();
if !corners.iter().all(|q| q.x.is_finite() && q.y.is_finite()) {
return;
}
let (Ok(index), Ok(start)) = (u32::try_from(self.list.images.len()), u32::try_from(self.list.verts.len())) else { return };
for q in corners {
self.list.bounds.add(q);
self.list.verts.push(q);
}
self.list.images.push(img);
self.list.prims.push(DPrim {
handle: ctx.top,
color: color.rgb,
aci7: color.aci7,
lw: 0.0,
transparency: self.transparency,
kind: Kind::Image { index },
start,
len: 4,
});
}
fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) {
self.tris_at(ctx, color, tris, ctx.xf);
}
@@ -302,6 +341,23 @@ impl Builder<'_> {
}
}
/// The clip boundary of image `i` as a closed outline, or `None` when the whole image shows.
/// `place` gives the position of a point of the entity's pixel grid (x right, y down from the
/// top-left corner of the image).
fn image_clip(i: &cadcraft_doc::Image, place: impl Fn(Vec2) -> Vec2) -> Option<Vec<Vec2>> {
if !i.clipping {
return None;
}
// Boundaries count from pixel centres: the top-left corner of the image is (-0.5, -0.5).
let at = |q: Vec2| place(Vec2::new(q.x + 0.5, q.y + 0.5));
match i.clip.as_slice() {
// Two opposite corners of a rectangle.
[a, b] => Some(vec![at(*a), at(Vec2::new(b.x, a.y)), at(*b), at(Vec2::new(a.x, b.y))]),
polygon if polygon.len() >= 3 => Some(polygon.iter().map(|q| at(*q)).collect()),
_ => None,
}
}
mod table;
pub use table::{MAX_LAYOUT_CELLS, TableFill, TableGeom, TableLine, TableText, table_covered, table_geometry, table_style_of};
@@ -315,7 +371,7 @@ mod text_entity;
mod viewports;
pub use dim_draw::{dim_text, dimension_in};
pub use display::{DPrim, DisplayList, Kind, MAX_TRANSPARENCY, NO_LAYER, PlotText, VIEWPORT_CONTENT, alpha};
pub use display::{DImage, DPrim, DisplayList, Kind, MAX_TRANSPARENCY, NO_LAYER, PlotText, VIEWPORT_CONTENT, alpha};
pub use pipeline::{Options, build, build_entities, build_plot};
use props::{default_lineweight, resolve_common};
use text_entity::text_style;
+97 -1
View File
@@ -4,7 +4,7 @@ use cadcraft_color::Rgb;
use cadcraft_geom::{Bounds2, Vec2};
use tiny_skia::{FillRule, LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform};
use crate::{DisplayList, Kind};
use crate::{DImage, DisplayList, Kind};
/// A 2D view: world point at the image centre and pixels per drawing unit.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -147,6 +147,13 @@ pub fn render(list: &DisplayList, view: &View, o: &RasterOptions) -> Option<Pixm
}
}
}
Kind::Image { index } => {
if let Some(img) = list.images.get(index as usize)
&& Bounds2::from_points(pts.iter().copied()).intersects(&vis)
{
image(&mut pm, img, view, p.alpha(), o.antialias);
}
}
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());
@@ -253,6 +260,95 @@ pub fn blank_outside_pixels(pm: &mut Pixmap, top_left: (f64, f64), bottom_right:
}
}
/// Draw `img` with the object opacity `alpha`, cut to its clip outlines.
fn image(pm: &mut Pixmap, img: &DImage, view: &View, alpha: u8, antialias: bool) {
let r = &img.raster;
let Some(to_screen) = grid_to_screen(img, view) else { return };
let Ok(mask) = clip_mask(pm, img, view, antialias) else { return };
// tiny-skia wants premultiplied pixels; opaque ones already are.
let owned;
let pixels = if r.opaque() {
tiny_skia::PixmapRef::from_bytes(r.rgba(), r.width(), r.height())
} else {
owned = premultiplied(r);
owned.as_ref().map(Pixmap::as_ref)
};
let Some(pixels) = pixels else { return };
let paint = Paint {
shader: tiny_skia::Pattern::new(
pixels,
tiny_skia::SpreadMode::Pad,
tiny_skia::FilterQuality::Bilinear,
img.opacity(alpha),
Transform::identity(),
),
anti_alias: antialias,
..Paint::default()
};
// The picture's own rectangle in its pixel grid, carried to the screen by `to_screen`.
// Filled as a path: tiny-skia's anti-aliased `fill_rect` trips a debug assertion on narrow
// rects (see the points above), which a one-pixel image drawn at its own size would be.
if let Some(rect) = tiny_skia::Rect::from_xywh(0.0, 0.0, r.width() as f32, r.height() as f32) {
pm.fill_path(&PathBuilder::from_rect(rect), &paint, FillRule::Winding, to_screen, mask.as_ref());
}
}
/// The transform from `img`'s pixel grid (x right, y down) to screen pixels; `None` when the
/// image has no area on screen or lies out of number range.
fn grid_to_screen(img: &DImage, view: &View) -> Option<Transform> {
let s = view.scale;
let (x, y) = view.to_screen(img.origin);
// Screen y runs down, world y up.
let ts = Transform::from_row((img.right.x * s) as f32, (-img.right.y * s) as f32, (img.down.x * s) as f32, (-img.down.y * s) as f32, x, y);
(ts.is_finite() && ts.invert().is_some()).then_some(ts)
}
/// The image has a clip outline without area: nothing of it shows.
struct Hidden;
/// The part of the picture `img`'s clip outlines leave visible: `None` for all of it.
fn clip_mask(pm: &Pixmap, img: &DImage, view: &View, antialias: bool) -> Result<Option<tiny_skia::Mask>, Hidden> {
let mut mask: Option<tiny_skia::Mask> = None;
for outline in &img.clips {
let path = outline_path(outline, view).ok_or(Hidden)?;
match &mut mask {
// The first outline starts the mask; each further one narrows it.
None => {
let mut m = tiny_skia::Mask::new(pm.width(), pm.height()).ok_or(Hidden)?;
m.fill_path(&path, FillRule::EvenOdd, antialias, Transform::identity());
mask = Some(m);
}
Some(m) => m.intersect_path(&path, FillRule::EvenOdd, antialias, Transform::identity()),
}
}
Ok(mask)
}
/// A closed world-space outline as a screen path.
fn outline_path(outline: &[Vec2], view: &View) -> Option<tiny_skia::Path> {
let mut pb = PathBuilder::new();
for (i, q) in outline.iter().enumerate() {
let (x, y) = view.to_screen(*q);
if i == 0 {
pb.move_to(x, y);
} else {
pb.line_to(x, y);
}
}
pb.close();
pb.finish()
}
/// The pixels of `r` with each colour multiplied by its alpha.
fn premultiplied(r: &cadcraft_doc::Raster) -> Option<Pixmap> {
let mut data = Vec::with_capacity(r.rgba().len());
for [red, green, blue, a] in r.rgba().as_chunks::<4>().0 {
let m = |c: &u8| ((u16::from(*c) * u16::from(*a) + 127) / 255) as u8;
data.extend_from_slice(&[m(red), m(green), m(blue), *a]);
}
Pixmap::from_vec(data, tiny_skia::IntSize::from_wh(r.width(), r.height())?)
}
/// 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()
+29 -1
View File
@@ -1,6 +1,6 @@
#[cfg(not(target_arch = "wasm32"))]
use super::display::append;
use super::display::{DPrim, DisplayList, Kind, PlotText, VIEWPORT_CONTENT};
use super::display::{DImage, DPrim, DisplayList, Kind, PlotText, VIEWPORT_CONTENT};
use super::draw::entity;
#[cfg(not(target_arch = "wasm32"))]
use super::pipeline::PARALLEL_CHUNK;
@@ -238,6 +238,11 @@ fn append_clipped(dst: &mut DisplayList, src: &DisplayList, clip: &Clip) {
push_raw(dst, p, Kind::Point, &[*q]);
}
}
(Kind::Image { index }, _) => {
if let Some(img) = src.images.get(index as usize) {
push_clipped_image(dst, p, img, pts, clip);
}
}
(Kind::Infinite { ray }, Clip::Rect(rect)) => {
if let (Some(base), Some(dir)) = (pts.first(), pts.get(1))
&& let Some((a, c)) = clip::clip_infinite(*base, *dir, ray, rect)
@@ -256,6 +261,29 @@ fn append_clipped(dst: &mut DisplayList, src: &DisplayList, clip: &Clip) {
}
}
/// Append image `img` (primitive `p`, corners `corners`) to `dst`, to be cut to `clip` when it
/// is drawn: the picture is not cut here, the outline is added to those it carries.
fn push_clipped_image(dst: &mut DisplayList, p: &DPrim, img: &DImage, corners: &[Vec2], clip: &Clip) {
let (outline, area) = match clip {
Clip::Rect(rect) => (vec![rect.min, Vec2::new(rect.max.x, rect.min.y), rect.max, Vec2::new(rect.min.x, rect.max.y)], *rect),
Clip::Poly(c) => (c.polygon().to_vec(), c.bounds()),
};
let own = Bounds2::from_points(corners.iter().copied());
if !own.intersects(&area) {
return;
}
let Ok(index) = u32::try_from(dst.images.len()) else { return };
let mut img = img.clone();
img.clips.push(outline);
dst.images.push(img);
// The list's bounds take only the part inside the clip, as for the other primitives.
let before = dst.bounds;
push_raw(dst, p, Kind::Image { index }, corners);
dst.bounds = before;
dst.bounds.add(own.min.max(area.min));
dst.bounds.add(own.max.min(area.max));
}
fn push_raw(dst: &mut DisplayList, p: &DPrim, kind: Kind, pts: &[Vec2]) {
let min = match kind {
Kind::Polyline => 2,