Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol

This commit is contained in:
bflatastic
2026-11-01 04:53:38 +07:00
parent 3f1928efc8
commit f22caa6108
105 changed files with 1218 additions and 212 deletions
+151
View File
@@ -0,0 +1,151 @@
//! Dynamic symbols (Symbol Options › Symbol Type): an instance of a dynamic symbol changes the
//! 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`]).
use std::sync::Arc;
use vectorcraft_color::BlendMode;
use vectorcraft_doc::symbol_overrides::{self, SymbolOverride};
use super::{Document, EngineError, Node, NodeId, NodeKind, Result, Session, Value, bad, check_numbers, ids_json, json, roots, selected_instances};
/// The `type` param, Symbol Options › Symbol Type: `Some(true)` for dynamic.
pub(super) fn type_param(p: &Value, cmd: &str) -> Result<Option<bool>> {
let Some(v) = p.get("type").filter(|v| !v.is_null()) else { return Ok(None) };
match v.as_str() {
Some("static") => Ok(Some(false)),
Some("dynamic") => Ok(Some(true)),
_ => Err(bad(cmd, format!("type must be static or dynamic, not {v}"))),
}
}
/// The `type` a symbol reports.
pub(super) fn type_id(dynamic: bool) -> &'static str {
if dynamic { "dynamic" } else { "static" }
}
/// 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.
pub(super) fn fit_overrides(d: &mut Document, ids: &[NodeId]) {
for id in ids {
let art = match d.node(*id) {
Some(n) if !n.extras().symbol_overrides.is_empty() => match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).filter(|s| s.dynamic).map(|s| s.art.clone()),
_ => continue,
},
_ => continue,
};
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),
None => e.symbol_overrides.clear(),
}
}
}
/// The instance `id` names (else the one selected object) and its symbol's art; the symbol must
/// be dynamic.
fn dynamic_instance(s: &Session, p: &Value, cmd: &str) -> Result<(NodeId, Arc<Node>)> {
let d = &s.doc()?.doc;
let not_one = || EngineError::Other("select a symbol instance".into());
let [id] = roots(s, p)?[..] else { return Err(not_one()) };
let Some(NodeKind::SymbolInstance { symbol, .. }) = d.node(id).map(|n| &n.kind) else { return Err(not_one()) };
let sym = d.symbols.iter().find(|x| x.name == *symbol).ok_or_else(|| bad(cmd, format!("no symbol named `{n}`", n = symbol)))?;
if !sym.dynamic {
let name = &sym.name;
return Err(bad(cmd, format!("`{name}` is a static symbol: set its Symbol Type to Dynamic first")));
}
Ok((id, sym.art.clone()))
}
/// The `path` param: child indices from the art's root, naming an object of `art`.
fn path_param(p: &Value, art: &Node, cmd: &str) -> Result<Vec<u32>> {
let path: Option<Vec<u32>> =
p.get("path").and_then(Value::as_array).and_then(|a| a.iter().map(|i| i.as_u64().and_then(|i| u32::try_from(i).ok())).collect());
let path = path.ok_or_else(|| bad(cmd, "path must be a list of child indices"))?;
if symbol_overrides::part(art, &path).is_none() {
let shown = json!(path);
return Err(bad(cmd, format!("no object at path {shown} in the symbol's art")));
}
Ok(path)
}
/// `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>>> {
let Some(v) = p.get(key) else { return Ok(None) };
if v.is_null() {
return Ok(Some(None));
}
let v = if v.is_string() { json!({ "color": v }) } else { v.clone() };
let paint = crate::cmd::paint::paint_from(s, &v)?.ok_or_else(|| bad(cmd, "give color, none, swatch or gradient"))?;
Ok(Some(Some(paint)))
}
/// The change `p` asks of the override `old` (for the object at `path`).
fn changed(s: &Session, p: &Value, old: SymbolOverride, cmd: &str) -> Result<SymbolOverride> {
let mut o = old;
if let Some(fill) = paint_change(s, p, "fill", cmd)? {
o.fill = fill;
}
if let Some(stroke) = paint_change(s, p, "stroke", cmd)? {
o.stroke = stroke;
}
check_numbers(p, &["opacity"], cmd)?;
if let Some(v) = p.get("opacity") {
o.opacity = v.as_f64().map(crate::cmd::opacitymask::percent);
}
if let Some(v) = p.get("blend") {
o.blend = match v {
Value::Null => None,
b => Some(b.as_str().and_then(BlendMode::parse).ok_or_else(|| bad(cmd, format!("unknown blend mode {b}")))?),
};
}
Ok(o)
}
pub(super) fn symbol_instance_set_appearance(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "symbol.instance.setAppearance";
if !["fill", "stroke", "opacity", "blend"].iter().any(|k| p.get(k).is_some()) {
return Err(bad(C, "give fill, stroke, opacity and/or blend"));
}
let (id, art) = dynamic_instance(s, p, C)?;
let path = path_param(p, &art, C)?;
let d = &s.doc()?.doc;
let old = d.node(id).and_then(|n| n.extras().symbol_overrides.iter().find(|o| o.path == path).cloned());
let new = changed(s, p, old.unwrap_or_else(|| SymbolOverride { path: path.clone(), ..Default::default() }), C)?;
let count = s.edit("Symbol Instance Appearance", |d, _| {
let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?;
let mut e = n.extras_mut();
let list = &mut e.symbol_overrides;
list.retain(|o| o.path != path);
if !new.is_empty() {
list.push(new);
}
Ok(list.len())
})?;
Ok(json!({ "id": id.0, "overrides": count }))
}
pub(super) fn symbol_instance_reset(s: &mut Session, p: &Value) -> Result<Value> {
let ids = selected_instances(s, p)?;
if ids.is_empty() {
return Err(EngineError::Other("no symbol instances selected".into()));
}
let d = &s.doc()?.doc;
let changed: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some_and(|n| !n.extras().symbol_overrides.is_empty())).collect();
if !changed.is_empty() {
s.edit("Reset Symbol Instance", |d, _| {
for id in &changed {
if let Some(n) = d.node_mut(*id) {
n.extras_mut().symbol_overrides.clear();
}
}
Ok(())
})?;
}
Ok(json!({ "ids": ids_json(&changed) }))
}
+46 -18
View File
@@ -1,3 +1,4 @@
use super::dynamic::{fit_overrides, type_id, type_param};
use super::{
Affine, AppearanceItem, Arc, CURRENT_SYMBOL, Color, Document, EngineError, FillLayer, HALF, Node, NodeId, NodeKind, Paint, Point, Result,
SIZES_KEY, Session, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration, Value, Vec2, bad, brush, color_value, ids_json, json,
@@ -128,6 +129,7 @@ pub(super) fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
"registration": d.symbol_registration(&sym.name).0,
"exportType": d.symbol_export_type(&sym.name).id(),
"nineSlice": nine_slice_json(d, &sym.name),
"type": type_id(sym.dynamic),
})
})
.collect();
@@ -138,7 +140,7 @@ pub(super) fn symbol_list(s: &mut Session, _: &Value) -> Result<Value> {
/// current symbol.
pub(crate) fn store_symbol(d: &mut Document, art: &Node, name: &str, size: (f64, f64)) {
let art = d.reid(art);
d.symbols.push(Symbol { name: name.into(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
set_natural_size(d, name, Some(size));
d.unknown.insert(CURRENT_SYMBOL.into(), json!(name));
}
@@ -179,10 +181,14 @@ pub(super) fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
let reg = registration_param(p, C)?.unwrap_or(SymbolRegistration::CENTER);
let export = export_type_param(p, C)?.unwrap_or_default();
let nine = nine_slice_param(p, C, None)?.flatten();
let dynamic = type_param(p, C)?.unwrap_or(false);
// Keep the art where it was: the geometric centre stays at `c`, whatever the registration is.
let place = place_xf(c + reg.offset_from_center(size.0, size.1), size, reg);
let id = s.edit("New Symbol", |d, sel| {
store_symbol(d, &art, &name, size);
if let Some(sym) = d.symbols.iter_mut().find(|x| x.name == name) {
sym.dynamic = dynamic;
}
d.set_symbol_registration(&name, reg);
d.set_symbol_export_type(&name, export);
d.set_symbol_nine_slice(&name, nine);
@@ -190,9 +196,14 @@ pub(super) fn symbol_new(s: &mut Session, p: &Value) -> Result<Value> {
sel.set([id]);
Ok(id)
})?;
Ok(
json!({ "name": name, "id": id.0, "registration": reg.0, "exportType": export.id(), "nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value) }),
)
Ok(json!({
"name": name,
"id": id.0,
"registration": reg.0,
"exportType": export.id(),
"nineSlice": nine.map_or(Value::Null, SymbolNineSlice::to_value),
"type": type_id(dynamic),
}))
}
pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
@@ -201,9 +212,10 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
let export = export_type_param(p, C)?;
let reg = registration_param(p, C)?;
let nine = nine_slice_param(p, C, s.doc()?.doc.symbol_nine_slice(&name))?;
let dynamic = type_param(p, C)?;
let new_name = str_param(p, "newName").map(str::trim).filter(|n| !n.is_empty()).map(str::to_string);
if new_name.is_none() && export.is_none() && reg.is_none() && nine.is_none() {
return Err(bad(C, "pass newName, registration, exportType and/or nineSlice"));
if new_name.is_none() && export.is_none() && reg.is_none() && nine.is_none() && dynamic.is_none() {
return Err(bad(C, "pass newName, registration, exportType, nineSlice and/or type"));
}
if let Some(ref n) = new_name
&& n != &name
@@ -241,6 +253,13 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(g) = nine {
d.set_symbol_nine_slice(&final_name, g);
}
if let Some(dynamic) = dynamic
&& let Some(sym) = d.symbols.iter_mut().find(|x| x.name == final_name)
{
// A static symbol's instances drop their own appearance changes.
sym.dynamic = dynamic;
fit_overrides(d, &instances_of(d, Some(&final_name)));
}
Ok(())
})?;
let d = &s.doc()?.doc;
@@ -249,6 +268,7 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
"registration": d.symbol_registration(&final_name).0,
"exportType": d.symbol_export_type(&final_name).id(),
"nineSlice": nine_slice_json(d, &final_name),
"type": type_id(d.symbols.iter().any(|x| x.name == final_name && x.dynamic)),
}))
}
@@ -279,11 +299,12 @@ pub(super) fn symbol_place(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "id": id.0 }))
}
/// The plain art of instance `inst` in document space (fresh ids).
fn expanded_instance(d: &mut Document, inst: &Node) -> Option<Node> {
/// The plain art of instance `inst` in document space (fresh ids), with the instance's own
/// appearance changes to a dynamic symbol's art when `own`.
fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option<Node> {
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return None };
let sym = d.symbols.iter().find(|s| &s.name == symbol)?;
let mut art = (*sym.art).clone();
let mut art = if own { vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, inst) } else { (*sym.art).clone() };
if let Some(f) = inst.appearance.fill()
&& let Some(c) = f.paint.color()
{
@@ -302,11 +323,13 @@ fn expanded_instance(d: &mut Document, inst: &Node) -> Option<Node> {
Some(art)
}
fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {
/// Replace instances `ids` with their art ([`expanded_instance`], `own` as there). Returns the
/// art's ids.
fn break_links(d: &mut Document, ids: &[NodeId], own: bool) -> Result<Vec<NodeId>> {
let mut out = vec![];
for id in ids {
let Some(inst) = d.node(*id).cloned() else { continue };
let Some(art) = expanded_instance(d, &inst) else { continue };
let Some(art) = expanded_instance(d, &inst, own) else { continue };
let (par, idx, _) = d.position(*id).ok_or(EngineError::NoNode(*id))?;
let aid = art.id;
d.remove(*id)?;
@@ -316,7 +339,7 @@ fn break_links(d: &mut Document, ids: &[NodeId]) -> Result<Vec<NodeId>> {
Ok(out)
}
fn selected_instances(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
pub(super) fn selected_instances(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
let d = &s.doc()?.doc;
let mut out = vec![];
for id in roots(s, p)? {
@@ -337,7 +360,7 @@ pub(super) fn symbol_break_link(s: &mut Session, p: &Value) -> Result<Value> {
return Err(EngineError::Other("no symbol instances selected".into()));
}
let out = s.edit("Break Link to Symbol", |d, sel| {
let out = break_links(d, &ids)?;
let out = break_links(d, &ids, true)?;
sel.set(out.iter().copied());
Ok(out)
})?;
@@ -351,8 +374,9 @@ pub(super) fn symbol_edit(s: &mut Session, p: &Value) -> Result<Value> {
Some(NodeKind::SymbolInstance { symbol, .. }) => symbol.clone(),
_ => return Err(EngineError::Other("select a symbol instance".into())),
};
// The symbol's own art: one instance's appearance changes would become every instance's.
let out = s.edit("Edit Symbol", |d, sel| {
let out = break_links(d, &[first])?;
let out = break_links(d, &[first], false)?;
sel.set(out.iter().copied());
Ok(out)
})?;
@@ -377,11 +401,14 @@ pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
}
set_natural_size(d, &name, Some(size));
let fix = Affine::scale_non_uniform(size.0 / old.0, size.1 / old.1);
for i in instances_of(d, Some(&name)) {
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(i).map(|n| &mut n.kind) {
let instances = instances_of(d, Some(&name));
for i in &instances {
if let Some(NodeKind::SymbolInstance { xf, .. }) = d.node_mut(*i).map(|n| &mut n.kind) {
*xf *= fix;
}
}
// Each instance keeps its appearance changes to the objects the new art still has.
fit_overrides(d, &instances);
let reg = d.symbol_registration(&name);
let id = instance_in_place(d, &ids, &name, place_xf(c + reg.offset_from_center(size.0, size.1), size, reg))?;
sel.set([id]);
@@ -400,7 +427,7 @@ pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
s.edit("Delete Symbol", |d, _| {
let inst = instances_of(d, Some(&name));
if expand {
break_links(d, &inst)?;
break_links(d, &inst, true)?;
} else {
remove_each(d, &inst)?;
}
@@ -430,7 +457,7 @@ pub(super) fn symbol_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let nine = d.symbol_nine_slice(&name);
s.edit("Duplicate Symbol", |d, _| {
let art = d.reid(&sym.art);
d.symbols.push(Symbol { name: new.clone(), art: Arc::new(art) });
d.symbols.push(Symbol { dynamic: sym.dynamic, name: new.clone(), art: Arc::new(art) });
set_natural_size(d, &new, Some(size));
d.set_symbol_registration(&new, reg);
d.set_symbol_export_type(&new, export);
@@ -462,6 +489,7 @@ pub(super) fn symbol_replace(s: &mut Session, p: &Value) -> Result<Value> {
*xf *= Affine::scale_non_uniform(new.0 / old.0, new.1 / old.1);
}
}
fit_overrides(d, &ids);
Ok(())
})?;
Ok(json!({ "ids": ids_json(&ids) }))