//! Live objects in the renderer. //! //! Blends and envelopes are evaluated by `vectorcraft_doc::live` and the resulting objects cached //! by `Arc` identity of the live node (so the steps keep stable `Arc`s and hit the geometry cache //! frame after frame). Gradient meshes are tessellated into many small solid-colour quads (vello //! has no mesh shading), with the density chosen from the on-screen patch size; each quad is //! grown by half a device pixel so neighbours overlap and no antialiasing seams show. use std::collections::HashMap; use std::sync::Arc; use vectorcraft_doc::live::{self, GradientMesh, MeshQuad}; use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeKind, StrokeLayer}; use vectorcraft_geom::{Affine, BezPath, PathData, Point}; use vello_cpu::RenderContext; use vello_cpu::peniko; use crate::{Frame, Renderer, blend_mode, text_geom}; type Expanded = Arc>>; type MeshEntry = (Arc, Arc>, u64); /// Per-renderer cache of evaluated live objects. #[derive(Default)] pub(crate) struct LiveCache { expanded: HashMap, Expanded, u64)>, meshes: HashMap<(usize, usize), MeshEntry>, stamp: u64, } impl LiveCache { fn tick(&mut self, stamp: u64) { self.stamp = stamp; if self.expanded.len() > 256 { self.expanded.retain(|_, e| stamp - e.2 <= 3); } if self.meshes.len() > 256 { self.meshes.retain(|_, e| stamp - e.2 <= 3); } } } /// Text converted to one outline path (for envelopes): glyphs in document space, painted with the /// first run's fill/stroke. pub(crate) fn outline_text(n: &Node) -> Option { let NodeKind::Text(t) = &n.kind else { return None }; let g = text_geom(t); let mut bp = g.all.clone(); bp.apply_affine(t.xf); let mut out = Node::path(n.id, PathData::from_bezpath(&bp), n.appearance.clone()); if out.appearance.items.is_empty() && let Some(r) = t.runs.first() { let mut ap = Appearance::default(); ap.items.push(AppearanceItem::Fill(FillLayer::new(r.style.fill.clone()))); if !r.style.stroke.is_none() && r.style.stroke_width > 0.0 { ap.items.push(AppearanceItem::Stroke(StrokeLayer::new(r.style.stroke.clone(), r.style.stroke_width))); } out.appearance = ap; } out.opacity = n.opacity; out.blend = n.blend; Some(out) } /// Evaluate a live node one level (blend steps / envelope result, text outlined; mesh → flat /// pieces). Non-live nodes return themselves. pub fn expand_live(n: &Node) -> Vec { let hook: &dyn Fn(&Node) -> Option = &outline_text; live::expand_live_with(n, Some(hook)) } /// Subdivisions per patch for a patch about `size_px` device pixels across. fn mesh_level(m: &GradientMesh, bounds_px: f64) -> usize { let per = bounds_px / (m.rows.max(m.cols).max(1) as f64); ((per / 5.0).ceil() as usize).clamp(2, 32) } impl Renderer { fn live_expanded(&mut self, a: &Arc, cache: bool) -> Expanded { let key = Arc::as_ptr(a) as usize; if cache && let Some(e) = self.live.expanded.get_mut(&key) && Arc::ptr_eq(&e.0, a) { e.2 = self.live.stamp; return e.1.clone(); } let hook: &dyn Fn(&Node) -> Option = &outline_text; let v: Expanded = match crate::effects::pathfinder_children(a, Some(hook)) { Some(children) => Arc::new(children), None => Arc::new(expand_live(a).into_iter().map(Arc::new).collect()), }; if cache { self.live.expanded.insert(key, (a.clone(), v.clone(), self.live.stamp)); } v } fn mesh_quads(&mut self, a: &Arc, m: &GradientMesh, level: usize, cache: bool) -> Arc> { let key = (Arc::as_ptr(a) as usize, level); if cache && let Some(e) = self.live.meshes.get_mut(&key) && Arc::ptr_eq(&e.0, a) { e.2 = self.live.stamp; return e.1.clone(); } let q = Arc::new(m.quads(level)); if cache { self.live.meshes.insert(key, (a.clone(), q.clone(), self.live.stamp)); } q } /// Draw a live node reached through the tree walk (handles its transparency group). pub(crate) fn draw_live(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc) { let stamp = self.stamp; self.live.tick(stamp); let layered = !f.opts.outline && (a.opacity < 1.0 || a.blend != vectorcraft_color::BlendMode::Normal || a.isolate); if layered { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(a.blend)), Some(a.opacity), None, None); } self.draw_live_body(ctx, f, a, true); if layered { ctx.pop_layer(); } self.stats.drawn += 1; } /// Draw the evaluated content of a live node (no transparency group). pub(crate) fn draw_live_body(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc, cache: bool) { match &a.kind { NodeKind::Mesh(m) => self.draw_mesh(ctx, f, a, m, cache), NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => { let items = self.live_expanded(a, cache); for c in items.iter() { self.draw_arc(ctx, f, c); } } _ => {} } } fn draw_mesh(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc, m: &GradientMesh, cache: bool) { if !m.is_valid() { return; } if f.opts.outline { let bp = m.lines().to_bezpath(); self.hairline(ctx, f, &bp, [0, 0, 0, 255]); return; } let Some(b) = m.bounds() else { return }; let level = mesh_level(m, b.width().max(b.height()) / f.px.max(1e-12)); let quads = self.mesh_quads(a, m, level, cache); ctx.set_transform(f.view); ctx.set_fill_rule(peniko::Fill::NonZero); let grow = f.px * 0.5; let alpha = self.alpha; for q in quads.iter() { if q.opacity <= 0.0 { continue; } let c = Point::new((q.pts[0].x + q.pts[1].x + q.pts[2].x + q.pts[3].x) / 4.0, (q.pts[0].y + q.pts[1].y + q.pts[2].y + q.pts[3].y) / 4.0); let mut bp = BezPath::new(); for (i, p) in q.pts.iter().enumerate() { let d = *p - c; let len = d.hypot(); let p2 = if len > 1e-12 { *p + d * (grow / len) } else { *p }; if i == 0 { bp.move_to(p2); } else { bp.line_to(p2); } } bp.close_path(); let [r, g, bl, al] = q.color.to_rgba8(q.opacity * alpha); ctx.set_paint(peniko::Color::from_rgba8(r, g, bl, al)); ctx.fill_path(&bp); } } /// Draw a live node reached without its `Arc` (thumbnails, clip fallbacks): uncached. pub(crate) fn draw_live_node(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node) { let a = Arc::new(n.clone()); self.draw_live_body(ctx, f, &a, false); } }