mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 04:19:59 -06:00
Merge main into land/1230
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user