mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 06:31:19 +00:00
A linked 1-bit image mask from a .ai file keeps its fill color as a colorize paint, so the image takes the color once its file is relinked. Until then the image shows the missing-link placeholder, and the canvas and PDF export colorized that placeholder too: its light tile turned nearly transparent and its red cross took the fill color. ImageBlob::takes_colorize is false for a preview whose pixels are not grayscale. While it is false, the canvas draws the image and PDF export writes it as it is, printed separations separate it as an uncolorized image, and transparency flattening (EPS, PDF 1.3 and PDF/X-1a export, Flatten Transparency) does not count its paint as transparency. The paint stays on the image, and a grayscale preview is still colorized. SVG and EPS export already write only grayscale images colorized.
718 lines
35 KiB
Rust
718 lines
35 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};
|
|
|
|
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
|
|
}
|
|
_ => 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.
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// 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`).
|
|
pub(crate) fn check_plugins(&mut self) {
|
|
let revision = effects::plugin_revision();
|
|
if revision == self.plugin_revision {
|
|
return;
|
|
}
|
|
self.plugin_revision = revision;
|
|
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, ®ion.0)
|
|
{
|
|
self.draw_image_in(ctx, f, im, Some(®ion), 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, ®ion.0)
|
|
{
|
|
self.draw_image_in(ctx, f, im, Some(®ion), 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((®ion.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
|
|
}
|
|
}
|