Files
vectorcraft/crates/render/src/renderer/nodes.rs
T
Drew Dibble b9dbd867f6 Canvas: a 3D effect changed on type or a group redraws where its solid was
When an edit changes an object, the canvas draws again only the area
the object painted before and after it, measured by its cull bounds.
Type and groups with Extrude & Bevel, Inflate or Rotate were measured by
their letters or members, not by the solid the effect draws, which
reaches well past them. Making the solid shallower or turning it left
pieces of the old solid on the canvas outside that area until the view
was drawn again.

An object with a 3D effect is now measured by the effect's art, as the
renderer draws it: with its geometry effects, each object in it with
the raster effects of its own fills and strokes and the art its brushed
strokes draw, grown by the reach of the object's own raster effects.
One function measures that art for every bounds the renderer and
exports take, where three copies of it disagreed.

- The renderer measures the art it draws. What it makes to measure an
  object it holds until the frame ends, with what the art read from
  outside the object (fonts, symbols, color settings), and keeps it
  with the art it draws if it draws the object while that is
  unchanged. Art measured but not drawn (culled, off the view) isn't
  kept. It holds at most as much as it keeps for the art it draws
  (64 MB); past that, an object is made again to draw it. An object
  whose 3D effect builds nothing (Revolve on a group, a solid too
  complex) is measured as it is drawn otherwise, as without a renderer.
  FrameStats::three_d_built counts the 3D art the renderer makes.
- The canvas measures what an edit changed through the renderer that
  drew the frame shown (Renderer::changed). When it draws the part on
  its own thread, it draws it with the art it made to measure it; when
  the render worker draws the frame, or nothing in view changed, that
  art is let go. Under Overprint Preview or Separations Preview, which
  draw copies of the objects, the edit is measured afresh. An object
  the frame doesn't draw (hidden, in a hidden group or layer, or
  hidden by the view) is measured as nothing, before and after, so an
  edit to it builds no solid: changed() without a renderer counts it as
  nothing too, where it measured it. The canvas measures the edit
  before it knows where the frame is drawn: when the worker draws it,
  the solids it built to measure it are built again there.
- Fit to Artwork Bounds, the Artboards panel's check for it and
  per-artboard SVG export measure every artboard through one renderer
  (Renderer::art_bounds_on, Renderer::effect_bounds), which keeps each
  object's bounds for the pass, a group holding 3D art or a hidden
  member included, so each solid is built once: 30 extruded type
  objects over 30 artboards take about 2 ms, where measuring their
  letters took under 1 ms and building each solid for each artboard
  would take 70 ms. Objects with brushed strokes, which culling leaves
  room for and exports don't, are measured on each artboard. The
  Artboard tool's canvas menu keeps whether Fit to Artwork Bounds has
  art to fit for the document's revision, where it measured it on
  every frame it was open.
- Other exports, Object › Rasterize and changed() without a renderer
  build the art on each call, as 3D paths and groups already did.
  Exports and Rasterize now take the solid of 3D type, where they
  cropped it to the letters, and the art its brushed strokes draw, not
  the room culling leaves for it. Rasterize on drawn 3D art measures it
  once while the art is kept.
- A shadow after a 3D effect, on the object or on a fill after the
  fill's own 3D effect, reaches past the solid once; it was counted
  twice.
- With Crop Marks or Vector Halftone, which draw other art, the object
  is measured by that art, as before.
- Culling leaves room for a 3D object's brushed strokes as for any
  object's, and the renderer measures a 3D object again when the brush
  library changes: a brush widened past that room would leave the
  object culled.

A document's first frame builds the solid of every 3D object, those off
the view too, to know whether it reaches into the view: 200 extruded
words with 150 off the view take about 12 ms, where measuring their
letters took 6 ms.

Type whose object effects leave it as it is (Revolve builds no art from
type) is measured by its own box grown to hold its glyphs, as plain
type is; it was measured by its box alone, which needn't hold them.
2026-11-08 23:17:50 -07:00

767 lines
38 KiB
Rust

use super::{
Affine, Arc, BezPath, ClipEntry, ClipPaintEntry, Compose, Composite, Document, FillRule, Frame, GeomEntry, ISOLATION_DIM, Ink, MAX_GUIDE_DASHES,
Node, NodeId, NodeKind, Rect, RenderContext, RenderOptions, Renderer, Shape, SymbolBoxes, TraceView, blend_mode, brush_fx, effects, fx, group,
holds_instance, holds_plugin_effect, images, kurbo, lru, mask_value, painted_items, plain_image, polygon_length, rects_overlap,
shape_masked_image, single_plain_fill, text_geom_snapped, white_shape_mask,
};
use crate::kept_paths::{self, KeptPath};
/// Does [`Renderer::bounds_of`] measure `n` by the 3D art this renderer draws and keeps for it: a
/// Revolve, Extrude & Bevel, Inflate or Rotate without Crop Marks or Vector Halftone, which draw
/// other art?
fn measures_three_d(n: &Node) -> bool {
(effects::has_revolve(n) || effects::has_solid(n)) && !effects::has_crop_marks(n) && !effects::addons::halftone::has_halftone(n)
}
/// Does `n` hold an object that [`Renderer::bounds_of`] measures by its 3D effect's art?
fn holds_measured_three_d(n: &Node) -> bool {
let mut holds = false;
n.walk(&mut |c| holds |= measures_three_d(c));
holds
}
impl Renderer {
/// Cached cull bounds of a node (containers union their cached children), symbol instances
/// measured by their art box from `symbols`.
pub(crate) fn bounds_of(&mut self, symbols: &SymbolBoxes, a: &Arc<Node>) -> Option<Rect> {
let key = Arc::as_ptr(a) as usize;
if let Some(e) = self.geom.get_mut(&key)
&& Arc::ptr_eq(&e.node, a)
{
e.stamp = self.stamp;
return e.bounds;
}
let b = match &a.kind {
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } if !fx::has_object_fx(a) => {
let mut acc: Option<Rect> = None;
// An Image Trace object's hidden source image shows in some of its views.
let source = a.extras().trace.is_some().then(|| children.first()).flatten();
for c in children {
if c.visible || source.is_some_and(|s| Arc::ptr_eq(s, c)) {
acc = vectorcraft_geom::union_opt(acc, self.bounds_of(symbols, c));
}
}
acc
}
// A 3D effect's art, measured as this renderer draws and keeps it ([`Self::fx_art`]): one
// build for the measure and the draw. Crop Marks and Vector Halftone draw other art, and
// an object whose 3D effect builds nothing (Revolve on a group, a solid too complex) is
// drawn otherwise, measured as below.
_ if measures_three_d(a)
&& let Some(b) = {
let art = self.measured_fx_art(symbols.document(), a);
self.three_d_art_bounds(symbols, &art)
} =>
{
// Culling leaves room for brush art as for any object ([`brush_fx::cull_bounds`]);
// the measure is made again when the brush library changes ([`Self::check_symbols`]).
let pad = brush_fx::pad(symbols, a);
b.map(|b| fx::with_own_raster_reach(symbols, a, b).inflate(pad, pad))
}
_ => self.kept_bounds(symbols, a).unwrap_or_else(|| brush_fx::cull_bounds(symbols, a)),
};
self.geom.insert(key, GeomEntry { node: a.clone(), bounds: b, path: None, stamp: self.stamp });
b
}
/// The kept path of long path or compound path `a` ([`KeptPath`]), made, or brought up to date
/// from the one kept for its id, when it is not `a`'s yet. `None` when `a` has none.
fn kept_path(&mut self, a: &Arc<Node>) -> Option<&mut KeptPath> {
if !kept_paths::kept(a) {
return None;
}
let stamp = self.stamp;
let made = match self.kept.get_mut(&a.id) {
Some(k) if Arc::ptr_eq(&k.node, a) => true,
Some(k) => {
// The object the path was made from, when no longer drawn, shares the path through
// its `geom` entry: dropping that entry lets the path change in place.
let old = Arc::as_ptr(&k.node) as usize;
if self.geom.get(&old).is_some_and(|e| Arc::ptr_eq(&e.node, &k.node) && e.stamp != stamp) {
self.geom.remove(&old);
}
k.update(a)
}
None => false,
};
if !made {
let k = KeptPath::new(a, stamp)?;
self.kept.insert(a.id, k);
}
let k = self.kept.get_mut(&a.id)?;
k.stamp = stamp;
Some(k)
}
/// Cull bounds of long path or compound path `a` measured from its kept path
/// ([`Self::kept_path`]): the same as [`brush_fx::cull_bounds`]. `None` when it has no kept path
/// or is measured another way ([`brush_fx::path_cull_bounds`]).
fn kept_bounds(&mut self, symbols: &SymbolBoxes, a: &Arc<Node>) -> Option<Option<Rect>> {
let k = self.kept_path(a)?;
brush_fx::path_cull_bounds(symbols, a, || {
let b = k.geometric()?;
let bp = k.path.clone();
Some(a.appearance.stroked_bounds_by(b, |s| {
let tips = k.tips(s);
s.bounds_with_tips(&bp, b, tips)
}))
})
}
/// Drop the cached bounds of symbol instances and of the objects that hold them when `doc`'s
/// symbols are not those of the document last drawn (by name and art): those bounds measured
/// the instances by their symbol's art. Likewise drop those of objects measured by a 3D
/// effect's art, and of the objects that hold them, when the brush library has changed.
pub(crate) fn check_symbols(&mut self, doc: &Document) {
let same = self.symbols.len() == doc.symbols.len()
&& self.symbols.iter().zip(&doc.symbols).all(|((name, art), s)| *name == s.name && std::ptr::eq(art.as_ptr(), Arc::as_ptr(&s.art)));
if !same {
self.geom.retain(|_, e| !holds_instance(&e.node));
self.symbols = doc.symbols.iter().map(|s| (s.name.clone(), Arc::downgrade(&s.art))).collect();
}
// That art is measured with the art its brushed strokes draw ([`Self::bounds_of`]).
let brushes = self.brushes.library_key_now(doc, self.stamp);
if brushes != self.brush_library {
self.brush_library = brushes;
self.geom.retain(|_, e| !holds_measured_three_d(&e.node));
}
}
/// When a plug-in has been installed or removed since the last check
/// ([`effects::plugin_revision`]), drop what the caches keep from plug-in effects: the effect
/// geometry of paths with a plug-in effect, the cull bounds of objects that have one or hold one,
/// of symbol instances and of the objects that hold them, and the art drawn for the effects of
/// objects with a plug-in effect. Rasters of raster effects and evaluations of live objects hold
/// the revision in their keys when what they draw has a plug-in effect (`fx::Inputs`,
/// `live::resources_key`).
///
/// The same caches are dropped when a live-effect plug-in that paints its pieces is installed
/// and the names of `doc`'s swatches or patterns have changed since the last check: whether its
/// pieces are used depends on those names.
pub(crate) fn check_plugins(&mut self, doc: &Document) {
let revision = effects::plugin_revision();
let mut stale = false;
if revision != self.plugin_revision {
self.plugin_revision = revision;
self.painting_plugins = effects::any_plugin_paints_output();
stale = true;
}
if self.painting_plugins {
let names = fx::paint_names(doc);
stale |= names != self.paint_names;
self.paint_names = names;
}
if stale {
self.fx_geom.retain(|_, e| !effects::has_plugin_effect(&e.node));
self.geom.retain(|_, e| !holds_plugin_effect(&e.node) && !holds_instance(&e.node));
self.fx_arts.retain(|(node, ..)| !effects::has_plugin_effect(node));
}
}
/// Cached region a clip group's clipping path `a` clips to ([`effects::clip_outline`]):
/// outlining text and uniting shapes is too slow to repeat every frame.
pub(crate) fn clip_of(&mut self, a: &Arc<Node>) -> Option<Arc<(BezPath, FillRule)>> {
let key = Arc::as_ptr(a) as usize;
if let Some((node, region)) = self.clips.get(&key, self.stamp)
&& Arc::ptr_eq(node, a)
{
return region.clone();
}
let region = effects::clip_outline(a).map(Arc::new);
let bytes = size_of::<ClipEntry>() + region.as_ref().map_or(0, |r| lru::path_bytes(&r.0));
self.clips.insert(key, (a.clone(), region.clone()), bytes, self.stamp);
region
}
/// Cached fills and strokes of clipping path `a` ([`Node::clip_paint`]): the parts keep their
/// allocations between frames, so their own geometry stays cached.
fn clip_paint_of(&mut self, a: &Arc<Node>) -> (Option<Arc<Node>>, Option<Arc<Node>>) {
let key = Arc::as_ptr(a) as usize;
if let Some((node, fill, stroke)) = self.clip_paints.get(&key, self.stamp)
&& Arc::ptr_eq(node, a)
{
return (fill.clone(), stroke.clone());
}
let paint = a.clip_paint();
let (fill, stroke) = (paint.fill.map(Arc::new), paint.stroke.map(Arc::new));
let bytes = size_of::<ClipPaintEntry>() + [&fill, &stroke].iter().filter_map(|p| p.as_deref()).map(lru::node_bytes).sum::<usize>();
self.clip_paints.insert(key, (a.clone(), fill.clone(), stroke.clone()), bytes, self.stamp);
(fill, stroke)
}
/// Cached BezPath of a path or compound path node (its bounds cached with it, see
/// [`Self::bounds_of`]).
fn path_of(&mut self, symbols: &SymbolBoxes, a: &Arc<Node>) -> Option<Arc<BezPath>> {
let key = Arc::as_ptr(a) as usize;
if let Some(e) = self.geom.get(&key)
&& Arc::ptr_eq(&e.node, a)
&& let Some(p) = &e.path
{
return Some(p.clone());
}
let p = match self.kept_path(a) {
Some(k) => k.path.clone(),
None => Arc::new(a.stroke_path()?),
};
let bounds = self.bounds_of(symbols, a);
self.geom.insert(key, GeomEntry { node: a.clone(), bounds, path: Some(p.clone()), stamp: self.stamp });
Some(p)
}
/// The cached path of path or compound path `n` ([`Self::path_of`]) when the frame is in
/// Outline view and `n` is `fx_owner`, else `None`.
fn outline_path(&mut self, f: &Frame, n: &Node) -> Option<Arc<BezPath>> {
if !f.opts.outline {
return None;
}
self.owner_path(f, n)
}
/// The cached path of path or compound path `n` ([`Self::path_of`]) when `n` is `fx_owner`,
/// else `None`.
fn owner_path(&mut self, f: &Frame, n: &Node) -> Option<Arc<BezPath>> {
let owner = self.fx_owner.as_ref().filter(|o| std::ptr::eq(&***o, n)).cloned()?;
self.path_of(f.symbols, &owner)
}
/// Symbol editing mode on screen: `id` is the instance editing was entered from, which is not
/// drawn (with its masks and effects), since the editing layer holds a copy of its art in its
/// place.
fn edited_instance(f: &Frame, id: NodeId) -> bool {
f.opts.isolated.is_some() && f.doc.symbol_edit.as_ref().is_some_and(|e| e.instance == Some(id))
}
/// Whether `a` is left out of this frame: hidden, a skipped template or guide, the instance a
/// symbol is edited from, or culled.
fn skipped(&mut self, f: &Frame, a: &Arc<Node>) -> bool {
let skipped_view_item = (f.opts.skip_templates || f.opts.hide_templates) && matches!(a.kind, NodeKind::Layer { template: true, .. })
|| (f.opts.skip_templates || f.opts.hide_guides) && matches!(a.kind, NodeKind::Path { guide: true, .. });
if !a.visible || skipped_view_item || f.opts.hidden.contains(&a.id) || Self::edited_instance(f, a.id) {
return true;
}
match self.bounds_of(f.symbols, a) {
Some(b) => {
let pad = f.px * 2.0;
// Level of detail: leaves smaller than a quarter pixel are invisible.
let culled =
!rects_overlap(b.inflate(pad, pad), f.visible) || (!a.is_container() && b.width() < f.px * 0.25 && b.height() < f.px * 0.25);
self.stats.culled += culled as usize;
culled
}
None => !a.is_container(),
}
}
pub(crate) fn draw_arc(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
if self.skipped(f, a) {
return;
}
// An image masked by one white shape painted straight into the shape, as an image in a
// clip group is (envelopes with Preserve Shape Using Transparency cut distorted images into
// many such pieces).
if let Some(m) = a.extras().mask.as_deref()
&& !f.opts.outline
&& !self.is_shape(a)
&& let NodeKind::Image(im) = &a.kind
&& white_shape_mask(m)
&& let Some(region) = self.clip_of(&m.art)
&& shape_masked_image(a, im, &region.0)
{
self.draw_image_in(ctx, f, im, Some(&region), images::image_tint(f.doc, a, f.ink), a.extras().pixelated);
self.stats.drawn += 1;
return;
}
if self.draw_masked_arc(ctx, f, a, false) {
return;
}
self.draw_arc_body(ctx, f, a);
}
/// Draw `a` through its opacity mask when it has one. Forced roots draw regardless of their
/// visibility and bounds.
fn draw_masked_arc(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>, force: bool) -> bool {
let Some(m) = a.extras().mask.as_deref().filter(|m| !m.disabled && !f.opts.outline && !self.is_shape(a)) else { return false };
// A masked object is a non-isolated group of its own, drawn over its own pixels: the
// mask is rendered over those only.
let bounds = self.bounds_of(f.symbols, a);
let Some((r, sub)) = group::area(ctx, f, bounds) else { return true };
let Some(mask) = self.mask_values(&sub, m, r.w, r.h) else { return true };
let blends = a.blend != vectorcraft_color::BlendMode::Normal || (!a.isolate && self.blends_through(a));
self.group(ctx, f, Composite { mask: Some(mask), blends, bounds, ..Default::default() }, &mut |r, c, fr| {
if force {
r.draw_node(c, fr, a, true);
} else {
r.draw_arc_body(c, fr, a);
}
});
true
}
/// Draw an `Arc` regardless of its visibility and bounds, applying its opacity mask.
pub(crate) fn draw_forced_arc(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
if !self.draw_masked_arc(ctx, f, a, true) {
self.draw_node(ctx, f, a, true);
}
}
/// The coverage of opacity mask `m` per output pixel (row-major): its art rendered offscreen,
/// as luminance ([`mask_value`]).
pub(super) fn mask_values(&mut self, f: &Frame, m: &vectorcraft_doc::OpacityMask, w: u16, h: u16) -> Option<Vec<u8>> {
let mut mctx = f.offscreen_context(w, h);
// Mask art is a picture of its own: it takes no part in a knockout group around the object,
// and its luminance is that of its screen colours.
let outer = (
std::mem::take(&mut self.knockout),
std::mem::take(&mut self.nested),
self.backdrop.take(),
std::mem::take(&mut self.clip_paths),
self.picture.take(),
);
let image_left_out = self.image_left_out.replace(false);
let failures = crate::paint::decode_failures();
let pattern_images_left_out = crate::pattern::images_left_out();
self.draw_kept_node(&mut mctx, &Frame { mt: false, ink: Ink::Display, ..*f }, &m.art);
let incomplete = self.image_left_out.unwrap_or(false)
|| crate::pattern::images_left_out() > pattern_images_left_out
|| (!f.opts.mark_undecoded_images && crate::paint::decode_failures() > failures);
self.image_left_out = image_left_out.map(|outer| outer || incomplete);
let pm = self.finish(&mut mctx, self.raster);
(self.knockout, self.nested, self.backdrop, self.clip_paths, self.picture) = outer;
if incomplete {
return None;
}
Some(pm.data().iter().map(|p| mask_value(p.r, p.g, p.b, p.a, m.clip, m.invert)).collect())
}
/// Everything [`Self::draw_arc`] does after culling.
fn draw_arc_body(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
// Fast path for plain paths: cached geometry, opacity folded into the paint.
// A blend mode on a single plain fill applies per draw (exact for one paint operation);
// a blend layer would composite the whole viewport.
let blended = a.blend != vectorcraft_color::BlendMode::Normal;
if let NodeKind::Path { rule, guide: false, .. } = &a.kind
&& !f.opts.outline
&& (!blended || single_plain_fill(a))
&& !a.isolate
&& !fx::has_fx(a)
&& (self.opacity_of(a) >= 1.0 || painted_items(a) == 1)
&& let Some(bp) = self.path_of(f.symbols, a)
{
self.alpha = self.opacity_of(a).clamp(0.0, 1.0);
self.cur = Some(a.clone());
if blended {
ctx.set_blend_mode(blend_mode(a.blend));
}
self.draw_shape(ctx, f, a, &bp, *rule);
if blended {
ctx.set_blend_mode(blend_mode(vectorcraft_color::BlendMode::Normal));
}
self.cur = None;
self.alpha = 1.0;
self.stats.drawn += 1;
return;
}
if fx::has_object_fx(a) {
return self.draw_object_fx(ctx, f, a, true);
}
if let NodeKind::Text(t) = &a.kind
&& self.opacity_of(a) >= 1.0
&& a.blend == vectorcraft_color::BlendMode::Normal
&& !fx::has_fx(a)
{
let t = &*f.doc.inline_resolved(t);
let (g, kept) = match f.text_snap(t) {
Some(xf) => (Arc::new(text_geom_snapped(t, Some(xf))), false),
None => self.text_geom_of(a, t),
};
self.draw_text_geom(ctx, f, a, t, &g, kept);
self.stats.drawn += 1;
return;
}
if vectorcraft_doc::live::is_live(a) || effects::has_pathfinder(a) {
// The isolated object itself (a repeat isolated to edit its source, #5054) draws its
// art as usual, as an isolated group does in `draw_layer_node`.
let settle = f.opts.isolated == Some(a.id) && f.doc.mask_edit.is_none();
let outer = self.isolation_settled;
self.isolation_settled = outer || settle;
self.draw_live(ctx, f, a);
self.isolation_settled = outer;
return;
}
// Its live-effect geometry depends on the node alone, and for plug-in effects on the
// plug-ins installed (`check_plugins`), so it can be kept across frames.
let outer = self.fx_owner.replace(a.clone());
self.draw_node(ctx, f, a, true);
self.fx_owner = outer;
}
pub(crate) fn draw_node(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, force: bool) {
if !force && (!n.visible || f.opts.hidden.contains(&n.id)) {
return;
}
// A cancelled export stops drawing; whoever asked for the picture drops it (see `cancel`).
if vectorcraft_doc::cancel::is_cancelled() {
return;
}
if force {
// Bounds were already checked by draw_arc (or the caller wants it drawn regardless).
} else if let Some(b) = fx::cull_bounds(f.symbols, n) {
let pad = f.px * 2.0;
if !rects_overlap(b.inflate(pad, pad), f.visible) {
self.stats.culled += n.count();
return;
}
} else if !n.is_container() {
return;
}
// Layer Options on screen: a layer whose Preview is off draws in outline, and Dim Images
// fades its images, a template's too (exports and thumbnails, which leave templates out,
// ignore both).
if let NodeKind::Layer { preview, dim_images, template, .. } = &n.kind
&& f.opts.dim_templates
&& !f.opts.skip_templates
{
// A template layer stays in Preview while the view is in Outline (and whatever its own
// Preview option says): its picture is there to be traced (#1486).
if *template && f.opts.outline {
let opts = RenderOptions { outline: false, ..f.opts.clone() };
return self.draw_node(ctx, &Frame { opts: &opts, ..*f }, n, force);
}
if !preview && !template && !f.opts.outline {
let opts = RenderOptions { outline: true, ..f.opts.clone() };
return self.draw_node(ctx, &Frame { opts: &opts, ..*f }, n, force);
}
if let Some(p) = *dim_images {
let outer = self.dim_images;
let dim = f32::from(p).clamp(0.0, 100.0) / 100.0;
self.dim_images = Some(outer.map_or(dim, |o| o.min(dim)));
self.draw_layer_node(ctx, f, n);
self.dim_images = outer;
return;
}
}
self.draw_layer_node(ctx, f, n);
}
/// [`Self::draw_node`] after culling and Layer Options.
fn draw_layer_node(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node) {
// Isolation mode: the art around the isolated container draws dimmed (#833).
if let Some((isolated, path)) = self.isolation_path(f) {
// A container on the way down to it draws as usual: its members decide.
if path.contains(&n.id) && n.id != isolated {
return self.draw_layer_node_now(ctx, f, n);
}
self.isolation_settled = true;
if n.id == isolated {
self.draw_layer_node_now(ctx, f, n);
} else {
self.dimmed(ctx, f, &mut |r, c, fr| r.draw_layer_node_now(c, fr, n));
}
self.isolation_settled = false;
return;
}
self.draw_layer_node_now(ctx, f, n);
}
/// In isolation mode, while drawing outside the isolated container and the art dimmed around
/// it: the isolated container and its layers and groups, from the top layer down to it.
pub(crate) fn isolation_path(&mut self, f: &Frame) -> Option<(NodeId, Vec<NodeId>)> {
// Opacity-mask editing isolates the temporary layer for tools; that layer is not painted on
// the page (the mask applies on the object), so canvas dimming would wash out everything.
let isolated = f.opts.isolated.filter(|_| f.doc.mask_edit.is_none() && !self.isolation_settled)?;
let doc = std::ptr::from_ref(f.doc) as usize;
if self.isolation.as_ref().is_none_or(|(stamp, d, id, _)| (*stamp, *d, *id) != (self.stamp, doc, isolated)) {
self.isolation = Some((self.stamp, doc, isolated, f.doc.ancestry(isolated).unwrap_or_default()));
}
let path = self.isolation.as_ref().map(|(.., path)| path.clone()).filter(|p| !p.is_empty())?;
Some((isolated, path))
}
/// `draw` at [`ISOLATION_DIM`], with nothing inside dimmed again.
fn dimmed(&mut self, ctx: &mut RenderContext, f: &Frame, draw: &mut group::Content) {
let settled = std::mem::replace(&mut self.isolation_settled, true);
let comp = Composite { opacity: ISOLATION_DIM, ..Default::default() };
self.group(ctx, f, comp, draw);
self.isolation_settled = settled;
}
/// [`Self::draw_layer_node`] past isolation mode's dimming.
fn draw_layer_node_now(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node) {
if fx::has_object_fx(n) {
return self.draw_object_fx(ctx, f, &Arc::new(n.clone()), false);
}
let outline = f.opts.outline;
let opacity = self.opacity_of(n);
// Whether this container's children knock each other out.
let knockout = !outline && n.knocks_out(self.knockout);
let enclosing = std::mem::replace(&mut self.knockout, knockout);
if !outline && (opacity < 1.0 || n.blend != vectorcraft_color::BlendMode::Normal || n.isolate || knockout) {
let blends = self.blends_through(n);
let bounds = if blends { self.node_bounds(f.symbols, n) } else { None };
let comp = Composite { blend: n.blend, opacity, isolated: n.isolate, blends, bounds, ..Default::default() };
self.group(ctx, f, comp, &mut |r, c, fr| r.draw_content(c, fr, n, knockout));
} else {
self.draw_content(ctx, f, n, knockout);
}
self.knockout = enclosing;
self.stats.drawn += 1;
}
/// What `n` draws inside its transparency group (`knockout`: its children knock each other out).
fn draw_content(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, knockout: bool) {
match &n.kind {
NodeKind::Group { children, clip: false } if f.opts.trace_views && !f.opts.outline && Self::trace_view(f, n) != TraceView::Result => {
self.draw_trace_view(ctx, f, children, Self::trace_view(f, n), knockout)
}
NodeKind::Layer { children, clip: false, .. } | NodeKind::Group { children, clip: false } => {
self.draw_children(ctx, f, children, knockout)
}
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } if f.opts.outline => {
for c in children {
self.draw_outline_member(ctx, f, c);
}
}
NodeKind::Group { children, clip: true } | NodeKind::Layer { children, clip: true, .. } => {
// Nothing to clip by hides the clipped art (as in the SVG and PDF output). A clip
// group doesn't isolate: blending inside it reaches the art below. The clipping
// path's fill paints behind the clipped art and its stroke over it, unclipped.
if let Some((clip, rest)) = children.split_first()
&& let Some(region) = self.clip_of(clip)
{
let (fill, stroke) = self.clip_paint_of(clip);
// An image inside its own frame painted straight into the clipping path, without
// a clip layer (envelopes cut distorted images into many such pieces).
if let ([image], None, None) = (rest, &fill, &stroke)
&& let NodeKind::Image(im) = &image.kind
&& plain_image(image, im, &region.0)
{
self.draw_image_in(ctx, f, im, Some(&region), images::image_tint(f.doc, image, f.ink), image.extras().pixelated);
self.stats.drawn += 1;
return;
}
let blends = self.blends_through(n);
let bounds = if blends { Some(region.0.bounding_box()) } else { None };
let comp = Composite { clip: Some((&region.0, region.1)), blends, bounds, ..Default::default() };
self.group(ctx, f, comp, &mut |r, c, fr| {
if let Some(fill) = &fill {
r.draw_arc(c, fr, fill);
}
r.draw_children(c, fr, rest, knockout)
});
if let Some(stroke) = &stroke {
self.draw_arc(ctx, f, stroke);
}
}
}
NodeKind::Path { path, rule, guide, .. } => {
let bp = self.outline_path(f, n).unwrap_or_else(|| Arc::new(path.to_bezpath()));
if *guide {
let ([r, g, b], dots) = f.opts.guide;
let mut screen = BezPath::clone(&bp);
screen.apply_affine(f.view);
// The rasterizer makes every dash of the whole path before clipping: a guide
// running far off screen (from a file) asked for millions of them. Past
// [`MAX_GUIDE_DASHES`] (by its control polygon, which is at least as long) it
// is drawn solid.
let h = self.hairline;
if dots && polygon_length(&screen) / (4.0 * h) <= MAX_GUIDE_DASHES {
ctx.set_transform(Affine::IDENTITY);
ctx.set_stroke(kurbo::Stroke::new(1.5 * h).with_dashes(0.0, [1.5 * h, 2.5 * h]));
ctx.set_paint(f.ink.fixed([r, g, b, 255]));
ctx.stroke_path(&screen);
} else {
self.hairline(ctx, f, &bp, [r, g, b, 255]);
}
} else {
self.draw_shape(ctx, f, n, &bp, *rule);
}
}
NodeKind::Compound { rule, .. } => {
// Its members assembled once and kept with the node in `geom`, as plain paths are
// (`path_of`): a compound path of thousands of members is not assembled again in
// every frame.
if let Some(bp) = self.owner_path(f, n).or_else(|| n.stroke_path().map(Arc::new)) {
self.draw_shape(ctx, f, n, &bp, *rule);
}
}
NodeKind::Text(t) => self.draw_text(ctx, f, n, t),
NodeKind::Image(im) => self.draw_image_in(ctx, f, im, None, images::image_tint(f.doc, n, f.ink), n.extras().pixelated),
NodeKind::SymbolInstance { symbol, xf } => {
if let Some(sym) = f.doc.symbols.iter().find(|s| &s.name == symbol) {
// The instance draws when the frame has room for its art's objects; a symbol
// with 9-slice guides is placed cell by cell (`Self::sliced_art`).
let mut used = self.symbol_nodes;
let placed = match f.doc.symbol_slice_map(symbol, *xf) {
Some(m) => self.sliced_art(f.doc, &sym.art, &m, *xf, n, &mut used),
None => {
let count = sym.art.count() as u64;
vectorcraft_doc::charge_symbol_art(&mut used, count, || (self.placed_art(&sym.art, *xf, false, Some(n)), count))
}
};
self.symbol_nodes = used;
if let Some((art, kept)) = placed {
self.draw_placed_art(ctx, f, &art, kept);
}
}
}
NodeKind::Blend { .. }
| NodeKind::Envelope { .. }
| NodeKind::Mesh(_)
| NodeKind::Repeat(_)
| NodeKind::PlacedDocument(_)
| NodeKind::Intertwine { .. } => match self.live_root.clone().filter(|a| std::ptr::eq(&**a, n)) {
Some(a) => self.draw_live_body(ctx, f, &a, true),
None => self.draw_live_node(ctx, f, n),
},
}
}
/// Draw symbol art `art` as [`Self::placed_art`] placed it for an instance or an inline graphic
/// (`kept`: kept between frames, see [`Self::draw_kept_node`]).
pub(super) fn draw_placed_art(&mut self, ctx: &mut RenderContext, f: &Frame, art: &Arc<Node>, kept: bool) {
let outer = std::mem::replace(&mut self.made_again, !kept);
if kept {
self.draw_kept_node(ctx, f, art);
} else {
self.draw_forced_arc(ctx, f, art);
}
self.made_again = outer;
}
/// [`Self::draw_node`] of `a` (drawn whatever its visibility and bounds), whose `Arc` stays the
/// same from frame to frame while the art does. When `a` is a live object,
/// [`Self::draw_content`] draws its evaluation from the live cache ([`Self::live_root`]).
pub(crate) fn draw_kept_node(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc<Node>) {
let live = vectorcraft_doc::live::is_live(a).then(|| a.clone());
let outer = std::mem::replace(&mut self.live_root, live);
self.draw_forced_arc(ctx, f, a);
self.live_root = outer;
}
/// The View `n` draws with on screen: Source Image when it is an Image Trace object listed in
/// [`RenderOptions::trace_source`], else its own.
fn trace_view(f: &Frame, n: &Node) -> TraceView {
match &f.opts.trace_source {
Some(ids) if n.extras().trace.is_some() && ids.contains(&n.id) => TraceView::Source,
_ => n.trace_view(),
}
}
/// An Image Trace object's `children` (the hidden source image, then the traced shapes) as
/// `view` shows them on screen.
fn draw_trace_view(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], view: TraceView, knockout: bool) {
let Some((source, shapes)) = children.split_first() else { return };
if view.shows_source() && matches!(source.kind, NodeKind::Image(_)) {
self.draw_forced_arc(ctx, f, source);
}
if view.shows_result() {
self.draw_children(ctx, f, shapes, knockout);
}
if view.shows_outlines() {
let opts = RenderOptions { outline: true, ..f.opts.clone() };
let frame = Frame { opts: &opts, ..*f };
for c in shapes {
self.draw_outline_member(ctx, &frame, c);
}
}
}
/// [`Self::draw_node`] for `c`, a member of a clip group or a shape of an Image Trace object in
/// Outline view, culled by its cached bounds ([`Self::bounds_of`]) and drawn with `c` as
/// `fx_owner`, so its path, its live-effect geometry and, through [`Self::draw_kept_node`], a
/// live object's evaluation come from the caches.
fn draw_outline_member(&mut self, ctx: &mut RenderContext, f: &Frame, c: &Arc<Node>) {
if !c.visible
|| f.opts.hidden.contains(&c.id)
|| f.opts.hide_guides && matches!(c.kind, NodeKind::Path { guide: true, .. })
|| Self::edited_instance(f, c.id)
{
return;
}
match self.bounds_of(f.symbols, c) {
Some(b) => {
let pad = f.px * 2.0;
if !rects_overlap(b.inflate(pad, pad), f.visible) {
self.stats.culled += c.count();
return;
}
}
None if !c.is_container() => return,
None => {}
}
let outer = self.fx_owner.replace(c.clone());
self.draw_kept_node(ctx, f, c);
self.fx_owner = outer;
}
/// The children of a group: as the elements of a knockout group when `knockout`.
pub(super) fn draw_children(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>], knockout: bool) {
if knockout {
self.draw_knockout(ctx, f, children);
} else if let Some((_, path)) = self.isolation_path(f) {
// Isolation mode, in a container on the way down to the isolated one: each run of
// members around it draws dimmed as one (#833).
let mut rest = children;
while let Some(i) = rest.iter().position(|c| path.contains(&c.id)) {
let (around, from) = rest.split_at(i);
if !around.is_empty() {
self.dimmed(ctx, f, &mut |r, c, fr| around.iter().for_each(|n| r.draw_arc(c, fr, n)));
}
let Some((on_path, after)) = from.split_first() else { break };
self.draw_arc(ctx, f, on_path);
rest = after;
}
if !rest.is_empty() {
self.dimmed(ctx, f, &mut |r, c, fr| rest.iter().for_each(|n| r.draw_arc(c, fr, n)));
}
} else {
for c in children {
self.draw_arc(ctx, f, c);
}
}
}
/// Cull bounds of `n` (symbol instances measured by their art box from `symbols`): cached when
/// it is a node of the tree being drawn.
fn node_bounds(&mut self, symbols: &SymbolBoxes, n: &Node) -> Option<Rect> {
match self.geom.get(&(n as *const Node as usize)) {
// The entry keeps its node alive, so the address is that node's.
Some(e) if std::ptr::eq(Arc::as_ptr(&e.node), n) => e.bounds,
_ => fx::cull_bounds(symbols, n),
}
}
/// The children of a knockout group ([`Node::knockout_elements`]). Each element first erases
/// what the elements below it drew wherever it paints (its knockout shape: its coverage at full
/// object opacity without its opacity mask, or as drawn when its opacity and mask define the
/// knockout shape), then adds itself: it composites against the group's backdrop instead of
/// over the elements below it (the art below the group when the group is non-isolated and
/// drawn over a copy of it, see [`Self::group`] and [`Self::knockout_over`]).
pub(crate) fn draw_knockout(&mut self, ctx: &mut RenderContext, f: &Frame, children: &[Arc<Node>]) {
let backdrop = self.backdrop.take();
for c in Node::knockout_elements(children) {
if self.skipped(f, c) {
continue;
}
// Hard-edge coverage is binary, so an opaque fill replaces its knockout shape exactly.
// This also serves supersampled exports. Analytical AA keeps the erase/add passes:
// their intermediate rounding can differ from source-over at partially covered edges.
if f.opts.anti_alias == crate::AntiAlias::None && crate::node_checks::opaque_knockout_fill(c) {
self.draw_arc(ctx, f, c);
continue;
}
self.knockout_pass(ctx, f, c, Compose::DestOut);
match &backdrop {
Some(b) => self.knockout_over(ctx, f, c, b),
None => self.knockout_pass(ctx, f, c, Compose::Plus),
}
}
self.backdrop = backdrop;
}
/// Whether `n` is the knockout element being drawn as its shape (see [`Self::draw_knockout`]).
pub(super) fn is_shape(&self, n: &Node) -> bool {
self.shape_of == n as *const Node as usize
}
}