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) -> Option { 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 = 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) -> Option> { 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::() + 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) -> (Option>, Option>) { 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::() + [&fill, &stroke].iter().filter_map(|p| p.as_deref()).map(lru::node_bytes).sum::(); 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) -> Option> { 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> { 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> { 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) -> 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) { 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, 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) { 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> { 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) { // 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)> { // 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 { 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(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, 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) { 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], 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) { 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], 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 { 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]) { 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 } }