mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Render: objects with plug-in effects follow the plug-ins installed
The renderer kept the shapes it drew for plug-in effects while the object stayed the same. When a plug-in was installed for the first time, installed again or removed, a renderer that had drawn the object went on drawing the shapes from before the change. When the plug-in registry's revision changes, the renderer now drops the effect geometry and the effect art of objects with a plug-in effect, and the cull bounds of every object that is or holds a symbol instance or an object with a plug-in effect. Each thread then drops the pattern tiles whose art has a plug-in effect or shows symbols or patterns. The renderer adds the revision to the inputs of a raster when its content has a plug-in effect, or shows symbols or patterns while the document's symbol or pattern art has one, and to the key of a live object's evaluation when the object, or an object it shows, has a plug-in effect. With other builds running, a temporary release test timed that key for the 400 live objects in each of three documents at 11 to 609 microseconds a frame, against 9 to 433 before. Docs: docs/target-app-parity.md, docs/gaps.md.
This commit is contained in:
Generated
+2
@@ -4846,10 +4846,12 @@ dependencies = [
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-effects",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-plugins",
|
||||
"vectorcraft-testkit",
|
||||
"vectorcraft-text",
|
||||
"vello_common 0.2.0",
|
||||
"vello_cpu 0.2.0",
|
||||
"wat",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
|
||||
@@ -749,6 +749,18 @@ pub fn has_geometry(effects: &[Effect]) -> bool {
|
||||
effects.iter().any(|e| e.visible && is_geometry(&e.id))
|
||||
}
|
||||
|
||||
/// Does `n` have a visible plug-in effect, its own or a fill's or stroke's? Such an effect draws
|
||||
/// what the plug-in installed under its id draws ([`plugin_revision`]).
|
||||
pub fn has_plugin_effect(n: &Node) -> bool {
|
||||
let plugin = |e: &Effect| e.visible && vectorcraft_plugins::effect::plugin_id(&e.id).is_some();
|
||||
n.appearance.effects.iter().any(plugin) || n.appearance.items.iter().any(|i| i.effects().iter().any(plugin))
|
||||
}
|
||||
|
||||
/// The plug-in registry's revision, which changes whenever a plug-in is installed or removed.
|
||||
pub fn plugin_revision() -> u64 {
|
||||
vectorcraft_plugins::registry::revision()
|
||||
}
|
||||
|
||||
/// Context the geometry effects may need from the object's appearance.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct GeomContext<'a> {
|
||||
|
||||
@@ -31,6 +31,9 @@ vello_cpu = { workspace = true, features = ["multithreading"] }
|
||||
|
||||
[dev-dependencies]
|
||||
vectorcraft-testkit = { workspace = true }
|
||||
# Live-effect plug-ins the cache tests install, written in WebAssembly text.
|
||||
vectorcraft-plugins = { workspace = true }
|
||||
wat = { workspace = true }
|
||||
# Decoders the export tests read their files back with.
|
||||
image = { workspace = true, features = ["tga"] }
|
||||
tiff = "0.11"
|
||||
|
||||
+34
-12
@@ -25,7 +25,8 @@
|
||||
//! A cached raster is made again when something its content reads from outside the object
|
||||
//! changes ([`Inputs`]): the pattern definitions, symbols, images and brushes it uses, the
|
||||
//! document's raster resolution, color mode and substitution highlights, the knockout its object
|
||||
//! inherits, the color settings and fonts, and the options and layer settings it is drawn with.
|
||||
//! inherits, the color settings and fonts, the plug-ins its plug-in effects run, and the options
|
||||
//! and layer settings it is drawn with.
|
||||
//!
|
||||
//! The same applies to one fill or stroke's own raster effects (around that item alone) and to
|
||||
//! type, images, symbol instances and live objects ([`Renderer::draw_object_fx`]): their art is
|
||||
@@ -567,6 +568,9 @@ pub(crate) struct Reads {
|
||||
/// Type: the fonts, whether overprinting shows ([`crate::proof::overprints`]) and Document
|
||||
/// Setup's substitution highlights.
|
||||
text: bool,
|
||||
/// Plug-in effects on its objects: the plug-ins installed. Those in the symbol and pattern art
|
||||
/// it shows are found with the document's inputs ([`DocInputs`]).
|
||||
plugins: bool,
|
||||
}
|
||||
|
||||
impl Reads {
|
||||
@@ -581,7 +585,7 @@ impl Reads {
|
||||
let mut reads = Self::default();
|
||||
nodes.for_each(|n| reads.add(n));
|
||||
if reads.symbols || reads.patterns {
|
||||
return Self { patterns: true, symbols: true, images: true, brushes: true, text: true };
|
||||
return Self { patterns: true, symbols: true, images: true, brushes: true, text: true, plugins: reads.plugins };
|
||||
}
|
||||
reads
|
||||
}
|
||||
@@ -591,9 +595,16 @@ impl Reads {
|
||||
self != Self::default()
|
||||
}
|
||||
|
||||
/// Can installing or removing a plug-in change what the content draws? It can when an object in
|
||||
/// it has a plug-in effect, and when it shows symbol or pattern art.
|
||||
pub(crate) fn plugins_can_change(self) -> bool {
|
||||
self.plugins || self.symbols || self.patterns
|
||||
}
|
||||
|
||||
fn add(&mut self, n: &Node) {
|
||||
let pattern = |p: &vectorcraft_color::Paint| matches!(p, vectorcraft_color::Paint::Pattern { .. });
|
||||
n.walk(&mut |c| {
|
||||
self.plugins |= effects::has_plugin_effect(c);
|
||||
for item in &c.appearance.items {
|
||||
self.patterns |= pattern(item.paint());
|
||||
self.brushes |= matches!(item, AppearanceItem::Stroke(s) if s.brush.is_some());
|
||||
@@ -649,16 +660,19 @@ impl Inputs {
|
||||
/// What raster contents read from the document by name or key, and the color settings, as a frame
|
||||
/// finds them ([`Renderer::doc_inputs`]): hashes of the patterns' tilings
|
||||
/// ([`crate::pattern::hash_tiling`]), of the symbols' names and art and of the images' keys and
|
||||
/// pixels, and the address of the color settings. Art, pixels and previews are hashed by the
|
||||
/// addresses of their shared values, and names, keys, tiles, tile types and overlaps are hashed by
|
||||
/// value. The live cache keeps these inputs with each evaluated live object and with each mesh's
|
||||
/// quads (`crate::live`), and [`Renderer::fx_art`] keeps them with art that can be made from
|
||||
/// evaluated live objects.
|
||||
/// pixels, the address of the color settings, and whether the patterns' and symbols' art has a
|
||||
/// plug-in effect. Art, pixels and previews are hashed by the addresses of their shared values,
|
||||
/// and names, keys, tiles, tile types and overlaps are hashed by value. The live cache keeps these
|
||||
/// inputs with each evaluated live object and with each mesh's quads (`crate::live`), and
|
||||
/// [`Renderer::fx_art`] keeps them with art that can be made from evaluated live objects.
|
||||
pub(crate) struct DocInputs {
|
||||
patterns: u64,
|
||||
symbols: u64,
|
||||
images: u64,
|
||||
pub(crate) cms: usize,
|
||||
/// Whether an object in a pattern's or a symbol's art, or in an opacity mask there, has a
|
||||
/// visible plug-in effect.
|
||||
plugins: bool,
|
||||
/// Weak references to the values whose addresses the hashes hold: a weak reference keeps the
|
||||
/// allocation, not the value, so no other value gets one of those addresses while these
|
||||
/// inputs are kept.
|
||||
@@ -680,7 +694,7 @@ fn raster_bytes(pm: &vello_cpu::Pixmap) -> usize {
|
||||
/// appearance item's own result on it (filled in as items are drawn). The node is kept alive, so
|
||||
/// its address names it alone and a hit is exact.
|
||||
pub(crate) struct FxGeom {
|
||||
node: Arc<Node>,
|
||||
pub(crate) node: Arc<Node>,
|
||||
g: Arc<BezPath>,
|
||||
items: Vec<Option<Arc<BezPath>>>,
|
||||
pub(crate) stamp: u64,
|
||||
@@ -865,7 +879,8 @@ impl Renderer {
|
||||
/// `draw_shape` for nodes with live effects.
|
||||
pub(crate) fn draw_shape_fx(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, bp: &BezPath, rule: FillRule) {
|
||||
// The node `draw_arc` is drawing gets its geometry from the cache: `bp` is its own path,
|
||||
// made from the node alone, so the effects give the same result every frame.
|
||||
// made from the node alone, so the effects give the same result every frame while the
|
||||
// plug-ins installed stay the same (`check_plugins`).
|
||||
let owner = self.fx_owner.as_ref().filter(|o| std::ptr::eq(&***o, n)).cloned();
|
||||
let mut cached = owner.map(|o| {
|
||||
let key = Arc::as_ptr(&o) as usize;
|
||||
@@ -1331,7 +1346,9 @@ impl Renderer {
|
||||
/// isolated group, isolation mode; the pattern definitions, symbols and brush library it
|
||||
/// reads; for images and placed documents their pixels and files, the layer's Dim Images and
|
||||
/// the options they are drawn with; for type the fonts, whether overprinting shows
|
||||
/// ([`crate::proof::overprints`]) and Document Setup's substitution highlights.
|
||||
/// ([`crate::proof::overprints`]) and Document Setup's substitution highlights; and the
|
||||
/// plug-ins installed, when an object in it has a plug-in effect, and when it shows symbol or
|
||||
/// pattern art and an object in the document's symbol or pattern art has one.
|
||||
pub(crate) fn raster_inputs(&mut self, f: &Frame, n: &Node, reads: Reads) -> Inputs {
|
||||
let doc = self.doc_inputs(f.doc);
|
||||
let mut h = DefaultHasher::new();
|
||||
@@ -1360,6 +1377,9 @@ impl Renderer {
|
||||
let highlights = f.opts.highlight_substitutions.then_some((setup.highlight_substituted_fonts, setup.highlight_substituted_glyphs));
|
||||
(vectorcraft_text::FontDb::global().generation(), crate::proof::overprints(f.opts), highlights).hash(&mut h);
|
||||
}
|
||||
if reads.plugins || (doc.plugins && (reads.symbols || reads.patterns)) {
|
||||
effects::plugin_revision().hash(&mut h);
|
||||
}
|
||||
Inputs { reads, key: h.finish(), _doc: doc }
|
||||
}
|
||||
|
||||
@@ -1390,9 +1410,11 @@ impl Renderer {
|
||||
let d = match &self.doc_inputs {
|
||||
Some((_, d)) if (d.patterns, d.symbols, d.images, d.cms) == found => d.clone(),
|
||||
_ => {
|
||||
let art = doc.patterns.iter().flat_map(|p| &p.art).chain(doc.symbols.iter().map(|s| &s.art)).map(Arc::downgrade).collect();
|
||||
let doc_art = || doc.patterns.iter().flat_map(|p| &p.art).chain(doc.symbols.iter().map(|s| &s.art));
|
||||
let art = doc_art().map(Arc::downgrade).collect();
|
||||
let pixels = doc.images.values().flat_map(|b| std::iter::once(&b.bytes).chain(&b.proxy)).map(Arc::downgrade).collect();
|
||||
Arc::new(DocInputs { patterns, symbols, images, cms: found.3, _held: (art, pixels, Arc::downgrade(&cms)) })
|
||||
let plugins = doc_art().any(|a| crate::holds_plugin_effect(a));
|
||||
Arc::new(DocInputs { patterns, symbols, images, cms: found.3, plugins, _held: (art, pixels, Arc::downgrade(&cms)) })
|
||||
}
|
||||
};
|
||||
self.doc_inputs = Some((at, d.clone()));
|
||||
|
||||
@@ -421,6 +421,9 @@ pub struct Renderer {
|
||||
/// keeps the art's allocation, so no other art can get its address, and does not keep the
|
||||
/// art's content.
|
||||
symbols: Vec<(String, std::sync::Weak<Node>)>,
|
||||
/// The plug-in revision ([`effects::plugin_revision`]) that `geom`, `fx_geom` and `fx_arts` were
|
||||
/// last checked against (see [`Self::check_plugins`]).
|
||||
plugin_revision: u64,
|
||||
/// Expanded stroke outlines keyed by ([`Self::stroke_owner`], stroke layer address, width bits,
|
||||
/// tolerance level): panning re-renders reuse them instead of re-expanding every stroke.
|
||||
strokes: PtrMap<(usize, usize, u64, i32), StrokeEntry>,
|
||||
@@ -577,6 +580,7 @@ impl Renderer {
|
||||
threads: default_threads(),
|
||||
geom: PtrMap::default(),
|
||||
symbols: vec![],
|
||||
plugin_revision: 0,
|
||||
strokes: PtrMap::default(),
|
||||
cur: None,
|
||||
made_again: false,
|
||||
@@ -633,6 +637,7 @@ impl Renderer {
|
||||
return Rendered { width: w, height: h, pixels };
|
||||
}
|
||||
}
|
||||
self.check_plugins();
|
||||
// The document's own symbols: proofing rewrites the colors and blend modes of their art.
|
||||
self.check_symbols(doc);
|
||||
let prepared = proof::prepare(doc, opts, &mut self.proof_art);
|
||||
@@ -823,6 +828,7 @@ impl Renderer {
|
||||
|
||||
/// Render node `n` of `doc` (its resources) into `w`×`h` transparent pixels through `view`.
|
||||
pub fn render_node(&mut self, doc: &Document, n: &Arc<Node>, w: u16, h: u16, view: Affine) -> Option<Rendered> {
|
||||
self.check_plugins();
|
||||
self.check_symbols(doc);
|
||||
let inv = view.inverse();
|
||||
let visible = inv.transform_rect_bbox(Rect::new(0.0, 0.0, w as f64, h as f64));
|
||||
@@ -861,6 +867,7 @@ impl Renderer {
|
||||
background: Option<[u8; 4]>,
|
||||
opts: &RenderOptions,
|
||||
) -> Option<Rendered> {
|
||||
self.check_plugins();
|
||||
self.check_symbols(doc);
|
||||
let b = brush_fx::cull_bounds(&doc.symbol_boxes(), n)?;
|
||||
let s = (size as f64 - 2.0) / b.width().max(b.height()).max(1e-6);
|
||||
@@ -922,6 +929,24 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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`).
|
||||
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.
|
||||
fn clip_of(&mut self, a: &Arc<Node>) -> Option<Arc<(BezPath, FillRule)>> {
|
||||
@@ -1086,7 +1111,8 @@ impl Renderer {
|
||||
if vectorcraft_doc::live::is_live(a) || effects::has_pathfinder(a) {
|
||||
return self.draw_live(ctx, f, a);
|
||||
}
|
||||
// Its live-effect geometry depends on the node alone, so it can be kept across frames.
|
||||
// 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;
|
||||
@@ -2117,6 +2143,14 @@ fn holds_instance(n: &Node) -> bool {
|
||||
holds
|
||||
}
|
||||
|
||||
/// Does `n` hold an object with a visible plug-in effect ([`effects::has_plugin_effect`]), in it or
|
||||
/// in the opacity masks of the objects in it?
|
||||
fn holds_plugin_effect(n: &Node) -> bool {
|
||||
let mut holds = false;
|
||||
n.walk(&mut |c| holds |= effects::has_plugin_effect(c) || c.extras().mask.as_ref().is_some_and(|m| holds_plugin_effect(&m.art)));
|
||||
holds
|
||||
}
|
||||
|
||||
/// A symbol instance's stain ([`vectorcraft_brush::stain`]) as bits for a cache key: its color
|
||||
/// model, the color's components and the amount (all 0 without a stain).
|
||||
fn stain_bits(stain: Option<(vectorcraft_color::Color, f32)>) -> [u32; 6] {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! Blends and envelopes are evaluated by `vectorcraft_doc::live` (through
|
||||
//! `vectorcraft_effects::expand_live`, as the exporters do) and the resulting objects cached by
|
||||
//! `Arc` identity of the live node and by the inputs from outside it that [`resources_key`] hashes:
|
||||
//! the symbols, patterns and placed files it shows, the fonts for type, and the color settings (so the steps keep
|
||||
//! the symbols, patterns and placed files it shows, the fonts for type, the plug-ins for plug-in
|
||||
//! effects, and the color settings (so the steps keep
|
||||
//! stable `Arc`s and hit the geometry cache frame after frame, and editing a symbol redraws an
|
||||
//! envelope around an instance of it). 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
|
||||
@@ -69,12 +70,13 @@ pub fn expand_live(n: &Node) -> Vec<Node> {
|
||||
|
||||
/// What the evaluation of `n` (a live node, or an object whose effect art can be made from
|
||||
/// evaluated live objects, see [`Renderer::live_found`]) reads from the document, placed files,
|
||||
/// the fonts and the color settings, hashed: the art of the symbols, the tilings of the patterns
|
||||
/// and the reads of the placed files it shows ([`hash_resources`]), the font generation
|
||||
/// ([`vectorcraft_text::FontDb::generation`]) when it shows type or a placed file, and the color
|
||||
/// settings (`cms`, their address). Art is hashed by the addresses of its `Arc`s, which copies of
|
||||
/// the document share: a copy of the document has the same key. It also returns weak references
|
||||
/// to the reads found.
|
||||
/// the fonts, the plug-ins and the color settings, hashed: the art of the symbols, the tilings of
|
||||
/// the patterns and the reads of the placed files it shows ([`hash_resources`]), the font
|
||||
/// generation ([`vectorcraft_text::FontDb::generation`]) when it shows type or a placed file, the
|
||||
/// plug-in revision ([`crate::effects::plugin_revision`]) when it shows an object with a plug-in
|
||||
/// effect, and the color settings (`cms`, their address). Art is hashed by the addresses of its
|
||||
/// `Arc`s, which copies of the document share: a copy of the document has the same key. It also
|
||||
/// returns weak references to the reads found.
|
||||
fn resources_key(doc: &Document, cms: usize, n: &Node) -> (u64, Vec<Weak<ReadDocument>>) {
|
||||
let mut h = DefaultHasher::new();
|
||||
let mut shown = Shown::default();
|
||||
@@ -82,6 +84,9 @@ fn resources_key(doc: &Document, cms: usize, n: &Node) -> (u64, Vec<Weak<ReadDoc
|
||||
if shown.text {
|
||||
vectorcraft_text::FontDb::global().generation().hash(&mut h);
|
||||
}
|
||||
if shown.plugins {
|
||||
crate::effects::plugin_revision().hash(&mut h);
|
||||
}
|
||||
cms.hash(&mut h);
|
||||
(h.finish(), shown.reads)
|
||||
}
|
||||
@@ -93,6 +98,8 @@ struct Shown {
|
||||
seen: Vec<usize>,
|
||||
/// Whether type or a placed file was found.
|
||||
text: bool,
|
||||
/// Whether an object with a visible plug-in effect was found.
|
||||
plugins: bool,
|
||||
/// Weak references to the reads of the placed files found.
|
||||
reads: Vec<Weak<ReadDocument>>,
|
||||
}
|
||||
@@ -103,12 +110,13 @@ struct Shown {
|
||||
/// the read of each placed file that [`vectorcraft_doc::PlacedDocument::art`] shows (none before
|
||||
/// the file is read). It walks `n` and then, one at a time from a list, the opacity-mask, pattern
|
||||
/// and symbol art it finds, each pattern and each symbol's art once ([`first_seen`]). `shown`
|
||||
/// records the type, placed files and reads found.
|
||||
/// records the type, placed files, reads and plug-in effects found.
|
||||
fn hash_resources<'a>(doc: &'a Document, n: &'a Node, shown: &mut Shown, h: &mut DefaultHasher) {
|
||||
let mut todo = vec![];
|
||||
let mut next = Some(n);
|
||||
while let Some(art) = next {
|
||||
art.walk(&mut |c| {
|
||||
shown.plugins |= crate::effects::has_plugin_effect(c);
|
||||
let runs = match &c.kind {
|
||||
NodeKind::Text(t) => t.runs.as_slice(),
|
||||
_ => &[],
|
||||
|
||||
@@ -141,6 +141,18 @@ impl<K: Hash + Eq + Clone, V, S: BuildHasher + Default> FrameCache<K, V, S> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the entries whose values `keep` returns false for.
|
||||
pub(crate) fn retain(&mut self, mut keep: impl FnMut(&V) -> bool) {
|
||||
let bytes = &mut self.bytes;
|
||||
self.map.retain(|_, s| {
|
||||
let kept = keep(&s.value);
|
||||
if !kept {
|
||||
*bytes -= s.bytes;
|
||||
}
|
||||
kept
|
||||
});
|
||||
}
|
||||
|
||||
/// A new stamp for caches without frames: each request its own, so every other entry may make
|
||||
/// room for it (least recently used first).
|
||||
pub(crate) fn tick(&mut self) -> u64 {
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
//! rasterized from (art `Arc` identity, tile, tile type, overlap): the pattern's "revision", and,
|
||||
//! when its art shows more than itself (a symbol, another pattern, an image, type, a brush), while
|
||||
//! what the document holds of those is what it held then ([`Deps`]), and while the color settings
|
||||
//! are the ones it was drawn through.
|
||||
//! are the ones it was drawn through. A tile whose art has a plug-in effect, or shows symbols or
|
||||
//! patterns, is also dropped when a plug-in is installed or removed.
|
||||
//!
|
||||
//! Pattern editing mode (`Document::pattern_edit`) draws only the temporary tile layer, with
|
||||
//! dimmed copies of it at the neighbouring tile positions and the tile edge.
|
||||
@@ -95,11 +96,30 @@ struct PatternCache {
|
||||
tiles: [FrameCache<TileKey, TileEntry>; 4],
|
||||
renderer: Option<Box<Renderer>>,
|
||||
depth: u32,
|
||||
/// The plug-in revision ([`crate::effects::plugin_revision`]) the tiles were last checked
|
||||
/// against (see [`PatternCache::check_plugins`]).
|
||||
plugin_revision: u64,
|
||||
}
|
||||
|
||||
impl Default for PatternCache {
|
||||
fn default() -> Self {
|
||||
Self { tiles: std::array::from_fn(|_| FrameCache::new(lru::PATTERN_BUDGET, usize::MAX)), renderer: None, depth: 0 }
|
||||
Self { tiles: std::array::from_fn(|_| FrameCache::new(lru::PATTERN_BUDGET, usize::MAX)), renderer: None, depth: 0, plugin_revision: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl PatternCache {
|
||||
/// When a plug-in has been installed or removed since the last check, drop the tiles whose art
|
||||
/// can draw differently with it: art with a plug-in effect, and art that shows symbols or
|
||||
/// patterns ([`crate::fx::Reads::plugins_can_change`]).
|
||||
fn check_plugins(&mut self) {
|
||||
let revision = crate::effects::plugin_revision();
|
||||
if revision == self.plugin_revision {
|
||||
return;
|
||||
}
|
||||
self.plugin_revision = revision;
|
||||
for tiles in &mut self.tiles {
|
||||
tiles.retain(|e| !e.art.iter().any(|a| crate::fx::Reads::of(a).plugins_can_change()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +161,7 @@ fn rasterize(r: &mut Renderer, doc: &Document, def: &PatternDef, region: Rect, w
|
||||
let view = Affine::scale_non_uniform(w as f64 / region.width().max(1e-9), h as f64 / region.height().max(1e-9))
|
||||
* Affine::translate(-region.origin().to_vec2());
|
||||
let opts = RenderOptions::default();
|
||||
r.check_plugins();
|
||||
r.check_symbols(doc);
|
||||
let symbols = doc.symbol_boxes().quick();
|
||||
r.stamp += 1;
|
||||
@@ -212,6 +233,7 @@ pub(crate) fn set_pattern_paint(ctx: &mut RenderContext, name: &str, xf: Affine,
|
||||
let cms = vectorcraft_color::cms::active();
|
||||
let hit = CACHE.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
c.check_plugins();
|
||||
let tiles = &mut c.tiles[ink as usize];
|
||||
let t = tiles.tick();
|
||||
tiles
|
||||
|
||||
@@ -878,6 +878,228 @@ fn live_objects_draw_symbol_and_pattern_art_edits() {
|
||||
edit_draws_as_new(with_art(d, vec![envelope]), red_bar);
|
||||
}
|
||||
|
||||
/// Installs live-effect plug-in `id`, replacing one with the same id: its effect draws `shape` in
|
||||
/// place of the geometry it is given.
|
||||
fn install_shape(id: &str, shape: &vectorcraft_geom::PathData) {
|
||||
use vectorcraft_plugins::wat as tpl;
|
||||
let output = serde_json::json!({"objects": [{"path": shape}]}).to_string();
|
||||
let module = wat::parse_str(tpl::returning(&tpl::manifest(id, "effect", "{}"), &output)).unwrap();
|
||||
vectorcraft_plugins::registry::install_bytes(&module).unwrap();
|
||||
}
|
||||
|
||||
/// The triangle with corners (`x`, `y`), (`x` + `size`, `y`) and (`x` + `size` / 2, `y` + `size` / 2).
|
||||
fn triangle(x: f64, y: f64, size: f64) -> vectorcraft_geom::PathData {
|
||||
use vectorcraft_geom::{PathData, Point, SubPath};
|
||||
let corners = [Point::new(x, y), Point::new(x + size, y), Point::new(x + size / 2.0, y + size / 2.0)];
|
||||
PathData::single(SubPath::polyline(&corners, true))
|
||||
}
|
||||
|
||||
/// Draws `d` with live-effect plug-in `id` drawing `before` (`None`: not installed), installs the
|
||||
/// plug-in drawing `after` (`None`: removes it) and draws `d` again, as [`edit_draws_as_new`] does.
|
||||
#[track_caller]
|
||||
fn plugin_change_draws_as_new(d: Document, id: &str, before: Option<vectorcraft_geom::PathData>, after: Option<vectorcraft_geom::PathData>) {
|
||||
match &before {
|
||||
Some(shape) => install_shape(id, shape),
|
||||
None => {
|
||||
vectorcraft_plugins::registry::remove(id);
|
||||
}
|
||||
}
|
||||
edit_draws_as_new(d, |_| match &after {
|
||||
Some(shape) => install_shape(id, shape),
|
||||
None => assert!(vectorcraft_plugins::registry::remove(id), "the plug-in was installed"),
|
||||
});
|
||||
}
|
||||
|
||||
/// What plug-in effects draw follows the plug-ins installed. In the next frame, a square with a
|
||||
/// plug-in effect is drawn as the plug-in installed again with another shape draws it, and as it
|
||||
/// is without the plug-in after the plug-in is removed; after the plug-in is installed again, so
|
||||
/// are the square with a drop shadow, the square blurred, and an instance of a symbol whose art, a
|
||||
/// group kept between frames, holds the square. A square left of the view, alone and in a group,
|
||||
/// is drawn where a plug-in installed for the first time draws it, in the view, and so is an
|
||||
/// instance left of the view whose symbol's art holds the square, when the plug-in installed again
|
||||
/// draws a shape that reaches the view.
|
||||
#[test]
|
||||
fn objects_with_plugin_effects_follow_the_plugins_installed() {
|
||||
let _fonts = fonts_steady();
|
||||
use vectorcraft_geom::shapes::rectangle;
|
||||
const ID: &str = "test.render.objects";
|
||||
let (small, big) = (Some(triangle(30.0, 40.0, 30.0)), Some(triangle(30.0, 40.0, 120.0)));
|
||||
// A black square of `d` from (`x`, `y`), 40 × 40, with the plug-in effect and `more` effects.
|
||||
let square = |d: &mut Document, x: f64, y: f64, more: Vec<vectorcraft_doc::Effect>| {
|
||||
let effects = std::iter::once(effect(&format!("plugin.{ID}"), serde_json::json!({}))).chain(more).collect();
|
||||
filled(d, rectangle(Rect::new(x, y, x + 40.0, y + 40.0)), Paint::solid(Color::BLACK), effects)
|
||||
};
|
||||
let alone = |x: f64, more: Vec<vectorcraft_doc::Effect>| {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let n = square(&mut d, x, 30.0, more);
|
||||
with_art(d, vec![n])
|
||||
};
|
||||
plugin_change_draws_as_new(alone(20.0, vec![]), ID, small.clone(), big.clone());
|
||||
plugin_change_draws_as_new(alone(20.0, vec![]), ID, small.clone(), None);
|
||||
plugin_change_draws_as_new(alone(20.0, vec![drop_shadow()]), ID, small.clone(), big.clone());
|
||||
let blur = effect("blur.gaussian", serde_json::json!({"radius": 1.0}));
|
||||
plugin_change_draws_as_new(alone(20.0, vec![blur]), ID, small.clone(), big.clone());
|
||||
// An instance of symbol `Shaped`, a group of the square around the origin.
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = square(&mut d, -20.0, -20.0, vec![]);
|
||||
let art = Node::group(d.alloc_id(), vec![Arc::new(art)]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
|
||||
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((100.0, 50.0)) });
|
||||
plugin_change_draws_as_new(with_art(d, vec![instance]), ID, small.clone(), big);
|
||||
// Left of the view, the square is culled until the plug-in draws it in the view.
|
||||
plugin_change_draws_as_new(alone(-100.0, vec![]), ID, None, small.clone());
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let n = square(&mut d, -100.0, 30.0, vec![]);
|
||||
let group = Node::group(d.alloc_id(), vec![Arc::new(n)]);
|
||||
plugin_change_draws_as_new(with_art(d, vec![group]), ID, None, small);
|
||||
// The instance's box lies from (-50, 30) to (-10, 70). Its cull bounds grow by how far the
|
||||
// effects in the art paint past the art, which the plug-in's first shape does not.
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = square(&mut d, -20.0, -20.0, vec![]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
|
||||
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((-30.0, 50.0)) });
|
||||
plugin_change_draws_as_new(with_art(d, vec![instance]), ID, Some(triangle(-15.0, -15.0, 10.0)), Some(triangle(0.0, 0.0, 150.0)));
|
||||
}
|
||||
|
||||
/// Installing a plug-in between two frames leaves the path a renderer keeps for a plain square and
|
||||
/// the art it keeps for a group with a Transform effect: the second frame draws them from what was
|
||||
/// kept.
|
||||
#[test]
|
||||
fn installing_a_plugin_keeps_what_is_kept_for_other_objects() {
|
||||
let _fonts = fonts_steady();
|
||||
use vectorcraft_geom::shapes::rectangle;
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let square = filled(&mut d, rectangle(Rect::new(20.0, 30.0, 60.0, 70.0)), Paint::solid(Color::BLACK), vec![]);
|
||||
let inner = filled(&mut d, rectangle(Rect::new(120.0, 30.0, 160.0, 70.0)), Paint::solid(Color::BLACK), vec![]);
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(inner)]);
|
||||
group.appearance.effects.push(effect("distort.transform", serde_json::json!({"moveH": 10.0})));
|
||||
let d = with_art(d, vec![square, group]);
|
||||
let [square, group] = [0, 1].map(|i| d.layers[0].children().unwrap()[i].clone());
|
||||
let kept = |r: &mut Renderer| {
|
||||
let path = r.geom.get(&(Arc::as_ptr(&square) as usize)).and_then(|e| e.path.clone()).unwrap();
|
||||
let art = r.fx_arts.get(&(Arc::as_ptr(&group) as usize), r.stamp).map(|e| e.1.clone()).unwrap();
|
||||
(path, art)
|
||||
};
|
||||
let opts = RenderOptions::default();
|
||||
let mut r = Renderer::new();
|
||||
r.render(&d, 200, 100, Affine::IDENTITY, &opts);
|
||||
let before = kept(&mut r);
|
||||
install_shape("test.render.other", &triangle(0.0, 0.0, 10.0));
|
||||
r.render(&d, 200, 100, Affine::IDENTITY, &opts);
|
||||
let after = kept(&mut r);
|
||||
assert!(Arc::ptr_eq(&before.0, &after.0), "the square's path is kept");
|
||||
assert!(Arc::ptr_eq(&before.1, &after.1), "the group's art is kept");
|
||||
}
|
||||
|
||||
/// The art drawn for an object's effects follows the plug-ins installed: in the next frame, a group
|
||||
/// with a plug-in effect, the same group feathered (Feather clips to the art's outline), an
|
||||
/// instance of symbol `S` ([`symbol_art`]) and type, each with a plug-in effect, are drawn as the
|
||||
/// plug-in installed again with another shape draws them.
|
||||
#[test]
|
||||
fn effect_art_follows_the_plugins_installed() {
|
||||
let _fonts = fonts_steady();
|
||||
use vectorcraft_doc::text::{CharStyle, TextObject};
|
||||
use vectorcraft_geom::shapes::rectangle;
|
||||
const ID: &str = "test.render.art";
|
||||
let plugin = || effect(&format!("plugin.{ID}"), serde_json::json!({}));
|
||||
let (small, big) = (Some(triangle(30.0, 40.0, 30.0)), Some(triangle(30.0, 40.0, 120.0)));
|
||||
// A group of a black square, with the plug-in effect and `more` effects.
|
||||
let group = |more: Vec<vectorcraft_doc::Effect>| {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let square = filled(&mut d, rectangle(Rect::new(20.0, 30.0, 60.0, 70.0)), Paint::solid(Color::BLACK), vec![]);
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(square)]);
|
||||
group.appearance.effects = std::iter::once(plugin()).chain(more).collect();
|
||||
with_art(d, vec![group])
|
||||
};
|
||||
plugin_change_draws_as_new(group(vec![]), ID, small.clone(), big.clone());
|
||||
let feather = effect("stylize.feather", serde_json::json!({"radius": 2.0}));
|
||||
plugin_change_draws_as_new(group(vec![feather]), ID, small.clone(), big.clone());
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = symbol_art(&mut d);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) });
|
||||
let mut instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((100.0, 50.0)) });
|
||||
instance.appearance.effects.push(plugin());
|
||||
plugin_change_draws_as_new(with_art(d, vec![instance]), ID, small.clone(), big.clone());
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let style = CharStyle { size: 30.0, ..Default::default() };
|
||||
let mut text = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(vectorcraft_geom::Point::new(10.0, 60.0), "Hamburg", style))));
|
||||
text.appearance.effects.push(plugin());
|
||||
plugin_change_draws_as_new(with_art(d, vec![text]), ID, small, big);
|
||||
}
|
||||
|
||||
/// Live objects follow the plug-ins installed: in the next frame, an envelope that distorts the
|
||||
/// appearance of a band with a plug-in effect, the same envelope without Distort Appearance, an
|
||||
/// envelope that distorts the appearance of an instance of a symbol whose art has a plug-in effect,
|
||||
/// and a blend of two squares with a plug-in effect are drawn as the plug-in installed again with
|
||||
/// another shape draws them.
|
||||
#[test]
|
||||
fn live_objects_follow_the_plugins_installed() {
|
||||
let _fonts = fonts_steady();
|
||||
use vectorcraft_geom::shapes::rectangle;
|
||||
const ID: &str = "test.render.live";
|
||||
let plugin = || effect(&format!("plugin.{ID}"), serde_json::json!({}));
|
||||
let (small, big) = (Some(triangle(30.0, 40.0, 30.0)), Some(triangle(30.0, 40.0, 120.0)));
|
||||
let black = |d: &mut Document, r: Rect| filled(d, rectangle(r), Paint::solid(Color::BLACK), vec![plugin()]);
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let band = black(&mut d, Rect::new(20.0, 30.0, 180.0, 70.0));
|
||||
let envelope = arc_envelope(&mut d, vec![band], true);
|
||||
plugin_change_draws_as_new(with_art(d, vec![envelope]), ID, small.clone(), big.clone());
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let band = black(&mut d, Rect::new(20.0, 30.0, 180.0, 70.0));
|
||||
let envelope = arc_envelope(&mut d, vec![band], false);
|
||||
plugin_change_draws_as_new(with_art(d, vec![envelope]), ID, small.clone(), big.clone());
|
||||
// Symbol `Shaped` is a square with the plug-in effect.
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = black(&mut d, Rect::new(-20.0, -20.0, 20.0, 20.0));
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
|
||||
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((100.0, 50.0)) });
|
||||
let envelope = arc_envelope(&mut d, vec![instance], true);
|
||||
plugin_change_draws_as_new(with_art(d, vec![envelope]), ID, small.clone(), big.clone());
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let keys = [Rect::new(20.0, 30.0, 40.0, 50.0), Rect::new(140.0, 30.0, 160.0, 50.0)].map(|r| Arc::new(black(&mut d, r)));
|
||||
let spec = vectorcraft_doc::live::BlendSpec { spacing: vectorcraft_doc::live::BlendSpacing::Steps(3), ..Default::default() };
|
||||
let blend = Node::new(d.alloc_id(), NodeKind::Blend { children: keys.to_vec(), spec: Box::new(spec) });
|
||||
plugin_change_draws_as_new(with_art(d, vec![blend]), ID, small, big);
|
||||
}
|
||||
|
||||
/// Pattern tiles and the rasters of raster effects follow the plug-ins installed when the art of a
|
||||
/// pattern or of a symbol has a plug-in effect: in the next frame, a band filled with such a
|
||||
/// pattern, the band with a drop shadow, and a group with a drop shadow around an instance of such
|
||||
/// a symbol are drawn as the plug-in installed again with another shape draws them.
|
||||
#[test]
|
||||
fn pattern_and_symbol_art_with_plugin_effects_follow_the_plugins_installed() {
|
||||
let _fonts = fonts_steady();
|
||||
use vectorcraft_doc::pattern::{PatternDef, pattern_paint};
|
||||
use vectorcraft_geom::shapes::rectangle;
|
||||
const ID: &str = "test.render.tiles";
|
||||
let plugin = || effect(&format!("plugin.{ID}"), serde_json::json!({}));
|
||||
// Pattern `Shaped` is a 10 × 10 square with the plug-in effect in a 20 × 20 tile, inside which
|
||||
// the plug-in draws; a band of `d` is filled with it.
|
||||
let band = |d: &mut Document, effects: Vec<vectorcraft_doc::Effect>| {
|
||||
let square = filled(d, rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Paint::solid(Color::BLACK), vec![plugin()]);
|
||||
let mut def = PatternDef::new("Shaped", vec![Arc::new(square)]);
|
||||
def.tile = Rect::new(0.0, 0.0, 20.0, 20.0);
|
||||
d.patterns.push(def);
|
||||
filled(d, rectangle(Rect::new(20.0, 20.0, 180.0, 80.0)), pattern_paint("Shaped"), effects)
|
||||
};
|
||||
let (small, big) = (Some(triangle(0.0, 0.0, 10.0)), Some(triangle(0.0, 0.0, 20.0)));
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let n = band(&mut d, vec![]);
|
||||
plugin_change_draws_as_new(with_art(d, vec![n]), ID, small.clone(), big.clone());
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let n = band(&mut d, vec![drop_shadow()]);
|
||||
plugin_change_draws_as_new(with_art(d, vec![n]), ID, small, big);
|
||||
// Symbol `Shaped` is a square around the origin with the plug-in effect, which draws inside
|
||||
// the instance's box, from (80, 30) to (120, 70).
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
let art = filled(&mut d, rectangle(Rect::new(-20.0, -20.0, 20.0, 20.0)), Paint::solid(Color::BLACK), vec![plugin()]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Shaped".into(), art: Arc::new(art) });
|
||||
let instance = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Shaped".into(), xf: Affine::translate((100.0, 50.0)) });
|
||||
let mut group = Node::group(d.alloc_id(), vec![Arc::new(instance)]);
|
||||
group.appearance.effects.push(drop_shadow());
|
||||
plugin_change_draws_as_new(with_art(d, vec![group]), ID, Some(triangle(82.0, 32.0, 10.0)), Some(triangle(82.0, 32.0, 30.0)));
|
||||
}
|
||||
|
||||
fn effect(id: &str, params: serde_json::Value) -> vectorcraft_doc::Effect {
|
||||
vectorcraft_doc::Effect { id: id.into(), params, visible: true }
|
||||
}
|
||||
|
||||
+1
-2
@@ -258,8 +258,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
rendering, the GPU backend spike (M5). Large-selection edits, cached live effects and Pathfinder Trim, Merge,
|
||||
Crop and Outline landed in #1173.
|
||||
- **Not redrawn:** the canvas keeps drawing these objects as they were:
|
||||
in the web build, type with a geometry effect keeps a substitute font's outlines after its own font has loaded;
|
||||
an object with a plug-in effect keeps the shape it was drawn with after the plug-in is installed, installed again or removed.
|
||||
in the web build, type with a geometry effect keeps a substitute font's outlines after its own font has loaded.
|
||||
- **Estimate:** 15–25 h (the budget run and fixes block beta).
|
||||
|
||||
## G16 Localization: the three missing key languages
|
||||
|
||||
@@ -614,6 +614,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
- **Live objects after document changes:** a blend, envelope or repeat, with or without an effect of its own, is evaluated again when one of these changes: the tiling of a pattern whose tiles an envelope bends (Distort Pattern Fills), a symbol shown inside a symbol, a pattern or an opacity mask, the fonts of its type, a placed document's file once the file is read, and the color settings. A gradient mesh, with or without an effect of its own, is colored again when the color settings change.
|
||||
|
||||
- **Plug-in effects after a plug-in changes:** when a plug-in is installed for the first time, installed again or removed (`plugin.install`, `plugin.reload`, `plugin.remove`, File › Open of a `.wasm` file, the Additional Plug-ins Folder preference), the next frame the canvas draws uses the plug-ins then installed for objects with a plug-in effect and their raster effects, for the blends, envelopes and repeats that show such objects, and for pattern tiles and symbol instances whose art holds one.
|
||||
|
||||
- **Full font names (#1258):** OpenType name ID 4 identifies its face alongside family/legacy/PostScript names, including localized names, for loaded fonts, cataloged files and Missing Fonts searches. Synthetic name-table regressions cover full names distinct from their PostScript name, real-family precedence and variable fonts' default faces. Windows font-service activation remains unverified; scores and hours are unchanged.
|
||||
### Corpus integrity evidence
|
||||
|
||||
|
||||
Reference in New Issue
Block a user