mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 23:31:23 +00:00
The appearance changes of the instances on the layers now follow the edited objects through the same pass as the instances in other symbols, graph designs, pattern tiles and brushes. That pass also reaches instances used as opacity-mask art, which keep their changes on the same objects when an edit reorders them and drop the change to an object the edit deletes.
526 lines
24 KiB
Rust
526 lines
24 KiB
Rust
//! Symbol editing mode (Edit Symbol, a double-click on an instance or on a symbol in the Symbols
|
|
//! panel).
|
|
//!
|
|
//! The symbol's art moves onto a temporary isolated layer, placed where the instance it was
|
|
//! entered from shows it (from the panel: at the center of the active artboard, or of the view in a
|
|
//! document without artboards), where every tool and command can edit it while the rest of the
|
|
//! document is dimmed. After each edit [`sync`] writes the layer's art back into the symbol, so
|
|
//! every instance updates live. Leaving the mode
|
|
//! (`symbol.exitEditing`, Escape, the isolation bar's back button, a double-click outside the art)
|
|
//! removes the layer, and saving or exporting never writes it ([`Document::without_edit_modes`]).
|
|
//! Symbol editing is not entered while a pattern or an opacity mask is edited, and neither of those
|
|
//! is entered while a symbol is edited.
|
|
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
|
|
use serde_json::{Value, json};
|
|
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::{EngineError, Result, Session, bad, id_param, number_or, ok, str_param};
|
|
|
|
/// The name of the undo step that enters symbol editing mode.
|
|
const ENTER: &str = "Edit Symbol";
|
|
|
|
/// The symbol instances among the selected objects, members of selected groups included.
|
|
fn selected_instances(st: &crate::DocState) -> Vec<NodeId> {
|
|
let mut out = vec![];
|
|
for id in &st.selection.objects {
|
|
if let Some(n) = st.doc.node(*id) {
|
|
n.walk(&mut |c| {
|
|
if matches!(c.kind, NodeKind::SymbolInstance { .. }) && !out.contains(&c.id) {
|
|
out.push(c.id);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The symbol and transform of instance `id`.
|
|
fn instance_of(d: &Document, id: NodeId) -> Option<(String, Affine)> {
|
|
match &d.node(id)?.kind {
|
|
NodeKind::SymbolInstance { symbol, xf } => Some((symbol.clone(), *xf)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Where editing places the art: the instance's own transform, or for one that flattens the art,
|
|
/// a placement at the symbol's natural size around the same point.
|
|
fn editing_xf(d: &Document, name: &str, xf: Affine) -> Affine {
|
|
let det = xf.determinant();
|
|
if det.is_finite() && det.abs() > 1e-12 { xf } else { place_xf(xf * Point::ZERO, natural_size(d, name), SymbolRegistration::CENTER) }
|
|
}
|
|
|
|
/// Enter symbol editing mode (`symbol.edit`).
|
|
pub(crate) fn enter(s: &mut Session, p: &Value) -> Result<Value> {
|
|
const C: &str = "symbol.edit";
|
|
let st = s.doc()?;
|
|
if st.doc.mask_edit.is_some() {
|
|
return Err(bad(C, "stop editing the opacity mask first"));
|
|
}
|
|
if st.doc.pattern_edit.is_some() {
|
|
return Err(bad(C, "finish editing the pattern first"));
|
|
}
|
|
let given = id_param(p, "id").map(|i| vec![i]).or_else(|| super::ids_param(p, "ids").filter(|v| !v.is_empty()));
|
|
let (name, xf, instance) = match given {
|
|
Some(ids) => {
|
|
let symbols = ids.iter().map(|id| instance_of(&st.doc, *id)).collect::<Option<Vec<_>>>();
|
|
let symbols = symbols.ok_or_else(|| bad(C, "select a symbol instance"))?;
|
|
let (name, xf) = symbols.first().cloned().ok_or_else(|| bad(C, "select a symbol instance"))?;
|
|
if symbols.iter().any(|(n, _)| *n != name) {
|
|
return Err(bad(C, "select instances of one symbol"));
|
|
}
|
|
let xf = editing_xf(&st.doc, &name, xf);
|
|
(name, xf, ids.first().copied())
|
|
}
|
|
None => {
|
|
let picked = if str_param(p, "name").is_some() { vec![] } else { selected_instances(st) };
|
|
let symbols: Vec<(String, Affine)> = picked.iter().filter_map(|id| instance_of(&st.doc, *id)).collect();
|
|
match symbols.first() {
|
|
Some((first, xf)) => {
|
|
if symbols.iter().any(|(n, _)| n != first) {
|
|
return Err(bad(C, "select instances of one symbol"));
|
|
}
|
|
(first.clone(), editing_xf(&st.doc, first, *xf), picked.first().copied())
|
|
}
|
|
None => {
|
|
let d = &st.doc;
|
|
let name = match str_param(p, "name") {
|
|
Some(n) if d.symbols.iter().any(|x| x.name == n) => n.to_string(),
|
|
Some(n) => return Err(bad(C, format!("no symbol named `{n}`"))),
|
|
None => d
|
|
.unknown
|
|
.get(super::brushsym::CURRENT_SYMBOL)
|
|
.and_then(Value::as_str)
|
|
.filter(|n| d.symbols.iter().any(|x| x.name == *n))
|
|
.or_else(|| d.symbols.first().map(|x| x.name.as_str()))
|
|
.map(str::to_string)
|
|
.ok_or_else(|| EngineError::Other("the document has no symbols".into()))?,
|
|
};
|
|
let artboard = p.get("artboard").and_then(Value::as_u64).map_or(0, super::index);
|
|
let center = match d.artboards.get(artboard).or(d.artboards.first()) {
|
|
Some(a) => a.rect.center(),
|
|
None => super::point_param(p, "viewCenter").unwrap_or(Point::ZERO),
|
|
};
|
|
let at = Point::new(number_or(p, "x", center.x, C)?, number_or(p, "y", center.y, C)?);
|
|
(name.clone(), place_xf(at, natural_size(d, &name), SymbolRegistration::CENTER), None)
|
|
}
|
|
}
|
|
}
|
|
};
|
|
// Editing another symbol (one from the Symbols panel, or through the API): the current edit
|
|
// ends first, once the new placement is known to be valid, so a refused switch keeps it open.
|
|
if st.doc.symbol_edit.is_some() {
|
|
if instance.is_some_and(|i| st.doc.symbol_edit.as_ref().is_some_and(|e| st.doc.ancestry(i).unwrap_or_default().contains(&e.layer))) {
|
|
return Err(bad(C, "exit isolation mode first"));
|
|
}
|
|
let mut trial = Document::clone(&st.doc);
|
|
let mut sel = Selection::default();
|
|
begin(&mut trial, &mut sel, &name, xf, instance)?;
|
|
if trial.too_deep(Some(&st.doc)) {
|
|
return Err(EngineError::Other(crate::TOO_DEEP.into()));
|
|
}
|
|
if !crate::sanity::doc_sane(&trial, &sel) {
|
|
return Err(EngineError::Other("result would exceed the canvas (coordinates out of range)".into()));
|
|
}
|
|
leave(s, &json!({}))?;
|
|
}
|
|
if let Some(i) = s.doc()?.isolation {
|
|
super::settle_interaction(s)?;
|
|
super::distortcmds::finish_edit_text(s.doc_mut()?, i);
|
|
}
|
|
s.end_text_edit();
|
|
let st = s.doc()?;
|
|
let back = Return { redo: st.history.redo.clone(), before: Some(st.doc.clone()) };
|
|
let (isolation, active_layer) = (st.isolation, st.active_layer);
|
|
let layer = s.edit(ENTER, |d, sel| {
|
|
let layer = begin(d, sel, &name, xf, instance)?;
|
|
if let Some(e) = d.symbol_edit.as_mut() {
|
|
e.back_isolation = isolation;
|
|
e.back_layer = active_layer;
|
|
}
|
|
Ok(layer)
|
|
})?;
|
|
s.doc_mut()?.symbol_edit_return = back;
|
|
show_editing(s, layer)?;
|
|
let mut out = json!({ "name": name, "layer": layer.0 });
|
|
if let Some(i) = instance {
|
|
out["id"] = json!(i.0);
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// A group whose members editing spreads out on the layer: nothing of its own that they would lose
|
|
/// while they are edited.
|
|
fn spreads(art: &Node) -> bool {
|
|
matches!(art.kind, NodeKind::Group { clip: false, .. })
|
|
&& art.has_default_transparency()
|
|
&& art.appearance.items.is_empty()
|
|
&& art.appearance.effects.is_empty()
|
|
}
|
|
|
|
/// Enter symbol editing for `name` inside an edit: copy its art, placed by `xf`, onto a new
|
|
/// editing layer and select it. Returns the layer.
|
|
fn begin(d: &mut Document, sel: &mut Selection, name: &str, xf: Affine, instance: Option<NodeId>) -> Result<NodeId> {
|
|
let art =
|
|
d.symbols.iter().find(|x| x.name == name).map(|x| x.art.clone()).ok_or_else(|| EngineError::Other(format!("no symbol named `{name}`")))?;
|
|
let group = spreads(&art);
|
|
let parts = match art.children() {
|
|
Some(ch) if group => ch.clone(),
|
|
_ => vec![art],
|
|
};
|
|
let layer = d.add_layer(Some(name));
|
|
let mut ids = Vec::with_capacity(parts.len());
|
|
for part in &parts {
|
|
let mut copy = d.reid(part);
|
|
copy.transform(xf, false);
|
|
ids.push(d.insert(Some(layer), usize::MAX, copy)?);
|
|
}
|
|
// Copied type whose layout caches `reid` dropped has them again.
|
|
super::typecmd::refresh_bounds_of(d, &ids);
|
|
d.symbol_edit = Some(SymbolEdit { symbol: name.to_string(), layer, instance, xf, group, changed: false, back_isolation: None, back_layer: None });
|
|
sel.set(ids);
|
|
Ok(layer)
|
|
}
|
|
|
|
/// Leave symbol editing mode (`symbol.exitEditing`); a no-op outside it. `object.exitIsolation` runs
|
|
/// it too while a symbol is edited. Isolation and the current layer go back to what they were
|
|
/// before. When nothing changed the art since editing was entered, the entering step is taken back
|
|
/// instead of a step being added, so the document and its redo steps are as they were.
|
|
pub(crate) fn leave(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let Some(se) = s.doc()?.doc.symbol_edit.clone() else { return ok() };
|
|
super::settle_interaction(s)?;
|
|
super::distortcmds::finish_edit_text(s.doc_mut()?, se.layer);
|
|
s.end_text_edit();
|
|
let st = s.doc_mut()?;
|
|
let unchanged = !st.doc.symbol_edit.as_ref().is_some_and(|e| e.changed);
|
|
let Return { redo, before } = std::mem::take(&mut st.symbol_edit_return);
|
|
// Saved while editing: the saved file holds no editing layer, so leaving keeps it saved.
|
|
let saved_here = st.saved_doc.symbol_edit.as_ref().is_some_and(|e| e.layer == se.layer);
|
|
let clean = !st.is_dirty();
|
|
// Not after edits that were undone (their redo steps hold the editing layer), nor after a save
|
|
// (taking the entering step back would undo what the save changed). The last step is the
|
|
// entering step only when it starts from the document entering started from: a batch step
|
|
// that entered after other edits starts from before them.
|
|
if unchanged
|
|
&& !saved_here
|
|
&& st.history.redo.is_empty()
|
|
&& let Some(entry) = st.history.undo.last()
|
|
&& before.as_ref().is_some_and(|b| Arc::ptr_eq(b, &entry.doc))
|
|
&& let Some(entry) = st.history.undo.pop()
|
|
{
|
|
let now = std::mem::replace(&mut st.doc, entry.doc);
|
|
// What changed outside the undo history while editing (the current symbol and brush, the
|
|
// perspective grid's view, export settings, the grid preferences) stays as it is now.
|
|
if st.doc.unknown != now.unknown || st.doc.export_settings != now.export_settings || st.doc.grid != now.grid {
|
|
let d = Arc::make_mut(&mut st.doc);
|
|
d.unknown = now.unknown.clone();
|
|
d.export_settings = now.export_settings.clone();
|
|
d.grid = now.grid.clone();
|
|
}
|
|
st.selection = Arc::unwrap_or_clone(entry.selection);
|
|
st.selection.prune(&st.doc);
|
|
st.history.redo = redo;
|
|
st.revision += 1;
|
|
} else {
|
|
s.edit("Exit Symbol Editing Mode", |d, sel| {
|
|
finish(d, sel);
|
|
Ok(())
|
|
})?;
|
|
if saved_here && clean {
|
|
s.doc_mut()?.mark_saved();
|
|
}
|
|
}
|
|
leave_editing(s, &se)?;
|
|
Ok(json!({ "name": se.symbol }))
|
|
}
|
|
|
|
/// Leave symbol editing inside an edit (no-op when not editing): drop the editing layer and select
|
|
/// the instance editing was entered from. The symbol already holds the layer's art ([`sync`]).
|
|
pub(crate) fn finish(d: &mut Document, sel: &mut Selection) {
|
|
let Some(se) = d.symbol_edit.take() else { return };
|
|
let _ = d.remove(se.layer);
|
|
match se.instance.filter(|i| d.node(*i).is_some()) {
|
|
Some(i) => sel.set([i]),
|
|
None => sel.clear(),
|
|
}
|
|
}
|
|
|
|
/// The redo steps entering symbol editing mode cleared, which leaving without an edit gives back,
|
|
/// and the document entering started from, which tells the entering step apart from any other.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(crate) struct Return {
|
|
redo: Vec<crate::HistoryEntry>,
|
|
before: Option<Arc<Document>>,
|
|
}
|
|
|
|
/// Isolate the editing layer `layer` and draw into it.
|
|
fn show_editing(s: &mut Session, layer: NodeId) -> Result<()> {
|
|
let st = s.doc_mut()?;
|
|
st.isolation = Some(layer);
|
|
st.active_layer = Some(layer);
|
|
st.revision += 1;
|
|
Ok(())
|
|
}
|
|
|
|
/// Return to the document after leaving the editing session `se`: to the isolation and current
|
|
/// layer from before it, while they are still there.
|
|
pub(crate) fn leave_editing(s: &mut Session, se: &SymbolEdit) -> Result<()> {
|
|
let st = s.doc_mut()?;
|
|
st.symbol_edit_return = Return::default();
|
|
go_back(st, se);
|
|
st.revision += 1;
|
|
Ok(())
|
|
}
|
|
|
|
/// Out of the editing session `se`: the isolation and current layer from before it, else none and
|
|
/// the default layer.
|
|
pub(crate) fn go_back(st: &mut crate::DocState, se: &SymbolEdit) {
|
|
st.isolation = se.back_isolation.filter(|i| st.doc.node(*i).is_some());
|
|
st.active_layer = se.back_layer.filter(|l| st.doc.node(*l).is_some_and(|n| n.is_layer())).or_else(|| st.doc.default_layer());
|
|
}
|
|
|
|
/// After undo or redo, isolation follows the restored document into or out of symbol editing
|
|
/// (`was` is the editing session before, if any), so it never names a layer that is gone. A group
|
|
/// isolated inside the symbol's art stays isolated while it is there.
|
|
pub(crate) fn follow_history(st: &mut crate::DocState, was: Option<&SymbolEdit>) {
|
|
match st.doc.symbol_edit.as_ref().map(|e| e.layer) {
|
|
Some(layer) => {
|
|
let inside = st.isolation.is_some_and(|i| i != layer && st.doc.ancestry(i).is_some_and(|a| a.contains(&layer)));
|
|
if !inside {
|
|
st.isolation = Some(layer);
|
|
}
|
|
st.active_layer = Some(layer);
|
|
}
|
|
None => {
|
|
// Out of the mode: isolation on the editing layer or inside it, and a current layer
|
|
// that is gone, go back to what they were before it.
|
|
let Some(was) = was else { return };
|
|
let gone = |id: Option<NodeId>| id.is_some_and(|i| i == was.layer || st.doc.node(i).is_none());
|
|
if gone(st.isolation) || gone(st.active_layer) {
|
|
go_back(st, was);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Does `n` hold an instance of symbol `name`, directly or through the art of the symbols it holds
|
|
/// instances of?
|
|
fn holds_symbol(d: &Document, n: &Node, name: &str, seen: &mut HashSet<String>) -> bool {
|
|
let mut found = false;
|
|
let mut nested = vec![];
|
|
n.walk(&mut |c| {
|
|
if let NodeKind::SymbolInstance { symbol, .. } = &c.kind {
|
|
if symbol == name {
|
|
found = true;
|
|
} else if !seen.contains(symbol) {
|
|
nested.push(symbol.clone());
|
|
}
|
|
}
|
|
});
|
|
if found {
|
|
return true;
|
|
}
|
|
for symbol in nested {
|
|
if seen.insert(symbol.clone())
|
|
&& let Some(art) = d.symbols.iter().find(|x| x.name == symbol).map(|x| x.art.clone())
|
|
&& holds_symbol(d, &art, name, seen)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// The object at `path` (child indices, paint order) among `nodes` and the groups they hold.
|
|
fn at_path<'a>(nodes: &'a [Arc<Node>], path: &[u32]) -> Option<&'a Node> {
|
|
let (first, rest) = path.split_first()?;
|
|
let mut n: &Node = nodes.get(*first as usize)?;
|
|
for i in rest {
|
|
n = n.children()?.get(*i as usize)?;
|
|
}
|
|
Some(n)
|
|
}
|
|
|
|
/// The path of object `id` among `nodes` and the groups they hold, at most `depth` levels down.
|
|
fn path_of(nodes: &[Arc<Node>], id: NodeId, depth: usize) -> Option<Vec<u32>> {
|
|
if depth == 0 {
|
|
return None;
|
|
}
|
|
for (i, n) in nodes.iter().enumerate() {
|
|
let i = u32::try_from(i).ok()?;
|
|
if n.id == id {
|
|
return Some(vec![i]);
|
|
}
|
|
if let Some(mut p) = n.children().and_then(|ch| path_of(ch, id, depth - 1)) {
|
|
p.insert(0, i);
|
|
return Some(p);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Appearance changes to objects of a symbol's art follow those objects through an edit of the
|
|
/// editing layer: `old` and `new` are the layer's objects before and after with whether the
|
|
/// symbol's art is a group of them (else it is the one object). A change to an object the edit
|
|
/// removed is dropped.
|
|
fn follow_paths(overrides: &mut Vec<SymbolOverride>, old: (&[Arc<Node>], bool), new: (&[Arc<Node>], bool)) {
|
|
let depth = vectorcraft_doc::symbol_overrides::MAX_PATH_DEPTH + 1;
|
|
overrides.retain_mut(|o| {
|
|
// The art's own group stays the art's root.
|
|
if old.1 && o.path.is_empty() {
|
|
return true;
|
|
}
|
|
let on_layer: Vec<u32> = if old.1 { o.path.clone() } else { std::iter::once(0).chain(o.path.iter().copied()).collect() };
|
|
let moved = at_path(old.0, &on_layer).and_then(|obj| path_of(new.0, obj.id, depth)).and_then(|p| match new.1 {
|
|
true => Some(p),
|
|
false => p.split_first().filter(|(first, _)| **first == 0).map(|(_, rest)| rest.to_vec()),
|
|
});
|
|
match moved {
|
|
Some(p) => {
|
|
o.path = p;
|
|
true
|
|
}
|
|
None => false,
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Whether `n` or its subtree (opacity masks included) holds an instance of symbol `name` with
|
|
/// appearance changes.
|
|
fn holds_changed_instance(n: &Node, name: &str) -> bool {
|
|
let mut found = false;
|
|
vectorcraft_doc::swatches::walk_with_masks(n, &mut |m| {
|
|
found |= matches!(&m.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == name) && !m.extras().symbol_overrides.is_empty();
|
|
});
|
|
found
|
|
}
|
|
|
|
/// Apply `f` to the appearance changes of each instance of symbol `name` in `n` and its subtree
|
|
/// (opacity masks included), copying only the shared nodes on the way to one.
|
|
fn each_changed_instance(n: &mut Node, name: &str, f: &mut dyn FnMut(&mut Vec<SymbolOverride>)) {
|
|
if matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == name) && !n.extras().symbol_overrides.is_empty() {
|
|
f(&mut n.extras_mut().symbol_overrides);
|
|
}
|
|
if let Some(mask) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut())
|
|
&& holds_changed_instance(&mask.art, name)
|
|
{
|
|
each_changed_instance(Arc::make_mut(&mut mask.art), name, f);
|
|
}
|
|
for c in n.children_mut().into_iter().flatten() {
|
|
if holds_changed_instance(c, name) {
|
|
each_changed_instance(Arc::make_mut(c), name, f);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
/// 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.
|
|
fn follow_objects(d: &mut Document, name: &str, old: (&[Arc<Node>], bool), new: (&[Arc<Node>], bool)) {
|
|
let art = d.symbols.iter().find(|s| s.name == name).filter(|s| s.dynamic).map(|s| s.art.clone());
|
|
let mut follow = |o: &mut Vec<SymbolOverride>| {
|
|
follow_paths(o, old, new);
|
|
match &art {
|
|
Some(art) => vectorcraft_doc::symbol_overrides::retain_parts(o, art),
|
|
None => o.clear(),
|
|
}
|
|
};
|
|
let symbols = d.symbols.iter_mut().filter(|s| s.name != name).map(|s| &mut s.art);
|
|
let trees = d.layers.iter_mut().chain(symbols).chain(d.graph_designs.iter_mut().map(|g| &mut g.art));
|
|
let patterns = d.patterns.iter_mut().chain(d.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut()));
|
|
for root in trees.chain(patterns.flat_map(|p| &mut p.art)) {
|
|
if holds_changed_instance(root, name) {
|
|
each_changed_instance(Arc::make_mut(root), name, &mut follow);
|
|
}
|
|
}
|
|
// The default brushes hold no instances, so only a stored library can change. A stored entry
|
|
// whose art holds no changed instance, or that does not parse, is stored again as it was read;
|
|
// a changed one is merged into its entry, which keeps the fields the brush definition does not read.
|
|
let Some(stored) = d.unknown.get(vectorcraft_brush::DOC_KEY).and_then(Value::as_array).cloned() else { return };
|
|
let mut changed = false;
|
|
let lib: Vec<Value> = stored
|
|
.into_iter()
|
|
.map(|raw| {
|
|
let Ok(mut b) = vectorcraft_brush::Brush::from_json(raw.clone()) else { return raw };
|
|
if !b.art().into_iter().any(|n| holds_changed_instance(n, name)) {
|
|
return raw;
|
|
}
|
|
let base = serde_json::to_value(&b).unwrap_or_default();
|
|
for art in b.art_mut() {
|
|
if holds_changed_instance(art, name) {
|
|
each_changed_instance(art, name, &mut follow);
|
|
}
|
|
}
|
|
changed = true;
|
|
super::swatch::merge_stored(raw, &base, serde_json::to_value(&b).unwrap_or_default())
|
|
})
|
|
.collect();
|
|
if changed {
|
|
d.unknown.insert(vectorcraft_brush::DOC_KEY.into(), Value::Array(lib));
|
|
}
|
|
}
|
|
|
|
/// Write the editing layer's art back into the symbol (called after every edit, with the document
|
|
/// as it was before); each instance of a dynamic symbol keeps its appearance changes to the objects
|
|
/// the art still has. Leaves the mode when the symbol or the layer is gone. Refuses art that holds
|
|
/// an instance of the symbol itself.
|
|
pub(crate) fn sync(before: &Document, d: &mut Document) -> Result<()> {
|
|
let Some(se) = d.symbol_edit.clone() else { return Ok(()) };
|
|
let art: Option<Vec<Arc<Node>>> = d.node(se.layer).and_then(|l| l.children().cloned());
|
|
let (Some(art), Some(index)) = (art, d.symbols.iter().position(|x| x.name == se.symbol)) else {
|
|
d.symbol_edit = None;
|
|
let _ = d.remove(se.layer);
|
|
return Ok(());
|
|
};
|
|
// Entering, and edits that left the layer's art alone, write nothing.
|
|
let same = |b: &[Arc<Node>]| b.len() == art.len() && b.iter().zip(&art).all(|(a, b)| Arc::ptr_eq(a, b));
|
|
let (old, old_group) = match before.symbol_edit.as_ref().filter(|b| b.layer == se.layer) {
|
|
None => return Ok(()),
|
|
Some(_) if before.node(se.layer).and_then(|l| l.children()).is_some_and(|b| same(b)) => return Ok(()),
|
|
Some(b) => (before.node(se.layer).and_then(|l| l.children().cloned()).unwrap_or_default(), b.group),
|
|
};
|
|
if art.iter().any(|n| holds_symbol(d, n, &se.symbol, &mut HashSet::new())) {
|
|
return Err(EngineError::Other("a symbol can't hold an instance of itself".into()));
|
|
}
|
|
let inv = se.xf.inverse();
|
|
let parts: Vec<Arc<Node>> = art
|
|
.iter()
|
|
.map(|n| {
|
|
let mut n = (**n).clone();
|
|
n.transform(inv, false);
|
|
Arc::new(n)
|
|
})
|
|
.collect();
|
|
// Once the layer's objects make a group, later edits give that group its members.
|
|
let (new_art, group) = if se.group {
|
|
let mut g = (*d.symbols[index].art).clone();
|
|
if let Some(ch) = g.children_mut() {
|
|
*ch = parts;
|
|
}
|
|
(g, true)
|
|
} else {
|
|
match <[Arc<Node>; 1]>::try_from(parts) {
|
|
Ok([one]) => (Arc::unwrap_or_clone(one), false),
|
|
Err(parts) => {
|
|
let id = d.alloc_id();
|
|
(Node::group(id, parts), true)
|
|
}
|
|
}
|
|
};
|
|
d.symbols[index].art = Arc::new(new_art);
|
|
follow_objects(d, &se.symbol, (&old, old_group), (&art, group));
|
|
if let Some(e) = d.symbol_edit.as_mut() {
|
|
e.changed = true;
|
|
e.group = group;
|
|
}
|
|
Ok(())
|
|
}
|