Files
cadcraft/crates/render/src/viewports.rs
T

317 lines
13 KiB
Rust

#[cfg(not(target_arch = "wasm32"))]
use super::display::append;
use super::display::{DImage, DPrim, DisplayList, Kind, PlotText, VIEWPORT_CONTENT};
use super::draw::entity;
#[cfg(not(target_arch = "wasm32"))]
use super::pipeline::PARALLEL_CHUNK;
use super::pipeline::{Options, style_table, top_ctx, with_point_view};
use super::{Bounds2, Builder, Ctx, Drawing, Entity, EntityKind, Handle, MAX_BLOCK_EXPANSION, Mat3, Space, Vec2, clip, paper, pen, plotstyle};
/// Upper bound on viewports drawn per layout (hostile files).
pub(crate) const MAX_VIEWPORTS: usize = 256;
/// A paper-space viewport: its border (as an ordinary entity) and model space seen through it.
/// Viewport id 1 is the paper-space view itself and is not drawn. A clipped viewport shows
/// model space through its clip boundary's outline (the boundary entity draws itself, so the
/// rectangle is not drawn); a viewport turned off shows only its border.
pub(crate) fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewport) {
if !viewport_shown(d, e, vp) {
return;
}
let poly = viewport_clip(d, e.handle, vp);
// Border (on the viewport's layer; layer off hides only the border).
if poly.is_none() {
entity(b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
}
if b.opts.skip_viewport == Some(e.handle) {
return;
}
let unit_mm = paper::unit_mm_of(d, e.handle);
match &poly {
Some(p) => contents(b, d, vp, p.bounds(), &Clip::Poly(p), unit_mm),
None => {
if let Some(rect) = viewport_rect(vp) {
contents(b, d, vp, rect, &Clip::Rect(rect), unit_mm);
}
}
}
}
/// Whether a viewport draws anything: id 1 is the paper-space view itself, and a hidden viewport
/// or one on a frozen layer shows neither its border nor its contents.
fn viewport_shown(d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewport) -> bool {
vp.id != 1 && e.common.visible && !d.is_hidden_object(e.handle) && !d.layer(&e.common.layer).is_some_and(|l| l.frozen)
}
/// A viewport's rectangle on paper; `None` for an unusable size or centre.
fn viewport_rect(vp: &cadcraft_doc::Viewport) -> Option<Bounds2> {
let center = vp.center.xy();
let ok = |v: f64| v.is_finite() && v > 0.0;
(ok(vp.width) && ok(vp.height) && center.is_finite()).then(|| {
let half = Vec2::new(vp.width / 2.0, vp.height / 2.0);
Bounds2::new(center - half, center + half)
})
}
/// Model space seen through the layout viewport `handle` of `space`, in paper units, clipped to
/// `area` (paper units) instead of to the viewport: what [`build`] draws inside that viewport,
/// over an area the caller chooses. The canvas builds the active viewport this way for an area
/// around it and draws it clipped to the viewport, so panning and zooming inside it moves the
/// geometry instead of rebuilding the layout (see [`Options::skip_viewport`]). Empty when the
/// viewport shows nothing.
pub fn viewport_content(d: &Drawing, space: &Space, handle: Handle, area: Bounds2, opts: &Options) -> DisplayList {
let Some(e) = d.space(space).and_then(|s| s.get(handle)) else { return DisplayList::default() };
let EntityKind::Viewport(vp) = &e.kind else { return DisplayList::default() };
if !viewport_shown(d, e, vp) || area.is_empty() || !(area.min.is_finite() && area.max.is_finite()) {
return DisplayList::default();
}
let opts = &with_point_view(d, space, opts);
let styler = style_table(d, space, opts, false).map(|t| plotstyle::Styler::new(d, t));
let mut b =
Builder { list: DisplayList::default(), opts, plotting: false, styles: styler.as_ref(), named: None, shared_budget: None, transparency: 0 };
contents(&mut b, d, vp, area, &Clip::Rect(area), paper::unit_mm_of(d, handle));
b.list
}
/// Model space seen through `vp`, in paper units, appended to `b`'s list clipped to `clip`;
/// `area` (the clip's bounds on paper) picks the model objects to draw.
fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bounds2, clip: &Clip, unit_mm: f64) {
if b.list.expanded >= MAX_BLOCK_EXPANSION || !vp.on {
return;
}
let center = vp.center.xy();
let ok = |v: f64| v.is_finite() && v > 0.0;
if !(ok(vp.width) && ok(vp.height) && ok(vp.view_height) && center.is_finite() && vp.view_center.is_finite()) {
return;
}
let s = vp.height / vp.view_height;
if !ok(s) {
return;
}
// Model space scaled onto the sheet and turned by the viewport's view twist.
let xf = vp.model_to_paper_matrix();
// The model box the area covers (turned, with a twist), with slack: entity bounds are
// approximate (text, dimensions).
let mbox = Bounds2::from_points(area.corners().map(|p| vp.paper_to_model(p)));
let win = mbox.expand(mbox.width().max(mbox.height()) * 0.05);
// PSLTSCALE on: model-space dashes are scaled so they measure the same on paper as in
// paper space, whatever the viewport scale.
let lt_factor = if d.header.i64("PSLTSCALE", 1) != 0 { 1.0 / s } else { 1.0 };
let ctx = || Ctx { lt_factor, ..top_ctx(d, xf, VIEWPORT_CONTENT, &vp.frozen_layers, &vp.layer_colors) };
let seen = |me: &Entity| {
if matches!(me.kind, EntityKind::Viewport(_)) {
return false;
}
if matches!(me.kind, EntityKind::Ray(_) | EntityKind::XLine(_)) {
return true;
}
let eb = cadcraft_doc::entity_bounds(d, me, 0);
eb.is_empty() || eb.intersects(&win)
};
// One expansion budget for the whole build: seed it with what the enclosing list has already
// spent so a viewport's contents cannot start over with a fresh allowance and exceed the
// documented limit (CWE-400). Both the serial sub-builder and the parallel chunks share it.
let budget = std::sync::atomic::AtomicUsize::new(b.list.expanded);
let mut sub = Builder {
list: DisplayList::default(),
opts: b.opts,
plotting: b.plotting,
styles: b.styles,
named: None,
shared_budget: Some(&budget),
transparency: 0,
};
#[cfg(not(target_arch = "wasm32"))]
let parallel = !b.plotting && b.styles.is_none() && d.model.len() >= 2 * PARALLEL_CHUNK;
#[cfg(target_arch = "wasm32")]
let parallel = false;
if parallel {
#[cfg(not(target_arch = "wasm32"))]
{
// As `build_parallel`: chunks built side by side, joined in drawing order.
use rayon::prelude::*;
let ents: Vec<&std::sync::Arc<Entity>> = d.model.iter().collect();
let opts = b.opts;
let parts: Vec<DisplayList> = ents
.par_chunks(PARALLEL_CHUNK)
.map(|chunk| {
let mut part = Builder {
list: DisplayList::default(),
opts,
plotting: false,
styles: None,
named: None,
shared_budget: Some(&budget),
transparency: 0,
};
let c = ctx();
for me in chunk.iter().filter(|me| seen(me)) {
entity(&mut part, &c, me);
}
part.list
})
.collect();
for part in parts {
append(&mut sub.list, part);
}
}
} else {
let c = ctx();
for me in d.model.iter().filter(|me| seen(me)) {
entity(&mut sub, &c, me);
}
}
// Plotted text glyphs move to the layout's list; their outlines carry the glyph's index there
// in their handle's high half until clipped (see `take_viewport_layers`).
for t in std::mem::take(&mut sub.list.texts) {
let k = b.list.texts.len() as u64 + 1;
if let Some(p) = sub.list.prims.get_mut(t.prim as usize)
&& k < u64::from(u32::MAX)
{
p.handle = Handle(p.handle.0 | (k << 32));
b.list.texts.push(PlotText { prim: PlotText::NONE, ..t });
}
}
// The contents are in paper units now: draw their pens before clipping.
pen::apply(&mut sub.list, unit_mm);
b.list.expanded = b.list.expanded.saturating_add(sub.list.expanded);
b.list.truncated |= sub.list.truncated;
append_clipped(&mut b.list, &sub.list, clip);
}
/// The clip region of a clipped viewport: the outline of its clip boundary entity, when that
/// is a closed shape in the drawing. `None` for a rectangular viewport (or a dangling,
/// unusable or self-referencing boundary, which falls back to the rectangle).
pub fn viewport_clip(d: &Drawing, handle: Handle, vp: &cadcraft_doc::Viewport) -> Option<clip::PolyClip> {
let h = vp.clip.filter(|h| *h != handle)?;
let be = d.entity(h)?;
clip::PolyClip::new(&clip::boundary_polygon(&be.kind)?)
}
/// `list` clipped to `clip` (a clipped viewport's outline in the list's own units), for a list
/// drawn inside a clipped viewport apart from its contents.
pub fn clip_list(list: &DisplayList, clip: &clip::PolyClip) -> DisplayList {
let mut out = DisplayList::default();
append_clipped(&mut out, list, &Clip::Poly(clip));
out
}
/// What viewport contents are clipped to.
enum Clip<'a> {
/// The viewport rectangle (the fast path).
Rect(Bounds2),
/// A clip boundary's outline.
Poly(&'a clip::PolyClip),
}
/// Append `src` to `dst`, clipped to `clip`.
fn append_clipped(dst: &mut DisplayList, src: &DisplayList, clip: &Clip) {
for p in &src.prims {
let pts = src.points(p);
match (p.kind, clip) {
(Kind::Polyline, Clip::Rect(rect)) => {
for piece in clip::clip_polyline(pts, rect) {
push_raw(dst, p, Kind::Polyline, &piece);
}
}
(Kind::Polyline, Clip::Poly(c)) => {
for piece in c.clip_polyline(pts) {
push_raw(dst, p, Kind::Polyline, &piece);
}
}
(Kind::Tris | Kind::Mask, Clip::Rect(rect)) => {
let t = clip::clip_triangles(pts, rect);
push_raw(dst, p, p.kind, &t);
}
(Kind::Tris | Kind::Mask, Clip::Poly(c)) => {
let t = c.clip_triangles(pts);
push_raw(dst, p, p.kind, &t);
}
(Kind::Point, _) => {
let inside = |q: Vec2| match clip {
Clip::Rect(rect) => rect.contains(q),
Clip::Poly(c) => c.contains(q),
};
if let Some(q) = pts.first()
&& inside(*q)
{
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)
{
push_raw(dst, p, Kind::Polyline, &[a, c]);
}
}
(Kind::Infinite { ray }, Clip::Poly(c)) => {
if let (Some(base), Some(dir)) = (pts.first(), pts.get(1)) {
for piece in c.clip_infinite(*base, *dir, ray) {
push_raw(dst, p, Kind::Polyline, &piece);
}
}
}
}
}
}
/// 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,
Kind::Tris | Kind::Mask => 3,
_ => 1,
};
if pts.len() < min {
return;
}
let tris = matches!(kind, Kind::Tris | Kind::Mask);
let start = if tris { dst.tris.len() } else { dst.verts.len() } as u32;
for q in pts {
dst.bounds.add(*q);
}
if tris {
dst.tris.extend_from_slice(pts);
} else {
dst.verts.extend_from_slice(pts);
}
dst.prims.push(DPrim {
handle: p.handle,
color: p.color,
aci7: p.aci7,
lw: p.lw,
transparency: p.transparency,
kind,
start,
len: pts.len() as u32,
});
}