mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
Merge pull request #3102 from dotCooCoo/fix/effect-art-follows-fonts-and-color-settings
Effects: effect art redraws when fonts arrive or the color settings change
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