mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 01:50:22 -06:00
Fix #4938: Dynamic symbols keep per-instance appearance linked to the symbol
This commit is contained in:
@@ -335,7 +335,7 @@ fn scatter_shapes() -> BrushLibrary {
|
||||
a.items.push(AppearanceItem::Fill(FillLayer::new(Paint::solid(gold))));
|
||||
a
|
||||
});
|
||||
doc.symbols.push(Symbol { name: "Sparkle".into(), art: Arc::new(sparkle) });
|
||||
doc.symbols.push(Symbol { dynamic: false, name: "Sparkle".into(), art: Arc::new(sparkle) });
|
||||
let instance = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Sparkle".into(), xf: Affine::scale(0.3) });
|
||||
let random = |a: f64, b: f64| (a, b);
|
||||
let base = Scatter { colorization: Colorization::Tints, ..Default::default() };
|
||||
|
||||
@@ -25,6 +25,7 @@ use super::{
|
||||
use crate::EngineError;
|
||||
|
||||
mod brushes;
|
||||
mod dynamic;
|
||||
mod symbols;
|
||||
|
||||
pub(crate) use brushes::{apply_brush, brush_targets, stroke_paint};
|
||||
@@ -32,6 +33,8 @@ 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,
|
||||
};
|
||||
use dynamic::{symbol_instance_reset, symbol_instance_set_appearance};
|
||||
use symbols::selected_instances;
|
||||
pub(crate) use symbols::{instance_in_place, natural_size, normalize, place_xf, set_natural_size, store_symbol};
|
||||
use symbols::{
|
||||
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_edit, symbol_list, symbol_new, symbol_options, symbol_place,
|
||||
@@ -157,13 +160,21 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
brush_freehand
|
||||
),
|
||||
// ----- symbols -----
|
||||
cmd!(query "symbol.list", "Symbols", [], None, "{} → {symbols: [{name, instances, size: [w,h], registration: 0..8, exportType: movieClip|graphic}], current}", has_doc, symbol_list),
|
||||
cmd!(
|
||||
query "symbol.list",
|
||||
"Symbols",
|
||||
[],
|
||||
None,
|
||||
"{} → {symbols: [{name, instances, size: [w,h], registration: 0..8, exportType: movieClip|graphic, nineSlice, type: static|dynamic}], current}",
|
||||
has_doc,
|
||||
symbol_list
|
||||
),
|
||||
cmd!(
|
||||
"symbol.new",
|
||||
"New Symbol…",
|
||||
[],
|
||||
None,
|
||||
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration), exportType?: movieClip (default)|graphic, nineSlice?: bool, nineSliceGuides?: array} make a symbol from the selection and replace it with an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds; nineSlice is Symbol Options › Enable Guides for 9-Slice Scaling, nineSliceGuides [left, top, right, bottom] as fractions 0..1 of the natural size (default a third each) → {name, id, registration, exportType, nineSlice}",
|
||||
"{name?, ids?|id?, registration?: 0..8 (9-point grid, 4 = centre; Symbol Options › Registration), exportType?: movieClip (default)|graphic, nineSlice?: bool, nineSliceGuides?: array, type?: static (default)|dynamic (Symbol Options › Symbol Type)} make a symbol from the selection and replace it with an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds; nineSlice is Symbol Options › Enable Guides for 9-Slice Scaling, nineSliceGuides [left, top, right, bottom] as fractions 0..1 of the natural size (default a third each) → {name, id, registration, exportType, nineSlice, type}",
|
||||
has_selection,
|
||||
symbol_new
|
||||
),
|
||||
@@ -172,7 +183,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Symbol Options…",
|
||||
[],
|
||||
None,
|
||||
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic, nineSlice?: bool, nineSliceGuides?: array} rename a symbol and/or set Symbol Options › Registration and Export Type and 9-slice guides (nineSliceGuides [left, top, right, bottom] fractions 0..1; default name: current) → {name, registration, exportType, nineSlice}",
|
||||
"{name?, newName?, registration?: 0..8, exportType?: movieClip|graphic, nineSlice?: bool, nineSliceGuides?: array, type?: static|dynamic} rename a symbol and/or set Symbol Options › Registration, Export Type, 9-slice guides (nineSliceGuides [left, top, right, bottom] fractions 0..1) and Symbol Type (a dynamic symbol's instances may change the appearance of objects in its art and stay linked; making it static drops those changes); default name: current → {name, registration, exportType, nineSlice, type}",
|
||||
has_doc,
|
||||
symbol_options
|
||||
),
|
||||
@@ -190,7 +201,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Break Link to Symbol",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?} expand selected instances into plain art → {ids}",
|
||||
"{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol) → {ids}",
|
||||
has_selection,
|
||||
symbol_break_link
|
||||
),
|
||||
@@ -199,7 +210,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Edit Symbol",
|
||||
[],
|
||||
None,
|
||||
"{id?, ids?: [id…] (instead of id)} break the link of the selected instance so its art can be edited; finish with symbol.update → {name, ids}",
|
||||
"{id?, ids?: [id…] (instead of id)} break the link of the selected instance so the symbol's art can be edited (without the instance's own appearance changes to a dynamic symbol); finish with symbol.update → {name, ids}",
|
||||
has_selection,
|
||||
symbol_edit
|
||||
),
|
||||
@@ -208,7 +219,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Redefine Symbol",
|
||||
[],
|
||||
None,
|
||||
"{name, ids?|id?} replace the symbol's art with the selection (instances update; the selection becomes an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds) → {name, id}",
|
||||
"{name, ids?|id?} replace the symbol's art with the selection (instances update, keeping their appearance changes to the objects at paths the new art still has; the selection becomes an instance; a frame of threaded text in the selection leaves its thread with the text it shows, which the symbol's art holds) → {name, id}",
|
||||
has_selection,
|
||||
symbol_update
|
||||
),
|
||||
@@ -227,10 +238,28 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Replace Symbol",
|
||||
[],
|
||||
None,
|
||||
"{name, ids?|id?} swap the symbol of the selected instances → {ids}",
|
||||
"{name, ids?|id?} swap the symbol of the selected instances (they keep their appearance changes to the objects at paths a dynamic new symbol's art has) → {ids}",
|
||||
has_selection,
|
||||
symbol_replace
|
||||
),
|
||||
cmd!(
|
||||
"symbol.instance.setAppearance",
|
||||
"Symbol Instance Appearance",
|
||||
[],
|
||||
None,
|
||||
"{id?|ids?: [one id] (default: the one selected object), path: [i…] (the object in the symbol's art: child indices in paint order from the art's root down through its groups; [] the root), fill?: \"#rrggbb\"|{…paint params of paint.setFill: color, none, swatch, tint, gradient, keepModel}|null (null: the symbol's own again), stroke?: as fill, opacity?: 0..100|null, blend?: name|null} change the appearance of one object of a dynamic symbol instance's art, for that instance only: it stays linked, so editing the symbol reshapes it and keeps the change; refused for a static symbol or a path naming no object → {id, overrides: how many objects the instance changes}",
|
||||
has_selection,
|
||||
symbol_instance_set_appearance
|
||||
),
|
||||
cmd!(
|
||||
"symbol.instance.reset",
|
||||
"Reset Symbol Instance",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?} drop the selected dynamic symbol instances' own appearance changes, so they show the symbol's appearance again → {ids: the instances that had changes}",
|
||||
has_selection,
|
||||
symbol_instance_reset
|
||||
),
|
||||
cmd!(
|
||||
"symbol.setCurrent",
|
||||
"Current Symbol",
|
||||
|
||||
@@ -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) }))
|
||||
|
||||
@@ -682,10 +682,12 @@ fn expanded_node(d: &mut Document, mut n: Node, brushes: &[vectorcraft_brush::Br
|
||||
// Type, images, symbol instances, live objects (and paths whose Transform effect copies
|
||||
// them with a gradient or changes their fills and strokes): through their outlines.
|
||||
let symbol = match &n.kind {
|
||||
NodeKind::SymbolInstance { symbol, .. } => d.symbols.iter().find(|s| s.name == *symbol).map(|s| s.art.clone()),
|
||||
NodeKind::SymbolInstance { symbol, .. } => {
|
||||
d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, &n))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
effects::expand_outlined(Some(&*d), &v, symbol.as_deref()).map(|mut m| {
|
||||
effects::expand_outlined(Some(&*d), &v, symbol.as_ref()).map(|mut m| {
|
||||
expand_pieces(d, &mut m, true, brushes, &mut stroke_art);
|
||||
m
|
||||
})
|
||||
|
||||
@@ -181,7 +181,7 @@ fn basic_shapes() -> SymbolLibrary {
|
||||
let id = doc.alloc_id();
|
||||
let art = Node::path(id, path, Appearance::basic(Paint::solid(fill), Paint::solid(edge), 1.0));
|
||||
let Ok((art, size, _)) = normalize(art) else { continue };
|
||||
doc.symbols.push(Symbol { name: name.into(), art: Arc::new(art) });
|
||||
doc.symbols.push(Symbol { dynamic: false, name: name.into(), art: Arc::new(art) });
|
||||
set_natural_size(&mut doc, name, Some(size));
|
||||
}
|
||||
doc.title = "Basic Shapes".into();
|
||||
|
||||
Reference in New Issue
Block a user