mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Effects: effect art redraws when fonts arrive or the color settings change
The renderer kept the art it made for an object's effects while the object stayed the same. In the web build, type that a geometry effect outlines, on its own, in a group or in a symbol's art, kept the substitute font's outlines after its own font loaded. On the desktop, Refresh Font List left them on a symbol instance whose art holds the type. Objects with Revolve, Extrude & Bevel, Inflate or Rotate, and objects and groups with a color adjustment, kept the colors they were shaded or recolored with after the color settings changed. Live objects already followed both. The renderer now keeps that art with the inputs it keeps for the art of live objects: the font generation when the art holds type, the art of the symbols it shows, and the color settings. A temporary release test timed the added key at 11 microseconds a frame for 400 type objects with a drop shadow and 0.77 ms for 400 symbol instances of 100 paths each with a drop shadow, against warm frames of 5.3 and 72 ms. Docs: docs/target-app-parity.md, docs/gaps.md.
This commit is contained in:
@@ -227,6 +227,43 @@ fn live_objects_draw_through_new_color_settings() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The art drawn for 3D effects and color adjustments follows the Color Settings: in the next
|
||||
/// frame, a CMYK cyan rectangle with Revolve, with Extrude & Bevel, with Inflate, with Rotate, with
|
||||
/// Brightness/Contrast, and a group holding it with Brightness/Contrast, draw through the new
|
||||
/// working CMYK profile.
|
||||
#[test]
|
||||
fn effect_art_draws_through_new_color_settings() {
|
||||
let _g = settings();
|
||||
let old = cms::active_settings();
|
||||
let cyan = json!({"c": 100, "m": 0, "y": 0, "k": 0});
|
||||
let brighter = json!({"brightness": 30});
|
||||
let cases = [
|
||||
("the revolved rectangle", "threeD.revolve", json!({}), false),
|
||||
("the extruded rectangle", "threeD.extrude", json!({}), false),
|
||||
("the inflated rectangle", "threeD.inflate", json!({}), false),
|
||||
("the rotated rectangle", "threeD.rotate", json!({}), false),
|
||||
("the adjusted rectangle", "adjust.brightnessContrast", brighter.clone(), false),
|
||||
("the adjusted group", "adjust.brightnessContrast", brighter, true),
|
||||
];
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let frame = |r: &mut Renderer, s: &Session| r.render(&s.doc().unwrap().doc, 400, 300, Affine::IDENTITY, &opts).pixels;
|
||||
for (what, effect, params, grouped) in cases {
|
||||
let mut s = session();
|
||||
let shape = rect(&mut s, 150.0, 60.0, 60.0, 120.0);
|
||||
exec(&mut s, "paint.setFill", json!({"ids": [shape.0], "color": cyan}));
|
||||
let target = if grouped { id_of(&exec(&mut s, "object.group", json!({"ids": [shape.0]}))) } else { shape };
|
||||
exec(&mut s, "effect.apply", json!({"ids": [target.0], "effect": effect, "params": params}));
|
||||
let mut r = Renderer::new();
|
||||
let before = frame(&mut r, &s);
|
||||
s.execute("edit.colorSettings", &json!({"cmyk": cms::DEVICE_CMYK})).unwrap();
|
||||
let fresh = frame(&mut Renderer::new(), &s);
|
||||
let warm = frame(&mut r, &s);
|
||||
cms::set_active(&old).unwrap();
|
||||
assert!(fresh != before, "{what}: the new profile changes the frame");
|
||||
assert!(warm == fresh, "{what}: drawn as a new renderer draws it");
|
||||
}
|
||||
}
|
||||
|
||||
/// #2276: with a working RGB space other than sRGB, the Eyedropper takes an image's sRGB pixel
|
||||
/// into the working space, so the sampled fill shows as the pixel it was picked from.
|
||||
#[test]
|
||||
|
||||
+21
-3
@@ -108,6 +108,23 @@ fn evaluates_live(n: &Node) -> bool {
|
||||
is_live(n) || (is_container(n) && to_members && !effects::has_pathfinder(n) && subtree_has_live(n))
|
||||
}
|
||||
|
||||
/// Can the art [`Renderer::fx_art`] makes for `n` change while `n` stays the same, other than
|
||||
/// through live objects it evaluates? It can when `n` holds type (laid out in the fonts installed),
|
||||
/// a symbol instance (made from its symbol's art), or an object with a 3D effect or a color
|
||||
/// adjustment (shaded or recolored through the color settings), in it or in the opacity masks of
|
||||
/// the objects in it.
|
||||
fn reads_outside(n: &Node) -> bool {
|
||||
let mut reads = false;
|
||||
n.walk(&mut |c| {
|
||||
reads |= matches!(c.kind, NodeKind::Text(_) | NodeKind::SymbolInstance { .. })
|
||||
|| effects::has_revolve(c)
|
||||
|| effects::has_solid(c)
|
||||
|| effects::has_adjustment(c)
|
||||
|| c.extras().mask.as_ref().is_some_and(|m| reads_outside(&m.art))
|
||||
});
|
||||
reads
|
||||
}
|
||||
|
||||
/// What [`Renderer::raster_fx`] paints effects around.
|
||||
struct Content<'a> {
|
||||
/// The object the effects belong to (cached shadows are checked against it).
|
||||
@@ -1358,8 +1375,9 @@ impl Renderer {
|
||||
/// as their evaluated art (groups and layers: [`effects::evaluate_container`], keeping their
|
||||
/// knockout setting), recoloured by the colour adjustments in it ([`Self::adjusted_art`]);
|
||||
/// without the object's transparency and effects (a path keeps its geometry effects). Art that
|
||||
/// is made from live objects it evaluated ([`evaluates_live`]) is kept only while the inputs of
|
||||
/// their evaluation stay the same ([`Self::live_found`]).
|
||||
/// is made from live objects it evaluated ([`evaluates_live`]), or from type, symbol art or
|
||||
/// colors shaded through the color settings ([`reads_outside`]), is kept only while what it
|
||||
/// reads from outside `a` stays the same ([`Self::live_found`]).
|
||||
fn fx_art(&mut self, doc: &vectorcraft_doc::Document, a: &Arc<Node>, cache: bool) -> Arc<Node> {
|
||||
let key = Arc::as_ptr(a) as usize;
|
||||
let mut kept = None;
|
||||
@@ -1372,7 +1390,7 @@ impl Renderer {
|
||||
Some(was) => kept = Some((art.clone(), was.0)),
|
||||
}
|
||||
}
|
||||
let found = (kept.is_some() || (cache && evaluates_live(a))).then(|| self.live_found(doc, a));
|
||||
let found = (kept.is_some() || (cache && (evaluates_live(a) || reads_outside(a)))).then(|| self.live_found(doc, a));
|
||||
if let Some((art, was)) = kept
|
||||
&& found.as_ref().is_some_and(|f| f.0 == was)
|
||||
{
|
||||
|
||||
@@ -410,8 +410,9 @@ type ClipEntry = (Arc<Node>, Option<Arc<(BezPath, FillRule)>>);
|
||||
/// A clipping path (kept alive) and what it paints: its fills and its strokes ([`Node::clip_paint`]).
|
||||
type ClipPaintEntry = (Arc<Node>, Option<Arc<Node>>, Option<Arc<Node>>);
|
||||
/// An object with effects (kept alive), the art drawn for it ([`Renderer::fx_art`]), and, when that
|
||||
/// art can be made from evaluated live objects (`fx::evaluates_live`), the inputs of their
|
||||
/// evaluation ([`live::Found`]).
|
||||
/// art can be made from evaluated live objects (`fx::evaluates_live`) or from type, symbol art or
|
||||
/// colors shaded through the color settings (`fx::reads_outside`), the inputs it was made with
|
||||
/// ([`live::Found`]).
|
||||
type FxArtEntry = (Arc<Node>, Arc<Node>, Option<live::Found>);
|
||||
/// What symbol art placed by [`Renderer::placed_art`] is kept under: the art's address, the bits
|
||||
/// of the transform's coefficients, whether strokes scale with it, and the stain ([`stain_bits`]).
|
||||
|
||||
@@ -68,8 +68,8 @@ pub fn expand_live(n: &Node) -> Vec<Node> {
|
||||
crate::effects::expand_live(None, n)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// What the evaluation of `n` (a live node, or an object whose effect art the renderer keeps with
|
||||
/// its inputs, see [`Renderer::live_found`]) reads from the document, placed files,
|
||||
/// 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
|
||||
@@ -343,7 +343,7 @@ fn spread_to_edges(px: &mut [vello_cpu::color::PremulRgba8], filled: Vec<bool>,
|
||||
impl Renderer {
|
||||
/// The [`resources_key`] of `n` in `doc`, with the frame's [`DocInputs`] ([`Found`]): `n` is a
|
||||
/// live node, or an object with effects whose art ([`Renderer::fx_art`]) can be made from
|
||||
/// evaluated live objects.
|
||||
/// evaluated live objects, type, symbol art or colors shaded through the color settings.
|
||||
pub(crate) fn live_found(&mut self, doc: &Document, n: &Node) -> Found {
|
||||
let inputs = self.doc_inputs(doc);
|
||||
let (key, reads) = resources_key(doc, inputs.cms, n);
|
||||
|
||||
@@ -155,6 +155,60 @@ fn symbol_type_redraws_when_its_font_becomes_available() {
|
||||
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
|
||||
}
|
||||
|
||||
/// Type that a geometry effect outlines redraws in a font that becomes available: the art drawn for
|
||||
/// the effects of the type, of a group that holds it and of a symbol instance whose art holds it
|
||||
/// is made again when the fonts change.
|
||||
#[test]
|
||||
fn type_with_geometry_effects_redraws_when_its_font_becomes_available() {
|
||||
let _adding = adding_fonts();
|
||||
use vectorcraft_doc::Effect;
|
||||
let moved = || Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 0.0}), visible: true };
|
||||
let cases = [
|
||||
("Moving Serif 4", "the type with a Transform effect"),
|
||||
("Shadow Serif 5", "the type with a Transform effect and a drop shadow"),
|
||||
("Massed Serif 4", "the group with a Transform effect"),
|
||||
("Stamps Serif 4", "the symbol instance with a Transform effect"),
|
||||
("Darker Serif 4", "the type with a Transform effect in Multiply"),
|
||||
];
|
||||
for (i, (family, what)) in cases.into_iter().enumerate() {
|
||||
let mut d = Document::new(300.0, 150.0);
|
||||
let style = CharStyle { size: 50.0, font_family: family.into(), ..Default::default() };
|
||||
let at = if i == 3 { Point::new(-140.0, 20.0) } else { Point::new(10.0, 80.0) };
|
||||
let mut object = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(at, "Hamburg", style))));
|
||||
match i {
|
||||
0 => object.appearance.effects.push(moved()),
|
||||
1 => {
|
||||
object.appearance.effects.push(moved());
|
||||
let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100});
|
||||
object.appearance.effects.push(Effect { id: "stylize.dropShadow".into(), params, visible: true });
|
||||
}
|
||||
2 => {
|
||||
object = Node::group(d.alloc_id(), vec![Arc::new(object)]);
|
||||
object.appearance.effects.push(moved());
|
||||
}
|
||||
3 => {
|
||||
let art = Node::group(d.alloc_id(), vec![Arc::new(object)]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "Word".into(), art: Arc::new(art) });
|
||||
object = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Word".into(), xf: Affine::translate((150.0, 75.0)) });
|
||||
object.appearance.effects.push(moved());
|
||||
}
|
||||
_ => {
|
||||
object.appearance.effects.push(moved());
|
||||
object.blend = vectorcraft_color::BlendMode::Multiply;
|
||||
}
|
||||
}
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, object).unwrap();
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
|
||||
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(family)) > 0);
|
||||
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
|
||||
assert_ne!(before, after, "{what}: drawn in the font that arrived");
|
||||
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "{what}: as a new renderer draws it");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type that an envelope bends redraws in a font that becomes available, in the envelope and in
|
||||
/// the art of a symbol whose instance an envelope bends: the envelopes are evaluated again when the
|
||||
/// fonts change.
|
||||
|
||||
@@ -313,8 +313,6 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
on 2026-10-13, missed all three render budgets at 20k paths), a spatial index for zoomed-in frames, dirty-region
|
||||
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.
|
||||
- **Estimate:** 15–25 h (the budget run and fixes block beta).
|
||||
|
||||
## G16 Localization: the three missing key languages
|
||||
|
||||
@@ -818,6 +818,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
|
||||
- **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.
|
||||
|
||||
- **Effect art after font and color settings changes:** when a font becomes available (fonts added, Refresh Font List, `text.rescanFonts`), the next frame the canvas draws outlines type in it where a geometry effect reshapes the type, a group that holds it or a symbol instance whose art holds it. When the color settings change (Edit › Color Settings, `edit.colorSettings`), the next frame shades objects with Revolve, Extrude & Bevel, Inflate or Rotate, and recolors objects and groups with a color adjustment, through the new settings.
|
||||
|
||||
- **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