mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
665 lines
33 KiB
Rust
665 lines
33 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,
|
|
};
|
|
|
|
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
|
|
}
|
|
_ => brush_fx::cull_bounds(symbols, a),
|
|
};
|
|
self.geom.insert(key, GeomEntry { node: a.clone(), bounds: b, path: None, stamp: self.stamp });
|
|
b
|
|
}
|
|
|
|
/// 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 = 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>> {
|
|
let owner = self.fx_owner.as_ref().filter(|o| f.opts.outline && 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(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(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 { children, rule } => {
|
|
if let Some(bp) = self.outline_path(f, n) {
|
|
return self.draw_shape(ctx, f, n, &bp, *rule);
|
|
}
|
|
let mut bp = BezPath::new();
|
|
for c in children {
|
|
if let Some(p) = c.path_data() {
|
|
bp.extend(p.to_bezpath());
|
|
}
|
|
}
|
|
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(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
|
|
}
|
|
}
|