Symbols: redefining a dynamic symbol drops paints its new objects do not draw

When a dynamic symbol is redefined or edited, each instance kept every
appearance change whose path still named an object, even when that
object is now one that draws no fill or stroke of its own (an image, a
blend, a mesh) or a symbol instance given a gradient or pattern fill.
symbol.instance.setAppearance refuses such a paint, but the change stayed
on the instance and still counted as a color the document uses.

Fitting the changes to the new art now also clears a fill or stroke the
object at that path does not draw, and drops a change left empty.
This commit is contained in:
dotCooCoo
2026-11-12 00:18:05 -08:00
parent 48e066f475
commit 175f51827d
5 changed files with 66 additions and 16 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ use brushes::{
brush_apply, brush_delete, brush_duplicate, brush_freehand, brush_get, brush_list, brush_move, brush_new, brush_options, brush_remove,
brush_selection_art, brush_set_current, brush_unused, expand_brush,
};
pub(crate) use dynamic::{edit_part, part_only, selected_part};
pub(crate) use dynamic::{edit_part, fit_parts, part_only, selected_part};
use dynamic::{symbol_instance_reset, symbol_instance_select_part, symbol_instance_set_appearance};
use symbols::selected_instances;
pub(crate) use symbols::{
+34 -9
View File
@@ -2,7 +2,7 @@
//! appearance of objects in the symbol's art and stays linked to the symbol
//! (`symbol.instance.setAppearance`, `symbol.instance.reset`). The changes live on the instance
//! ([`vectorcraft_doc::symbol_overrides`]); editing the symbol keeps those whose object is still
//! there ([`fit_overrides`]).
//! there and draws them ([`fit_overrides`]).
use std::sync::Arc;
@@ -27,7 +27,7 @@ pub(super) fn type_id(dynamic: bool) -> &'static str {
}
/// Fit the appearance changes of instances `ids` to their symbols: an instance of a static (or
/// missing) symbol keeps none, an instance of a dynamic one those naming an object of its art.
/// missing) symbol keeps none, an instance of a dynamic one those its art still draws ([`fit_parts`]).
pub(super) fn fit_overrides(d: &mut Document, ids: &[NodeId]) {
for id in ids {
let art = match d.node(*id) {
@@ -40,7 +40,7 @@ pub(super) fn fit_overrides(d: &mut Document, ids: &[NodeId]) {
let Some(n) = d.node_mut(*id) else { continue };
let mut e = n.extras_mut();
match art {
Some(art) => symbol_overrides::retain_parts(&mut e.symbol_overrides, &art),
Some(art) => fit_parts(&mut e.symbol_overrides, &art),
None => e.symbol_overrides.clear(),
}
}
@@ -177,17 +177,42 @@ fn check_paint_kinds(p: &Value, art: &Node, path: &[u32], cmd: &str) -> Result<(
fn solid_fill_check(p: &Value, art: &Node, path: &[u32], fill: Option<&vectorcraft_color::Paint>, cmd: &str) -> Result<()> {
let asked = p.get("fill").is_some_and(|v| !v.is_null());
let is_instance = symbol_overrides::part(art, path).is_some_and(|n| matches!(n.kind, NodeKind::SymbolInstance { .. }));
let draws_nothing = |f: &vectorcraft_color::Paint| match f {
vectorcraft_color::Paint::Gradient(_) => true,
vectorcraft_color::Paint::Pattern { .. } => true,
vectorcraft_color::Paint::None | vectorcraft_color::Paint::Solid { .. } => false,
};
if asked && is_instance && fill.is_some_and(draws_nothing) {
if asked && is_instance && fill.is_some_and(|f| !stains(f)) {
return Err(bad(cmd, "fill on a symbol instance must be a solid color or none"));
}
Ok(())
}
/// Can a symbol instance's fill be `f`? An instance stains its art with a solid fill only.
fn stains(f: &vectorcraft_color::Paint) -> bool {
match f {
vectorcraft_color::Paint::Gradient(_) => false,
vectorcraft_color::Paint::Pattern { .. } => false,
vectorcraft_color::Paint::None | vectorcraft_color::Paint::Solid { .. } => true,
}
}
/// Keep the appearance changes `overrides` that name an object of `art` (a symbol's new art), each
/// without a fill or stroke that object does not draw ([`paint_refusal`], [`stains`]); a change
/// left with nothing in it is dropped.
pub(crate) fn fit_parts(overrides: &mut Vec<SymbolOverride>, art: &Node) {
symbol_overrides::retain_parts(overrides, art);
for o in overrides.iter_mut() {
let Some(n) = symbol_overrides::part(art, &o.path) else { continue };
let instance = matches!(n.kind, NodeKind::SymbolInstance { .. });
if paint_refusal(n, "fill", FIT).is_some() || (instance && o.fill.as_ref().is_some_and(|f| !stains(f))) {
o.fill = None;
}
if paint_refusal(n, "stroke", FIT).is_some() {
o.stroke = None;
}
}
overrides.retain(|o| !o.is_empty());
}
/// The command name [`fit_parts`] gives [`paint_refusal`], whose refusal it does not show.
const FIT: &str = "symbol.update";
/// `fill` or `stroke` of `p` for override `o`: absent leaves it, null clears it, a colour string or
/// an object of `paint.setFill`'s paint params sets it.
fn paint_change(s: &Session, p: &Value, key: &str, cmd: &str) -> Result<Option<Option<vectorcraft_color::Paint>>> {
+4 -4
View File
@@ -19,7 +19,7 @@ use vectorcraft_doc::symbol_overrides::SymbolOverride;
use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Selection, SymbolEdit, SymbolRegistration};
use vectorcraft_geom::{Affine, Point};
use super::brushsym::{natural_size, place_xf};
use super::brushsym::{fit_parts, natural_size, place_xf};
use super::{EngineError, Result, Session, bad, number_or, object_param, ok, str_param};
/// The name of the undo step that enters symbol editing mode.
@@ -425,8 +425,8 @@ fn each_changed_instance(n: &mut Node, name: &str, f: &mut dyn FnMut(&mut Vec<Sy
}
/// Each instance's appearance changes to objects of symbol `name`'s art follow those objects
/// through an edit of the editing layer (see [`follow_paths`]), and keep only the changes to
/// objects the art still has. That covers the instances on the layers and in the art of the
/// through an edit of the editing layer (see [`follow_paths`]), and keep only the changes the art
/// still draws ([`fit_parts`]). That covers the instances on the layers and in the art of the
/// document's other symbols, graph designs, pattern tiles (the tiles pattern editing's Cancel puts
/// back included) and brushes, opacity-mask art included. The editing layer holds no instance of
/// the symbol, since [`sync`] refuses one.
@@ -435,7 +435,7 @@ fn follow_objects(d: &mut Document, name: &str, old: (&[Arc<Node>], bool), new:
let mut follow = |o: &mut Vec<SymbolOverride>| {
follow_paths(o, old, new);
match &art {
Some(art) => vectorcraft_doc::symbol_overrides::retain_parts(o, art),
Some(art) => fit_parts(o, art),
None => o.clear(),
}
};
@@ -432,6 +432,30 @@ fn redefining_the_symbol_keeps_the_changes_whose_objects_remain() {
assert_eq!(overrides(&s, inst), 0);
}
#[test]
fn redefining_the_symbol_drops_paints_its_new_objects_do_not_draw() {
let mut s = session();
let inst = pair(&mut s, "dynamic");
let p = json!({"id": inst.0, "path": [1], "fill": "#ff0000", "stroke": "#ff0000", "opacity": 50});
s.execute("symbol.instance.setAppearance", &p).unwrap();
// The second object is now an image, which draws no fill or stroke of its own.
let a = square(&mut s, 10.0);
place_image(&mut s);
let image = top(&s);
s.execute("symbol.update", &json!({"name": "Pair", "ids": [a.0, image.0]})).unwrap();
let o = node(&s, inst).extras().symbol_overrides.clone();
assert_eq!(o.len(), 1, "the opacity change stays: {o:?}");
assert_eq!((&o[0].fill, &o[0].stroke, o[0].opacity), (&None, &None, Some(0.5)));
// A change of paint alone goes with its paint: the first object is now a blend.
s.execute("symbol.instance.setAppearance", &json!({"id": inst.0, "path": [0], "stroke": "#ff0000"})).unwrap();
let (b, c) = (square(&mut s, 10.0), square(&mut s, 60.0));
let blend = id(&s.execute("object.blend.make", &json!({"ids": [b.0, c.0], "steps": 1})).unwrap());
let d = square(&mut s, 120.0);
s.execute("symbol.update", &json!({"name": "Pair", "ids": [blend.0, d.0]})).unwrap();
let o = node(&s, inst).extras().symbol_overrides.clone();
assert_eq!(o.iter().map(|o| o.path.clone()).collect::<Vec<_>>(), [vec![1]], "{o:?}");
}
#[test]
fn replace_symbol_drops_the_changes_made_for_the_old_symbol() {
let mut s = session();
+3 -2
View File
@@ -2575,8 +2575,9 @@ already walks Image Trace. Short agent entry points:
locked or sits in a hidden or locked group or layer. Choosing another selection tool selects the instance whole again.
`symbol.instance.reset {ids?}` drops the selected instances' changes (the Properties
panel's Reset). Redefining the symbol (`symbol.update`) reshapes every instance and keeps each change whose path the
new art still has; `symbol.breakLink` bakes the changes into the expanded art; `symbol.edit` edits the symbol's own
art, and each edit there keeps the changes whose path the art still has; making the symbol static drops them, and
new art still has, less a fill or stroke the object now at that path does not draw (a change left empty is dropped);
`symbol.breakLink` bakes the changes into the expanded art; `symbol.edit` edits the symbol's own art, and each edit
there keeps the changes the same way; making the symbol static drops them, and
so does `symbol.replace` with a different symbol (a replace with the instance's own symbol keeps them).
```json