mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
Symbols: Edit Symbol opens the symbol in its own editing mode, and every instance follows each edit
Squashed from #5064 onto main.
This commit is contained in:
@@ -35,10 +35,10 @@ use brushes::{
|
||||
};
|
||||
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};
|
||||
pub(crate) use symbols::{fit_instances_of, instance_in_place, instances_of, 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,
|
||||
symbol_replace, symbol_select_instances, symbol_set_current, symbol_spray, symbol_unused, symbol_update,
|
||||
symbol_adjust, symbol_break_link, symbol_delete, symbol_duplicate, symbol_list, symbol_new, symbol_options, symbol_place, symbol_replace,
|
||||
symbol_select_instances, symbol_set_current, symbol_spray, symbol_unused, symbol_update,
|
||||
};
|
||||
|
||||
const SIZES_KEY: &str = vectorcraft_doc::SYMBOL_SIZES;
|
||||
@@ -210,9 +210,18 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Edit Symbol",
|
||||
[],
|
||||
None,
|
||||
"{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
|
||||
"{id?, ids?: [id…] (instead of id), name?, x?, y?, artboard?: index=0 (the active artboard; the app passes it), viewCenter?: [x, y] (the app passes the view's center)} edit a symbol's art in symbol editing mode: the instance `id` (default: the selected instances, which must share one symbol) where it shows the art, else the symbol `name` (default: the current symbol) centered on (x, y) (default: the center of artboard `artboard`, or of the first artboard when there is no such artboard; in a document without artboards, viewCenter, else (0, 0)); every instance follows each edit and the instance stays linked (symbol.breakLink expands one); refused while a pattern or an opacity mask is edited → {name, layer, id?}",
|
||||
has_doc,
|
||||
super::symboledit::enter
|
||||
),
|
||||
cmd!(
|
||||
"symbol.exitEditing",
|
||||
"Exit Symbol Editing Mode",
|
||||
[],
|
||||
None,
|
||||
"{} leave symbol editing mode; after an edit, select the instance it was entered from; leaving without having changed the art adds no undo step and restores the selection from before entry (object.exitIsolation also leaves it) → {name?}",
|
||||
has_doc,
|
||||
super::symboledit::leave
|
||||
),
|
||||
cmd!(
|
||||
"symbol.update",
|
||||
|
||||
@@ -95,7 +95,14 @@ pub(crate) fn normalize(mut art: Node) -> Result<(Node, (f64, f64), Point)> {
|
||||
Ok((art, (w, h), c))
|
||||
}
|
||||
|
||||
fn instances_of(d: &Document, name: Option<&str>) -> Vec<NodeId> {
|
||||
/// After the art of symbol `name` changed: each of its instances keeps its appearance changes to
|
||||
/// the objects the art still has.
|
||||
pub(crate) fn fit_instances_of(d: &mut Document, name: &str) {
|
||||
let ids = instances_of(d, Some(name));
|
||||
fit_overrides(d, &ids);
|
||||
}
|
||||
|
||||
pub(crate) fn instances_of(d: &Document, name: Option<&str>) -> Vec<NodeId> {
|
||||
let mut v = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
|
||||
@@ -274,6 +281,14 @@ pub(super) fn symbol_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if d.unknown.get(CURRENT_SYMBOL).and_then(Value::as_str) == Some(name.as_str()) {
|
||||
d.unknown.insert(CURRENT_SYMBOL.into(), json!(n));
|
||||
}
|
||||
// Symbol editing mode follows the new name, and so does its layer.
|
||||
if let Some(e) = d.symbol_edit.as_mut().filter(|e| e.symbol == name) {
|
||||
e.symbol = n.clone();
|
||||
let layer = e.layer;
|
||||
if let Some(l) = d.node_mut(layer) {
|
||||
l.name = Some(n.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(r) = reg {
|
||||
d.set_symbol_registration(&final_name, r);
|
||||
@@ -398,34 +413,35 @@ pub(super) fn symbol_break_link(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "ids": ids_json(&out) }))
|
||||
}
|
||||
|
||||
pub(super) fn symbol_edit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = selected_instances(s, p)?;
|
||||
let first = *ids.first().ok_or_else(|| EngineError::Other("select a symbol instance".into()))?;
|
||||
let name = match &s.doc()?.doc.node(first).map(|n| n.kind.clone()) {
|
||||
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], false)?;
|
||||
sel.set(out.iter().copied());
|
||||
Ok(out)
|
||||
})?;
|
||||
Ok(json!({ "name": name, "ids": ids_json(&out) }))
|
||||
}
|
||||
|
||||
pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "symbol.update";
|
||||
let name = name_param(p, C)?.to_string();
|
||||
if !s.doc()?.doc.symbols.iter().any(|x| x.name == name) {
|
||||
return Err(bad(C, format!("no symbol named `{name}`")));
|
||||
}
|
||||
// Redefining the symbol being edited leaves symbol editing mode in the same step.
|
||||
let session = s.doc()?.doc.symbol_edit.clone().filter(|e| e.symbol == name);
|
||||
if session.is_some() {
|
||||
crate::cmd::settle_interaction(s)?;
|
||||
s.end_text_edit();
|
||||
}
|
||||
let ids = roots(s, p)?;
|
||||
// The art on the editing layer can't redefine it: the symbol would hold an instance of itself.
|
||||
if let Some(se) = &session {
|
||||
let d = &s.doc()?.doc;
|
||||
if ids.iter().any(|i| d.ancestry(*i).is_some_and(|a| a.contains(&se.layer))) {
|
||||
return Err(EngineError::Other("a symbol can't hold an instance of itself".into()));
|
||||
}
|
||||
}
|
||||
let mut art = selection_art(s, p)?.ok_or_else(|| EngineError::Other("nothing selected".into()))?;
|
||||
crate::cmd::threads::keep_shown_text(&s.doc()?.doc, &mut art);
|
||||
let (art, size, c) = normalize(art)?;
|
||||
let old = natural_size(&s.doc()?.doc, &name);
|
||||
let editing = session.is_some();
|
||||
let id = s.edit("Redefine Symbol", |d, sel| {
|
||||
if editing {
|
||||
crate::cmd::symboledit::finish(d, sel);
|
||||
}
|
||||
let art = d.reid(&art);
|
||||
if let Some(sym) = d.symbols.iter_mut().find(|x| x.name == name) {
|
||||
sym.art = Arc::new(art);
|
||||
@@ -445,6 +461,9 @@ pub(super) fn symbol_update(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
if let Some(se) = &session {
|
||||
crate::cmd::symboledit::leave_editing(s, se)?;
|
||||
}
|
||||
Ok(json!({ "name": name, "id": id.0 }))
|
||||
}
|
||||
|
||||
@@ -476,8 +495,18 @@ pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, format!("no symbol named `{name}`")));
|
||||
}
|
||||
let expand = switch_or(p, "expandInstances", true, C)?;
|
||||
// Deleting the symbol being edited leaves symbol editing mode in the same step.
|
||||
let session = s.doc()?.doc.symbol_edit.clone().filter(|e| names.contains(&e.symbol));
|
||||
let editing = session.is_some();
|
||||
if editing {
|
||||
crate::cmd::settle_interaction(s)?;
|
||||
s.end_text_edit();
|
||||
}
|
||||
let label = if names.len() == 1 { "Delete Symbol" } else { "Delete Symbols" };
|
||||
s.edit(label, |d, _| {
|
||||
s.edit(label, |d, sel| {
|
||||
if editing {
|
||||
crate::cmd::symboledit::finish(d, sel);
|
||||
}
|
||||
// Every instance goes before any definition, whatever the order of `names`: expanding an
|
||||
// instance of one symbol can bring out instances of another being deleted (nested in its
|
||||
// art), which expand in turn while their definitions are still there. Each round expands
|
||||
@@ -519,6 +548,9 @@ pub(super) fn symbol_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(se) = &session {
|
||||
crate::cmd::symboledit::leave_editing(s, se)?;
|
||||
}
|
||||
ok()
|
||||
}
|
||||
|
||||
|
||||
@@ -481,6 +481,7 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let label = e.label.clone();
|
||||
let costs = e.costs();
|
||||
let editing = st.doc.mask_edit.map(|m| m.layer);
|
||||
let editing_symbol = st.doc.symbol_edit.clone();
|
||||
let doc = std::mem::replace(&mut st.doc, e.doc);
|
||||
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
|
||||
let selection = shared_selection(selection, st.history.redo.last());
|
||||
@@ -494,6 +495,7 @@ fn undo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
crate::deselect_hidden(&menu, &st.doc, &mut st.selection);
|
||||
st.sync_new_art();
|
||||
super::maskedit::follow_history(st, editing);
|
||||
super::symboledit::follow_history(st, editing_symbol.as_ref());
|
||||
leave_isolation_if_gone(st);
|
||||
super::globaledit::follow_history(st);
|
||||
Ok(json!({ "undone": label }))
|
||||
@@ -507,6 +509,7 @@ fn redo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let label = e.label.clone();
|
||||
let costs = e.costs();
|
||||
let editing = st.doc.mask_edit.map(|m| m.layer);
|
||||
let editing_symbol = st.doc.symbol_edit.clone();
|
||||
let doc = std::mem::replace(&mut st.doc, e.doc);
|
||||
let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection));
|
||||
let selection = shared_selection(selection, st.history.undo.last());
|
||||
@@ -519,6 +522,7 @@ fn redo(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
crate::deselect_hidden(&menu, &st.doc, &mut st.selection);
|
||||
st.sync_new_art();
|
||||
super::maskedit::follow_history(st, editing);
|
||||
super::symboledit::follow_history(st, editing_symbol.as_ref());
|
||||
leave_isolation_if_gone(st);
|
||||
super::globaledit::follow_history(st);
|
||||
Ok(json!({ "redone": label }))
|
||||
|
||||
@@ -90,7 +90,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Batch",
|
||||
[],
|
||||
None,
|
||||
"{label?, commands: [{command, params}], artboard?: index (the active artboard; the app passes it: each edit.pasteInPlace, edit.pasteInFront, edit.pasteInBack, object.align, object.distribute, object.distributeSpacing or select.allOnArtboard step that names none gets it, and each artboard.fitToArt or artboard.fitToSelection step that names no index gets it as index, or the last artboard of a document with fewer)} run several commands as ONE undo step (one in each document they edit: steps may open, switch, close or revert documents); stops at the first error and rolls everything back, documents included",
|
||||
"{label?, commands: [{command, params}], artboard?: index (the active artboard; the app passes it: each edit.pasteInPlace, edit.pasteInFront, edit.pasteInBack, object.align, object.distribute, object.distributeSpacing, select.allOnArtboard or symbol.edit step that names none gets it, and each artboard.fitToArt or artboard.fitToSelection step that names no index gets it as index, or the last artboard of a document with fewer)} run several commands as ONE undo step (one in each document they edit: steps may open, switch, close or revert documents); stops at the first error and rolls everything back, documents included",
|
||||
has_doc,
|
||||
batch::batch
|
||||
),
|
||||
|
||||
@@ -334,13 +334,27 @@ pub(super) fn isolate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
pub(super) fn exit_isolation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Opacity mask editing and pattern editing isolate their editing layer. Leaving isolation mode
|
||||
// then leaves that mode, as Stop Editing Opacity Mask and the isolation bar's Done do.
|
||||
// Opacity mask editing, pattern editing and symbol editing isolate their editing layer. Leaving
|
||||
// isolation mode then leaves that mode, as Stop Editing Opacity Mask and the isolation bar's
|
||||
// Done do. A group isolated inside the symbol's art goes back to the symbol's layer.
|
||||
let st = s.doc()?;
|
||||
if let Some(layer) = st.doc.symbol_edit.as_ref().map(|e| e.layer)
|
||||
&& let Some(inner) = st.isolation.filter(|i| *i != layer)
|
||||
{
|
||||
crate::cmd::settle_interaction(s)?;
|
||||
let st = s.doc_mut()?;
|
||||
crate::cmd::distortcmds::finish_edit_text(st, inner);
|
||||
st.isolation = Some(layer);
|
||||
st.selection.set([inner]);
|
||||
st.revision += 1;
|
||||
return ok();
|
||||
}
|
||||
let leave_mode: Option<fn(&mut Session, &Value) -> Result<Value>> = if st.doc.mask_edit.is_some() {
|
||||
Some(crate::cmd::maskedit::leave)
|
||||
} else if st.doc.pattern_edit.is_some() {
|
||||
Some(crate::cmd::patterncmds::pattern_done)
|
||||
} else if st.doc.symbol_edit.is_some() {
|
||||
Some(crate::cmd::symboledit::leave)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -161,12 +161,18 @@ fn in_pattern_edit(s: &Session) -> std::result::Result<(), String> {
|
||||
editing(s).map(|_| ()).ok_or_else(|| "not in pattern editing mode".into())
|
||||
}
|
||||
|
||||
/// Make: a selection, and no opacity mask is being edited
|
||||
/// ([`super::maskedit::outside_mask_editing`]), since the tile layer would replace the mask editing
|
||||
/// layer as the isolated layer.
|
||||
/// Make: a selection, and no opacity mask or symbol is being edited
|
||||
/// ([`super::maskedit::outside_mask_editing`], [`outside_symbol_editing`]), since the tile layer
|
||||
/// would replace that mode's editing layer as the isolated layer.
|
||||
fn can_make_pattern(s: &Session) -> std::result::Result<(), String> {
|
||||
has_selection(s)?;
|
||||
super::maskedit::outside_mask_editing(s)
|
||||
super::maskedit::outside_mask_editing(s)?;
|
||||
outside_symbol_editing(s)
|
||||
}
|
||||
|
||||
/// No symbol is being edited (symbol editing mode shows the symbol in isolation mode).
|
||||
fn outside_symbol_editing(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.active().is_some_and(|st| st.doc.symbol_edit.is_some()) { Err("exit isolation mode first".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
fn can_edit_pattern(s: &Session) -> std::result::Result<(), String> {
|
||||
@@ -175,6 +181,7 @@ fn can_edit_pattern(s: &Session) -> std::result::Result<(), String> {
|
||||
return Err("already editing a pattern".into());
|
||||
}
|
||||
super::maskedit::outside_mask_editing(s)?;
|
||||
outside_symbol_editing(s)?;
|
||||
if s.active().is_some_and(|st| st.doc.patterns.is_empty()) { Err("the document has no patterns".into()) } else { Ok(()) }
|
||||
}
|
||||
|
||||
|
||||
@@ -773,9 +773,10 @@ fn select(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
Ok(json!({"id": target.0}))
|
||||
}
|
||||
/// The layer an opacity mask or a pattern is edited in, which stays isolated while that mode is on.
|
||||
/// The layer an opacity mask, a pattern or a symbol is edited in, which stays isolated while that
|
||||
/// mode is on.
|
||||
fn editing_layer(d: &Document) -> Option<NodeId> {
|
||||
d.mask_edit.map(|m| m.layer).or_else(|| d.pattern_edit.as_ref().map(|e| e.layer))
|
||||
d.mask_edit.map(|m| m.layer).or_else(|| d.pattern_edit.as_ref().map(|e| e.layer)).or_else(|| d.symbol_edit.as_ref().map(|e| e.layer))
|
||||
}
|
||||
/// Leave isolation mode, Construction mode included. While an opacity mask or a pattern is edited,
|
||||
/// its editing layer stays isolated.
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
//! 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::{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.
|
||||
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"));
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/// Each instance's appearance changes to objects of symbol `name`'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 names no object afterwards.
|
||||
fn follow_objects(d: &mut Document, name: &str, old: (&[Arc<Node>], bool), new: (&[Arc<Node>], bool)) {
|
||||
let depth = vectorcraft_doc::symbol_overrides::MAX_PATH_DEPTH + 1;
|
||||
for id in super::brushsym::instances_of(d, Some(name)) {
|
||||
let Some(n) = d.node_mut(id) else { continue };
|
||||
if n.extras().symbol_overrides.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut e = n.extras_mut();
|
||||
e.symbol_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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
super::brushsym::fit_instances_of(d, &se.symbol);
|
||||
if let Some(e) = d.symbol_edit.as_mut() {
|
||||
e.changed = true;
|
||||
e.group = group;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user