mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol
This commit is contained in:
@@ -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) }))
|
||||
}
|
||||
@@ -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) }))
|
||||
|
||||
Reference in New Issue
Block a user