mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -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:
@@ -47,6 +47,7 @@ impl Document {
|
||||
patterns,
|
||||
pattern_edit,
|
||||
mask_edit,
|
||||
symbol_edit,
|
||||
next_id: _,
|
||||
unknown,
|
||||
page_isolate,
|
||||
@@ -113,6 +114,7 @@ impl Document {
|
||||
(a, b) => a.is_none() && b.is_none(),
|
||||
}
|
||||
&& *mask_edit == other.mask_edit
|
||||
&& *symbol_edit == other.symbol_edit
|
||||
&& *unknown == other.unknown
|
||||
&& *page_isolate == other.page_isolate
|
||||
&& *page_knockout == other.page_knockout
|
||||
@@ -221,6 +223,7 @@ impl Document {
|
||||
patterns: vec![],
|
||||
pattern_edit: None,
|
||||
mask_edit: None,
|
||||
symbol_edit: None,
|
||||
next_id: 1,
|
||||
unknown: Default::default(),
|
||||
page_isolate: false,
|
||||
|
||||
@@ -98,18 +98,22 @@ impl Document {
|
||||
self.views = kept;
|
||||
}
|
||||
|
||||
/// Leave opacity-mask editing: drop the temporary editing layer, a working copy of art the
|
||||
/// mask already holds (the engine syncs it after every edit).
|
||||
/// Leave opacity-mask editing and symbol editing: drop the temporary editing layer, a working
|
||||
/// copy of art the mask or the symbol already holds (the engine syncs it after every edit).
|
||||
pub fn drop_edit_modes(&mut self) {
|
||||
if let Some(me) = self.mask_edit.take() {
|
||||
self.layers.retain(|l| l.id != me.layer);
|
||||
}
|
||||
if let Some(se) = self.symbol_edit.take() {
|
||||
self.layers.retain(|l| l.id != se.layer);
|
||||
}
|
||||
}
|
||||
|
||||
/// The document as saved and exported: without the opacity-mask editing layer. Borrowed when
|
||||
/// no mask is being edited. (Pattern editing is saved: its tile layer holds unapplied edits.)
|
||||
/// The document as saved and exported: without the opacity-mask or symbol editing layer.
|
||||
/// Borrowed when neither mode is on. (Pattern editing is saved: its tile layer holds unapplied
|
||||
/// edits.)
|
||||
pub fn without_edit_modes(&self) -> std::borrow::Cow<'_, Document> {
|
||||
if self.mask_edit.is_none() {
|
||||
if self.mask_edit.is_none() && self.symbol_edit.is_none() {
|
||||
return std::borrow::Cow::Borrowed(self);
|
||||
}
|
||||
let mut d = self.clone();
|
||||
|
||||
@@ -142,7 +142,7 @@ pub use guides::{GridPrefs, Guide, RulerMode};
|
||||
#[cfg(test)]
|
||||
use limits::MAX_NEXT_ID;
|
||||
pub use limits::{DocError, FORMAT_VERSION, MAX_COORD};
|
||||
pub use records::{ImageBlob, MaskEdit, NewArtIds, SavedSelection, SavedView, Symbol};
|
||||
pub use records::{ImageBlob, MaskEdit, NewArtIds, SavedSelection, SavedView, Symbol, SymbolEdit};
|
||||
pub use units::{ColorMode, Unit, parse_number};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -222,6 +222,10 @@ pub struct Document {
|
||||
/// still read so files saved mid-edit by older versions load without the editing layer.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub mask_edit: Option<MaskEdit>,
|
||||
/// Symbol editing mode, while active. Never saved (see [`Document::without_edit_modes`]): the
|
||||
/// symbol already holds the art of its editing layer.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub symbol_edit: Option<SymbolEdit>,
|
||||
next_id: u64,
|
||||
/// Foreign data preserved on round-trip.
|
||||
#[serde(default, skip_serializing_if = "skip::is_empty_map")]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Arc, Deserialize, Node, NodeId, Point, Serialize};
|
||||
use super::{Affine, Arc, Deserialize, Node, NodeId, Point, Serialize};
|
||||
|
||||
/// New art on a locked artboard ([`Document::new_art`]). Setting it changes the document in
|
||||
/// place, so a selection change that sets it doesn't replace the document.
|
||||
@@ -44,6 +44,29 @@ pub struct MaskEdit {
|
||||
pub layer: NodeId,
|
||||
}
|
||||
|
||||
/// Symbol editing mode: the art of symbol `symbol` lives on the temporary `layer` while editing,
|
||||
/// placed by `xf` (from the symbol's coordinates to the document's), and goes back into the symbol
|
||||
/// after each edit, so every instance follows it.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SymbolEdit {
|
||||
pub symbol: String,
|
||||
pub layer: NodeId,
|
||||
/// The instance editing was entered from (a double-click on it), selected again on leaving after an edit;
|
||||
/// `None` when it was entered from the Symbols panel.
|
||||
pub instance: Option<NodeId>,
|
||||
pub xf: Affine,
|
||||
/// The symbol's art is a group whose members are on the layer: the group takes them back.
|
||||
pub group: bool,
|
||||
/// An edit changed the art since editing was entered.
|
||||
pub changed: bool,
|
||||
/// The isolated object from before editing was entered, isolated again on leaving.
|
||||
#[serde(default)]
|
||||
pub back_isolation: Option<NodeId>,
|
||||
/// The current layer from before editing was entered, current again on leaving.
|
||||
#[serde(default)]
|
||||
pub back_layer: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// A saved view (View → New View…): zoom, centre and rotation, listed at the bottom of the View menu.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SavedView {
|
||||
|
||||
@@ -84,6 +84,7 @@ mod style;
|
||||
pub mod stylelib;
|
||||
mod swatch;
|
||||
pub mod swatchlib;
|
||||
pub(crate) mod symboledit;
|
||||
pub mod symbollib;
|
||||
pub(crate) mod tabs;
|
||||
mod textedit;
|
||||
@@ -183,11 +184,15 @@ pub fn has_multi(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
if st.selection.len() < 2 { Err("select at least two objects".into()) } else { Ok(()) }
|
||||
}
|
||||
/// The isolated group, layer or object in isolation mode. Pattern editing mode and opacity mask
|
||||
/// editing isolate their own layer, which does not count here; an object isolated inside them
|
||||
/// counts.
|
||||
/// The isolated group, layer or object in isolation mode. Pattern editing mode, opacity mask
|
||||
/// editing and symbol editing mode isolate their own layer, which does not count here; an object
|
||||
/// isolated inside them counts.
|
||||
pub(crate) fn ordinary_isolation(st: &crate::DocState) -> Option<vectorcraft_doc::NodeId> {
|
||||
st.isolation.filter(|l| !st.doc.pattern_edit.as_ref().is_some_and(|e| e.layer == *l) && !st.doc.mask_edit.as_ref().is_some_and(|e| e.layer == *l))
|
||||
st.isolation.filter(|l| {
|
||||
!st.doc.pattern_edit.as_ref().is_some_and(|e| e.layer == *l)
|
||||
&& !st.doc.mask_edit.as_ref().is_some_and(|e| e.layer == *l)
|
||||
&& !st.doc.symbol_edit.as_ref().is_some_and(|e| e.layer == *l)
|
||||
})
|
||||
}
|
||||
/// Objects are selected ([`has_selection`]), and in isolation mode ([`ordinary_isolation`]) at
|
||||
/// least one of them is inside the isolated object: Object › Lock's and Object › Hide's items.
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -2,10 +2,10 @@ use super::{NodeId, Value, cmd};
|
||||
|
||||
/// The commands that act on the active artboard (#693), which the app passes as their `artboard`:
|
||||
/// Paste in Place, in Front and in Back paste onto it, Align to Artboard aligns to it, Distribute
|
||||
/// Objects and Distribute Spacing with Align to Artboard distribute objects across it, and All on
|
||||
/// Active Artboard selects on it. A `command.batch` given `artboard` passes it on to each of their
|
||||
/// steps that names none.
|
||||
pub const ON_ACTIVE_ARTBOARD: [&str; 7] = [
|
||||
/// Objects and Distribute Spacing with Align to Artboard distribute objects across it, All on
|
||||
/// Active Artboard selects on it, and Edit Symbol from the Symbols panel centers the symbol's art
|
||||
/// on it. A `command.batch` given `artboard` passes it on to each of their steps that names none.
|
||||
pub const ON_ACTIVE_ARTBOARD: [&str; 8] = [
|
||||
"edit.pasteInPlace",
|
||||
"edit.pasteInFront",
|
||||
"edit.pasteInBack",
|
||||
@@ -13,6 +13,7 @@ pub const ON_ACTIVE_ARTBOARD: [&str; 7] = [
|
||||
"object.distribute",
|
||||
"object.distributeSpacing",
|
||||
"select.allOnArtboard",
|
||||
"symbol.edit",
|
||||
];
|
||||
|
||||
/// The commands that resize the artboards the app passes (Object › Artboards): the app passes the
|
||||
@@ -23,9 +24,9 @@ pub const FIT_ACTIVE_ARTBOARD: [&str; 2] = ["artboard.fitToArt", "artboard.fitTo
|
||||
|
||||
/// `(command, param)` pairs the journal records only so a replay reproduces the original run
|
||||
/// (dates from the clock: `cmd::clock_date`; the artboards the app passes to the
|
||||
/// [`ON_ACTIVE_ARTBOARD`] and [`FIT_ACTIVE_ARTBOARD`] commands and to a batch); an action leaves
|
||||
/// them out ([`Session::journal_for_action`]).
|
||||
const REPLAY_ONLY: [(&str, &str); 14] = [
|
||||
/// [`ON_ACTIVE_ARTBOARD`] and [`FIT_ACTIVE_ARTBOARD`] commands and to a batch; the view's center
|
||||
/// the app passes to Edit Symbol); an action leaves them out ([`Session::journal_for_action`]).
|
||||
const REPLAY_ONLY: [(&str, &str); 16] = [
|
||||
("file.new", "created"),
|
||||
("document.save", "modified"),
|
||||
("file.saveAs", "modified"),
|
||||
@@ -39,6 +40,8 @@ const REPLAY_ONLY: [(&str, &str); 14] = [
|
||||
("artboard.fitToArt", "indices"),
|
||||
("artboard.fitToSelection", "index"),
|
||||
("select.allOnArtboard", "artboard"),
|
||||
("symbol.edit", "artboard"),
|
||||
("symbol.edit", "viewCenter"),
|
||||
("command.batch", "artboard"),
|
||||
];
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct DocState {
|
||||
/// Select › Start Global Edit: the objects edited together until Stop Global Edit or Escape
|
||||
/// ([`cmd::globaledit`]). Editing state: not saved, not undoable.
|
||||
pub global_edit: Option<cmd::globaledit::GlobalEdit>,
|
||||
/// What leaving symbol editing mode goes back to ([`cmd::symboledit`]).
|
||||
pub(crate) symbol_edit_return: cmd::symboledit::Return,
|
||||
pub interaction: Option<Interaction>,
|
||||
/// Edits made while this is open are one undo step (a scrubbed numeric field).
|
||||
pub undo_group: Option<UndoGroup>,
|
||||
@@ -246,6 +248,7 @@ impl DocState {
|
||||
variables_highlight: None,
|
||||
isolation: None,
|
||||
global_edit: None,
|
||||
symbol_edit_return: Default::default(),
|
||||
interaction: None,
|
||||
undo_group: None,
|
||||
last_transform: None,
|
||||
|
||||
@@ -37,6 +37,10 @@ impl Session {
|
||||
if shaper_changed || dim_changed {
|
||||
cmd::objects_on_path::refresh(&before, Arc::make_mut(&mut st.doc));
|
||||
}
|
||||
// Symbol editing: the symbol follows its art on the editing layer, before
|
||||
// inline graphics follow their symbols.
|
||||
let symbol_synced =
|
||||
if st.doc.symbol_edit.is_some() { cmd::symboledit::sync(&before, Arc::make_mut(&mut st.doc)) } else { Ok(()) };
|
||||
// Inline graphics in text follow their symbols.
|
||||
cmd::inline::refresh(&before, Arc::make_mut(&mut st.doc));
|
||||
// Text Wrap: area type follows its wrap objects; then threads re-flow.
|
||||
@@ -60,7 +64,9 @@ impl Session {
|
||||
if !st.doc.variables.bindings.is_empty() {
|
||||
Arc::make_mut(&mut st.doc).prune_variable_bindings();
|
||||
}
|
||||
if st.doc.too_deep(Some(&before)) {
|
||||
if let Err(e) = symbol_synced {
|
||||
Err(e)
|
||||
} else if st.doc.too_deep(Some(&before)) {
|
||||
// Deeper trees overflow the stack of what walks them.
|
||||
Err(EngineError::Other(TOO_DEEP.into()))
|
||||
} else if doc_sane(&st.doc, &st.selection) && objects_in_range(&st.doc, &spread) {
|
||||
@@ -82,6 +88,21 @@ impl Session {
|
||||
// The art on a locked artboard can't stay selected, however it got there, except
|
||||
// the new art this edit made and new art still selected.
|
||||
st.protect_locked_artboards(&before_sel.objects, &before_sel.new_art, Some(&*before));
|
||||
// Symbol or opacity mask editing ended because the edit deleted its layer:
|
||||
// isolation on the layer or inside it leaves too, out of symbol editing to what it
|
||||
// was before.
|
||||
let mode_layer = before.symbol_edit.as_ref().map(|e| e.layer).or(before.mask_edit.map(|m| m.layer));
|
||||
if let Some(layer) = mode_layer
|
||||
&& st.doc.node(layer).is_none()
|
||||
&& st.isolation.is_some_and(|i| i == layer || st.doc.node(i).is_none())
|
||||
{
|
||||
if let Some(se) = &before.symbol_edit {
|
||||
cmd::symboledit::go_back(st, se);
|
||||
} else {
|
||||
st.isolation = None;
|
||||
st.active_layer = st.doc.default_layer();
|
||||
}
|
||||
}
|
||||
st.revision += 1;
|
||||
if st.interaction.is_none() {
|
||||
let selection = match st.history.undo.last() {
|
||||
|
||||
@@ -158,6 +158,7 @@ mod swatchcmds;
|
||||
mod swatches;
|
||||
mod swatchlib;
|
||||
mod symbolbounds;
|
||||
mod symboledit;
|
||||
mod symbollib;
|
||||
mod targeting;
|
||||
mod textcombos;
|
||||
|
||||
@@ -1117,13 +1117,12 @@ fn place_and_break_link_round_trip_geometry() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_then_update_redefines_all_instances() {
|
||||
fn break_link_then_update_redefines_all_instances() {
|
||||
let mut s = session();
|
||||
let (inst, name, _) = make_symbol(&mut s);
|
||||
let other = NodeId(s.execute("symbol.place", &json!({"x": 300, "y": 200})).unwrap()["id"].as_u64().unwrap());
|
||||
select(&mut s, &[inst]);
|
||||
let r = s.execute("symbol.edit", &json!({})).unwrap();
|
||||
assert_eq!(r["name"], name);
|
||||
let r = s.execute("symbol.breakLink", &json!({})).unwrap();
|
||||
let art = NodeId(r["ids"][0].as_u64().unwrap());
|
||||
// Make the art twice as wide, then redefine.
|
||||
s.execute("object.transform", &json!({"ids": [art.0], "matrix": [2, 0, 0, 1, -140, 0]})).ok();
|
||||
|
||||
@@ -144,8 +144,14 @@ fn break_link_bakes_the_changes_and_edit_symbol_shows_the_symbols_own_art() {
|
||||
};
|
||||
let art = NodeId(s.execute("symbol.breakLink", &json!({"ids": [inst.0]})).unwrap()["ids"][0].as_u64().unwrap());
|
||||
assert_eq!(fills(&s, art), ["#0000ff", "#ff0000"]);
|
||||
let art = NodeId(s.execute("symbol.edit", &json!({"ids": [other.0]})).unwrap()["ids"][0].as_u64().unwrap());
|
||||
assert_eq!(fills(&s, art), ["#0000ff", "#0000ff"], "the symbol's art, without this instance's changes");
|
||||
// Symbol editing mode shows the symbol's art, without this instance's changes.
|
||||
s.execute("symbol.edit", &json!({"ids": [other.0]})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let layer = st.doc.symbol_edit.as_ref().unwrap().layer;
|
||||
let parts: Vec<String> =
|
||||
st.doc.node(layer).unwrap().children().unwrap().iter().map(|c| c.appearance.fill_paint().color().unwrap().to_hex()).collect();
|
||||
assert_eq!(parts, ["#0000ff", "#0000ff"], "the symbol's art, without this instance's changes");
|
||||
assert_eq!(overrides(&s, other), 1, "the instance keeps its changes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
//! Symbol editing mode (`symbol.edit`, `symbol.exitEditing`).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeKind;
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use crate::*;
|
||||
|
||||
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
|
||||
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
|
||||
}
|
||||
|
||||
fn id(v: &Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().expect("an id"))
|
||||
}
|
||||
|
||||
/// A 400 × 300 document with the 80 × 40 symbol "Box" made from a rectangle at (100, 50), its
|
||||
/// instance there and a second instance centered on (300, 200).
|
||||
fn two_instances() -> (Session, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 400, "height": 300}));
|
||||
run(&mut s, "shape.rectangle", json!({"x": 100, "y": 50, "width": 80, "height": 40}));
|
||||
let first = id(&run(&mut s, "symbol.new", json!({"name": "Box"})));
|
||||
let second = id(&run(&mut s, "symbol.place", json!({"name": "Box", "x": 300, "y": 200})));
|
||||
(s, first, second)
|
||||
}
|
||||
|
||||
/// The bounds of `id` as the document measures them: an instance's by its symbol's art.
|
||||
fn bounds(s: &Session, id: NodeId) -> Rect {
|
||||
s.doc().unwrap().doc.bounds_of(&[id], false).expect("bounds")
|
||||
}
|
||||
|
||||
fn close(a: Rect, b: Rect) -> bool {
|
||||
[(a.x0, b.x0), (a.y0, b.y0), (a.x1, b.x1), (a.y1, b.y1)].iter().all(|(x, y)| (x - y).abs() < 1e-6)
|
||||
}
|
||||
|
||||
fn is_instance(s: &Session, id: NodeId) -> bool {
|
||||
s.doc().unwrap().doc.node(id).is_some_and(|n| matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Box"))
|
||||
}
|
||||
|
||||
/// The objects on the editing layer.
|
||||
fn layer_art(s: &Session) -> Vec<NodeId> {
|
||||
let st = s.doc().unwrap();
|
||||
let layer = st.doc.symbol_edit.as_ref().expect("editing a symbol").layer;
|
||||
st.doc.node(layer).and_then(|l| l.children()).map(|c| c.iter().map(|n| n.id).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_symbol_edits_the_art_where_the_instance_shows_it_and_every_instance_follows() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
run(&mut s, "select.set", json!({"ids": [first.0]}));
|
||||
let r = run(&mut s, "symbol.edit", json!({}));
|
||||
assert_eq!((r["name"].as_str(), r["id"].as_u64()), (Some("Box"), Some(first.0)));
|
||||
let layer = NodeId(r["layer"].as_u64().unwrap());
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.isolation, Some(layer));
|
||||
assert_eq!(st.active_layer, Some(layer));
|
||||
// The instance stays linked: editing the symbol doesn't expand it.
|
||||
assert!(is_instance(&s, first) && is_instance(&s, second));
|
||||
let art = layer_art(&s);
|
||||
assert_eq!(art.len(), 1);
|
||||
assert!(close(bounds(&s, art[0]), Rect::new(100.0, 50.0, 180.0, 90.0)), "{:?}", bounds(&s, art[0]));
|
||||
assert_eq!(s.doc().unwrap().selection.objects, art);
|
||||
// An edit of the art reaches both instances at once, each at its own place.
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
assert!(close(bounds(&s, first), Rect::new(110.0, 50.0, 190.0, 90.0)), "{:?}", bounds(&s, first));
|
||||
assert!(close(bounds(&s, second), Rect::new(270.0, 180.0, 350.0, 220.0)), "{:?}", bounds(&s, second));
|
||||
// New art goes into the symbol.
|
||||
run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 10, "height": 10}));
|
||||
assert_eq!(layer_art(&s).len(), 2);
|
||||
assert!(close(bounds(&s, second), Rect::new(260.0, 180.0, 350.0, 240.0)), "{:?}", bounds(&s, second));
|
||||
let r = run(&mut s, "symbol.exitEditing", json!({}));
|
||||
assert_eq!(r["name"], "Box");
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none());
|
||||
assert!(st.doc.node(layer).is_none(), "the editing layer is gone");
|
||||
assert_eq!(st.selection.objects, vec![first]);
|
||||
assert!(is_instance(&s, first));
|
||||
assert!(close(bounds(&s, second), Rect::new(260.0, 180.0, 350.0, 240.0)), "{:?}", bounds(&s, second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_instance_transform_places_the_art_and_edits_go_back_through_it() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
// The second instance twice as large: a 20 pt move there is a 10 pt move of the symbol's art.
|
||||
run(&mut s, "select.set", json!({"ids": [second.0]}));
|
||||
run(&mut s, "object.scale", json!({"sx": 200, "sy": 200, "origin": [300, 200]}));
|
||||
let big = bounds(&s, second);
|
||||
assert!(close(big, Rect::new(220.0, 160.0, 380.0, 240.0)), "{big:?}");
|
||||
run(&mut s, "symbol.edit", json!({"id": second.0}));
|
||||
let art = layer_art(&s);
|
||||
assert!(close(bounds(&s, art[0]), big), "{:?}", bounds(&s, art[0]));
|
||||
run(&mut s, "object.move", json!({"dx": 20, "dy": 0}));
|
||||
assert!(close(bounds(&s, second), Rect::new(240.0, 160.0, 400.0, 240.0)), "{:?}", bounds(&s, second));
|
||||
assert!(close(bounds(&s, first), Rect::new(110.0, 50.0, 190.0, 90.0)), "{:?}", bounds(&s, first));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_symbols_panel_edits_a_symbol_without_an_instance_at_the_artboard_center() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
run(&mut s, "edit.clear", json!({"ids": [first.0, second.0]}));
|
||||
run(&mut s, "select.none", json!({}));
|
||||
let r = run(&mut s, "symbol.edit", json!({"name": "Box"}));
|
||||
assert!(r.get("id").is_none());
|
||||
let art = layer_art(&s);
|
||||
assert!(close(bounds(&s, art[0]), Rect::new(160.0, 130.0, 240.0, 170.0)), "{:?}", bounds(&s, art[0]));
|
||||
run(&mut s, "object.move", json!({"dx": 0, "dy": 5}));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
// No instance was added, and a new one shows the edited art.
|
||||
let mut instances = 0;
|
||||
s.doc().unwrap().doc.walk(|n| instances += usize::from(matches!(n.kind, NodeKind::SymbolInstance { .. })));
|
||||
assert_eq!(instances, 0);
|
||||
let placed = id(&run(&mut s, "symbol.place", json!({"name": "Box", "x": 200, "y": 150})));
|
||||
assert!(close(bounds(&s, placed), Rect::new(160.0, 135.0, 240.0, 175.0)), "{:?}", bounds(&s, placed));
|
||||
assert!(s.execute("symbol.edit", &json!({"name": "Nope"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_symbols_panel_edits_a_symbol_at_the_center_of_the_active_artboard_or_the_view() {
|
||||
let (mut s, _, _) = two_instances();
|
||||
run(&mut s, "artboard.new", json!({"x": 500, "y": 0, "width": 200, "height": 100}));
|
||||
run(&mut s, "select.none", json!({}));
|
||||
run(&mut s, "symbol.edit", json!({"name": "Box", "artboard": 1}));
|
||||
let art = layer_art(&s);
|
||||
assert!(close(bounds(&s, art[0]), Rect::new(560.0, 30.0, 640.0, 70.0)), "{:?}", bounds(&s, art[0]));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
// An artboard the document doesn't have: the first one.
|
||||
run(&mut s, "symbol.edit", json!({"name": "Box", "artboard": 7, "viewCenter": [1000, 1000]}));
|
||||
let art = layer_art(&s);
|
||||
assert!(close(bounds(&s, art[0]), Rect::new(160.0, 130.0, 240.0, 170.0)), "{:?}", bounds(&s, art[0]));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
// Without artboards, the view's center.
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).artboards.clear();
|
||||
run(&mut s, "symbol.edit", json!({"name": "Box", "viewCenter": [1000, 1000]}));
|
||||
let art = layer_art(&s);
|
||||
assert!(close(bounds(&s, art[0]), Rect::new(960.0, 980.0, 1040.0, 1020.0)), "{:?}", bounds(&s, art[0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_without_an_edit_leaves_the_document_and_its_history_as_they_were() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
let art = s.doc().unwrap().doc.symbols[0].art.clone();
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
|
||||
run(&mut s, "object.exitIsolation", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), steps);
|
||||
assert!(std::sync::Arc::ptr_eq(&st.doc.symbols[0].art, &art));
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none());
|
||||
// Outside the mode, leaving it does nothing.
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
|
||||
// A step undone before editing can still be redone after leaving without an edit.
|
||||
run(&mut s, "shape.rectangle", json!({"x": 300, "y": 20, "width": 10, "height": 10}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
assert!(s.doc().unwrap().history.redo.is_empty());
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
assert_eq!(s.doc().unwrap().history.redo.len(), 1);
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
// Leaving after an edit that was undone adds a step, keeping the redo step that holds the
|
||||
// editing layer apart from the document outside the mode.
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), steps + 1);
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_without_an_edit_keeps_the_current_symbol_picked_while_editing() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}));
|
||||
run(&mut s, "symbol.new", json!({"name": "Small"}));
|
||||
run(&mut s, "symbol.setCurrent", json!({"name": "Box"}));
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
// A click in the Symbols panel while editing: not an undo step.
|
||||
run(&mut s, "symbol.setCurrent", json!({"name": "Small"}));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), steps);
|
||||
assert_eq!(st.doc.unknown.get("currentSymbol").and_then(Value::as_str), Some("Small"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_right_after_a_batch_that_only_entered_takes_its_step_back() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
run(&mut s, "command.batch", json!({"commands": [{"command": "symbol.edit", "params": {"id": first.0}}]}));
|
||||
assert!(s.doc().unwrap().doc.symbol_edit.is_some());
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), steps);
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_right_after_a_batch_that_edited_and_entered_keeps_its_edits() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
// A batch named like the entering step that moves an instance before entering: leaving keeps
|
||||
// the move and adds a step of its own.
|
||||
let moved = Rect::new(310.0, 180.0, 390.0, 220.0);
|
||||
let commands = json!([
|
||||
{"command": "select.set", "params": {"ids": [second.0]}},
|
||||
{"command": "object.move", "params": {"dx": 50, "dy": 0}},
|
||||
{"command": "symbol.edit", "params": {"id": first.0}},
|
||||
]);
|
||||
run(&mut s, "command.batch", json!({"label": "Edit Symbol", "commands": commands}));
|
||||
assert!(close(bounds(&s, second), moved), "{:?}", bounds(&s, second));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none());
|
||||
assert_eq!(st.history.undo.len(), steps + 2);
|
||||
assert!(close(bounds(&s, second), moved), "{:?}", bounds(&s, second));
|
||||
// Undo takes back the exit, then the batch's move and entry together.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.doc().unwrap().doc.symbol_edit.is_some());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none());
|
||||
assert!(close(bounds(&s, second), Rect::new(260.0, 180.0, 340.0, 220.0)), "{:?}", bounds(&s, second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redefining_the_symbol_being_edited_leaves_symbol_editing_mode_in_the_same_step() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
let other = id(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 200, "width": 30, "height": 30})));
|
||||
let layer = NodeId(run(&mut s, "symbol.edit", json!({"id": first.0}))["layer"].as_u64().unwrap());
|
||||
// The art on the editing layer can't redefine the symbol.
|
||||
let e = s.execute("symbol.update", &json!({"name": "Box"})).unwrap_err().to_string();
|
||||
assert!(e.contains("can't hold an instance of itself"), "{e}");
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
run(&mut s, "symbol.update", json!({"name": "Box", "ids": [other.0]}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none() && st.doc.node(layer).is_none());
|
||||
assert_eq!(st.history.undo.len(), steps + 1);
|
||||
// Every instance shows the new 30 × 30 art, and a later edit can't bring the old art back.
|
||||
assert!(close(bounds(&s, second), Rect::new(285.0, 185.0, 315.0, 215.0)), "{:?}", bounds(&s, second));
|
||||
run(&mut s, "shape.rectangle", json!({"x": 300, "y": 10, "width": 10, "height": 10}));
|
||||
assert!(close(bounds(&s, second), Rect::new(285.0, 185.0, 315.0, 215.0)), "{:?}", bounds(&s, second));
|
||||
// Undo goes back into the edit as it was.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.isolation, Some(layer));
|
||||
assert!(st.doc.symbol_edit.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_goes_back_to_the_isolation_before_and_undo_keeps_a_group_isolated_inside() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
// The instance in an isolated group.
|
||||
let other = id(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 200, "width": 20, "height": 20})));
|
||||
run(&mut s, "select.set", json!({"ids": [first.0, other.0]}));
|
||||
let group = id(&run(&mut s, "object.group", json!({})));
|
||||
run(&mut s, "object.isolate", json!({"id": group.0}));
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(group));
|
||||
// A group isolated inside the symbol's art stays isolated through an undo.
|
||||
let layer = NodeId(run(&mut s, "symbol.edit", json!({"id": first.0}))["layer"].as_u64().unwrap());
|
||||
run(&mut s, "shape.rectangle", json!({"x": 100, "y": 100, "width": 10, "height": 10}));
|
||||
let art: Vec<u64> = layer_art(&s).iter().map(|i| i.0).collect();
|
||||
run(&mut s, "select.set", json!({ "ids": art }));
|
||||
let inner = id(&run(&mut s, "object.group", json!({})));
|
||||
run(&mut s, "object.isolate", json!({"id": inner.0}));
|
||||
run(&mut s, "select.all", json!({}));
|
||||
run(&mut s, "object.move", json!({"dx": 5, "dy": 0}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(inner));
|
||||
run(&mut s, "object.exitIsolation", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(layer));
|
||||
run(&mut s, "object.exitIsolation", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(group));
|
||||
// Deleting the editing layer goes back there too.
|
||||
let layer = NodeId(run(&mut s, "symbol.edit", json!({"id": first.0}))["layer"].as_u64().unwrap());
|
||||
run(&mut s, "layer.delete", json!({"ids": [layer.0]}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(group));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaving_after_a_save_keeps_the_document_saved() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
for edit in [false, true] {
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
if edit {
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
}
|
||||
// The save leaves the editing layer out of the file.
|
||||
s.doc_mut().unwrap().mark_saved();
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(!st.is_dirty(), "edit: {edit}");
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none(), "edit: {edit}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_the_editing_layer_leaves_symbol_editing_mode() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
let layer = NodeId(run(&mut s, "symbol.edit", json!({"id": first.0}))["layer"].as_u64().unwrap());
|
||||
run(&mut s, "layer.delete", json!({"ids": [layer.0]}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.doc.node(layer).is_none());
|
||||
assert_eq!(st.isolation, None);
|
||||
assert!(st.active_layer.is_some_and(|l| st.doc.node(l).is_some()));
|
||||
assert!(is_instance(&s, first));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_and_redo_restore_the_art_every_instance_shows_and_the_mode() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
let layer = s.doc().unwrap().doc.symbol_edit.as_ref().unwrap().layer;
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(layer), "back in symbol editing mode");
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(close(bounds(&s, second), Rect::new(260.0, 180.0, 340.0, 220.0)), "{:?}", bounds(&s, second));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(layer));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none());
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
run(&mut s, "edit.redo", json!({}));
|
||||
assert!(close(bounds(&s, second), Rect::new(270.0, 180.0, 350.0, 220.0)), "{:?}", bounds(&s, second));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(layer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instances_of_one_symbol_only_and_no_instance_of_itself() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}));
|
||||
let other = id(&run(&mut s, "symbol.new", json!({"name": "Small"})));
|
||||
run(&mut s, "select.set", json!({"ids": [first.0, other.0]}));
|
||||
let e = s.execute("symbol.edit", &json!({})).unwrap_err().to_string();
|
||||
assert!(e.contains("select instances of one symbol"), "{e}");
|
||||
let e = s.execute("symbol.edit", &json!({"ids": [first.0, other.0]})).unwrap_err().to_string();
|
||||
assert!(e.contains("select instances of one symbol"), "{e}");
|
||||
assert!(s.execute("symbol.edit", &json!({"ids": [first.0, 999_999]})).is_err());
|
||||
assert!(s.doc().unwrap().doc.symbol_edit.is_none());
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
let e = s.execute("symbol.place", &json!({"name": "Box"})).unwrap_err().to_string();
|
||||
assert!(e.contains("can't hold an instance of itself"), "{e}");
|
||||
assert_eq!(layer_art(&s).len(), 1);
|
||||
// An instance of another symbol may go in.
|
||||
run(&mut s, "symbol.place", json!({"name": "Small"}));
|
||||
assert_eq!(layer_art(&s).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_editing_modes_and_symbol_editing_do_not_overlap() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
assert!(s.execute("object.pattern.make", &json!({})).is_err());
|
||||
assert!(s.execute("transparency.editOpacityMask", &json!({})).is_err());
|
||||
// Editing another symbol from the panel ends this edit first.
|
||||
run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
run(&mut s, "select.set", json!({"ids": [second.0]}));
|
||||
run(&mut s, "symbol.edit", json!({}));
|
||||
run(&mut s, "symbol.edit", json!({"name": "Box", "x": 50, "y": 50}));
|
||||
assert_eq!(s.doc().unwrap().doc.symbol_edit.as_ref().unwrap().instance, None);
|
||||
assert_eq!(s.doc().unwrap().doc.layers.iter().filter(|l| l.name.as_deref() == Some("Box")).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renaming_deleting_and_saving_while_a_symbol_is_edited() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
let layer = s.doc().unwrap().doc.symbol_edit.as_ref().unwrap().layer;
|
||||
// Saving leaves the editing layer out; the symbol already holds the edit.
|
||||
let saved = s.doc().unwrap().doc.without_edit_modes().into_owned();
|
||||
assert!(saved.symbol_edit.is_none() && saved.node(layer).is_none());
|
||||
assert!(saved.symbols[0].art.geometric_bounds().is_some());
|
||||
run(&mut s, "symbol.options", json!({"name": "Box", "newName": "Crate"}));
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.symbol_edit.as_ref().unwrap().symbol, "Crate");
|
||||
assert_eq!(st.doc.node(layer).and_then(|l| l.name.clone()).as_deref(), Some("Crate"));
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
// Deleting the symbol being edited leaves the mode in the same step.
|
||||
run(&mut s, "symbol.delete", json!({"name": "Crate"}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none() && st.doc.node(layer).is_none());
|
||||
assert!(st.doc.symbols.is_empty());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(layer));
|
||||
}
|
||||
|
||||
/// Deleting several symbols, the one being edited among them, leaves the mode in the same step.
|
||||
#[test]
|
||||
fn deleting_several_symbols_with_the_edited_one_leaves_the_mode() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
run(&mut s, "shape.rectangle", json!({"x": 10, "y": 200, "width": 20, "height": 20}));
|
||||
run(&mut s, "symbol.new", json!({"name": "Dot"}));
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
let layer = s.doc().unwrap().doc.symbol_edit.as_ref().unwrap().layer;
|
||||
run(&mut s, "symbol.delete", json!({"names": ["Dot", "Box"]}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.isolation.is_none() && st.doc.node(layer).is_none());
|
||||
assert!(st.doc.symbols.is_empty());
|
||||
}
|
||||
|
||||
/// Editing a dynamic symbol keeps each instance's appearance changes to the objects its art still
|
||||
/// has, and drops the changes to an object the edit deleted.
|
||||
#[test]
|
||||
fn editing_a_dynamic_symbol_keeps_the_instances_changes_to_the_objects_still_there() {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 400, "height": 300}));
|
||||
let a = id(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})));
|
||||
let b = id(&run(&mut s, "shape.rectangle", json!({"x": 60, "y": 10, "width": 40, "height": 40})));
|
||||
let inst = id(&run(&mut s, "symbol.new", json!({"name": "Pair", "ids": [a.0, b.0], "type": "dynamic"})));
|
||||
run(&mut s, "symbol.instance.setAppearance", json!({"id": inst.0, "path": [0], "fill": "#00ff00"}));
|
||||
run(&mut s, "symbol.instance.setAppearance", json!({"id": inst.0, "path": [1], "fill": "#ff0000"}));
|
||||
// Each change by the path it names and the color it paints.
|
||||
let changes = |s: &Session| -> Vec<(Vec<u32>, String)> {
|
||||
let n = s.doc().unwrap().doc.node(inst).unwrap().clone();
|
||||
n.extras().symbol_overrides.iter().map(|o| (o.path.clone(), o.fill.as_ref().and_then(|p| p.color()).unwrap().to_hex())).collect()
|
||||
};
|
||||
let other = id(&run(&mut s, "symbol.place", json!({"name": "Pair", "x": 200, "y": 200})));
|
||||
run(&mut s, "symbol.edit", json!({"id": other.0}));
|
||||
let art = layer_art(&s);
|
||||
assert_eq!(art.len(), 2);
|
||||
// Deleting the first object: the change to the second follows it to the first place.
|
||||
run(&mut s, "select.set", json!({"ids": [art[0].0]}));
|
||||
run(&mut s, "edit.clear", json!({}));
|
||||
assert_eq!(changes(&s), vec![(vec![0], "#ff0000".to_string())], "the change to the deleted object goes");
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
assert_eq!(changes(&s), vec![(vec![0], "#ff0000".to_string())]);
|
||||
assert!(s.doc().unwrap().doc.node(inst).is_some_and(|n| matches!(n.kind, NodeKind::SymbolInstance { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_back_into_an_earlier_edit_leaves_it_to_its_own_isolation() {
|
||||
let (mut s, first, second) = two_instances();
|
||||
let other = id(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 200, "width": 20, "height": 20})));
|
||||
run(&mut s, "select.set", json!({"ids": [second.0, other.0]}));
|
||||
let group = id(&run(&mut s, "object.group", json!({})));
|
||||
// The first edit is entered with nothing isolated.
|
||||
run(&mut s, "symbol.edit", json!({"id": first.0}));
|
||||
run(&mut s, "object.move", json!({"dx": 10, "dy": 0}));
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, None);
|
||||
// The second edit is entered with the group isolated.
|
||||
run(&mut s, "object.isolate", json!({"id": group.0}));
|
||||
run(&mut s, "symbol.edit", json!({"id": second.0}));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(group));
|
||||
// Back in the first edit, leaving it goes back to what it was entered from.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(s.doc().unwrap().doc.symbol_edit.is_some());
|
||||
run(&mut s, "symbol.exitEditing", json!({}));
|
||||
assert_eq!(s.doc().unwrap().isolation, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undoing_entry_with_a_group_isolated_inside_the_art_goes_back_to_before() {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 400, "height": 300}));
|
||||
let a = id(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})));
|
||||
let b = id(&run(&mut s, "shape.rectangle", json!({"x": 40, "y": 10, "width": 20, "height": 20})));
|
||||
run(&mut s, "select.set", json!({"ids": [a.0, b.0]}));
|
||||
let inner = id(&run(&mut s, "object.group", json!({})));
|
||||
let c = id(&run(&mut s, "shape.rectangle", json!({"x": 10, "y": 40, "width": 20, "height": 20})));
|
||||
run(&mut s, "select.set", json!({"ids": [inner.0, c.0]}));
|
||||
let instance = id(&run(&mut s, "symbol.new", json!({"name": "Pair"})));
|
||||
let before = s.doc().unwrap().active_layer;
|
||||
assert!(before.is_some());
|
||||
run(&mut s, "symbol.edit", json!({"id": instance.0}));
|
||||
let layer = s.doc().unwrap().doc.symbol_edit.as_ref().unwrap().layer;
|
||||
let group = {
|
||||
let st = s.doc().unwrap();
|
||||
layer_art(&s).into_iter().find(|i| st.doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Group { .. }))).expect("a group")
|
||||
};
|
||||
run(&mut s, "object.isolate", json!({"id": group.0}));
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(group));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.doc.symbol_edit.is_none() && st.doc.node(layer).is_none());
|
||||
assert_eq!(st.isolation, None);
|
||||
assert_eq!(st.active_layer, before);
|
||||
}
|
||||
@@ -1170,6 +1170,11 @@ impl SelectionTool {
|
||||
if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Text(_))) {
|
||||
return vec![Action::SwitchTool("type".into())];
|
||||
}
|
||||
// A symbol instance: its symbol is edited in symbol editing mode, except one inside
|
||||
// the art of a symbol already being edited.
|
||||
if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::SymbolInstance { .. })) {
|
||||
return if cx.doc.symbol_edit.is_some() { vec![] } else { vec![Action::Exec("symbol.edit".into(), json!({ "id": top.0 }))] };
|
||||
}
|
||||
// An object that [`isolates`], not the one already isolated.
|
||||
if cx.double_click_isolate && cx.doc.node(top).is_some_and(|n| isolates(&n.kind)) && cx.isolation != Some(top) {
|
||||
return vec![Action::Exec("object.isolate".into(), json!({ "id": top.0 }))];
|
||||
|
||||
@@ -1964,3 +1964,37 @@ fn the_preview_memo_measures_once_per_revision_selection_and_preview_bounds() {
|
||||
let c = cx(&moved, &s, &p);
|
||||
assert_eq!(selection_bounds(&c).map(|r| r.x1), Some(250.0));
|
||||
}
|
||||
|
||||
/// A double-click on a symbol instance edits its symbol in symbol editing mode (`symbol.edit`), and
|
||||
/// one outside the symbol's art in that mode leaves it, as it leaves isolation mode. An instance
|
||||
/// inside the art of the symbol being edited is not entered.
|
||||
#[test]
|
||||
fn a_double_click_on_a_symbol_instance_edits_its_symbol() {
|
||||
use vectorcraft_doc::{Appearance, Node, NodeKind, Symbol, SymbolEdit};
|
||||
let (mut d, rect) = doc_with_rect();
|
||||
let art = Node::path(NodeId(0), vectorcraft_geom::shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)), Appearance::default_art());
|
||||
d.symbols.push(Symbol { dynamic: false, name: "Dot".into(), art: std::sync::Arc::new(art) });
|
||||
let (l, id) = (d.layers[0].id, d.alloc_id());
|
||||
let xf = Affine::translate((300.0, 300.0)) * Affine::scale(2.0);
|
||||
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: "Dot".into(), xf })).unwrap();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let double = |d: &vectorcraft_doc::Document, isolation: Option<NodeId>, x: f64, y: f64| {
|
||||
let c = ToolContext { isolation, ..cx(d, &s, &p) };
|
||||
SelectionTool::default().pointer(&c, &ev(PointerKind::DoubleClick, x, y))
|
||||
};
|
||||
assert_eq!(double(&d, None, 300.0, 300.0), vec![Action::Exec("symbol.edit".into(), json!({"id": id.0}))]);
|
||||
assert_eq!(double(&d, None, 150.0, 150.0), vec![Action::Exec("object.isolate".into(), json!({"id": rect.0}))]);
|
||||
// In symbol editing mode, with the instance on the editing layer.
|
||||
d.symbol_edit = Some(SymbolEdit {
|
||||
symbol: "Other".into(),
|
||||
layer: l,
|
||||
instance: None,
|
||||
xf: Affine::IDENTITY,
|
||||
group: false,
|
||||
changed: false,
|
||||
back_isolation: None,
|
||||
back_layer: None,
|
||||
});
|
||||
assert_eq!(double(&d, Some(l), 300.0, 300.0), vec![]);
|
||||
assert_eq!(double(&d, Some(l), 450.0, 50.0), vec![Action::Exec("object.exitIsolation".into(), json!({}))]);
|
||||
}
|
||||
|
||||
@@ -38,8 +38,9 @@ impl VectorcraftApp {
|
||||
}
|
||||
let mut params = params;
|
||||
// Paste in place, in front and in back (#693), Select › All on Active Artboard (#1006), and Align,
|
||||
// Distribute Objects and Distribute Spacing with Align to Artboard act on the active artboard, the
|
||||
// view's, which the app passes as `artboard`. A batch passes it on to those steps.
|
||||
// Distribute Objects and Distribute Spacing with Align to Artboard, and Edit Symbol from the Symbols
|
||||
// panel act on the active artboard, the view's, which the app passes as `artboard`. A batch passes
|
||||
// it on to those steps.
|
||||
if (vectorcraft_engine::ON_ACTIVE_ARTBOARD.contains(&id) || id == "command.batch")
|
||||
&& params.get("artboard").is_none()
|
||||
&& self.view().is_some()
|
||||
@@ -48,6 +49,29 @@ impl VectorcraftApp {
|
||||
{
|
||||
p.insert("artboard".into(), serde_json::json!(panels::artboards::selected(self, n)));
|
||||
}
|
||||
// Edit Symbol from the Symbols panel in a document without artboards centers the symbol's
|
||||
// art in the view, whose center the app passes as `viewCenter`, to a batch's symbol.edit
|
||||
// steps too.
|
||||
if matches!(id, "symbol.edit" | "command.batch")
|
||||
&& let Some(c) = self.view().map(|v| v.center)
|
||||
{
|
||||
let fill = |p: &mut serde_json::Value| {
|
||||
if let Some(p) = p.as_object_mut() {
|
||||
p.entry("viewCenter").or_insert(serde_json::json!([c.x, c.y]));
|
||||
}
|
||||
};
|
||||
if id == "symbol.edit" {
|
||||
fill(&mut params);
|
||||
} else if let Some(serde_json::Value::Array(steps)) = params.get_mut("commands") {
|
||||
for step in steps {
|
||||
if step.get("command").and_then(serde_json::Value::as_str) == Some("symbol.edit")
|
||||
&& let Some(step) = step.as_object_mut()
|
||||
{
|
||||
fill(step.entry("params").or_insert_with(|| serde_json::json!({})));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Object › Artboards › Fit to Artwork Bounds resizes the selected artboards (the Artboard
|
||||
// tool's, else the active one), which the app passes as its `indices`, and Fit to Selected
|
||||
// Art resizes the active artboard, which the app passes as its `index`.
|
||||
|
||||
@@ -104,17 +104,22 @@ fn snapping_menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
|
||||
/// The Control bar's isolation button. In isolation mode it is Exit Isolation Mode, except while a
|
||||
/// pattern is edited (the isolation bar's Done and Cancel end that). Otherwise, with one object
|
||||
/// selected that [`vectorcraft_tools::select::isolates`], it is Isolate Selected Object, which
|
||||
/// also isolates a path inside a group that Direct Selection selected. An envelope's contents
|
||||
/// get no button: the envelope's Edit Envelope and Edit Contents buttons show instead.
|
||||
/// pattern is edited (the isolation bar's Done and Cancel end that), and Exit Symbol Editing Mode
|
||||
/// while a symbol's art is the isolated layer. Otherwise, with one object selected that
|
||||
/// [`vectorcraft_tools::select::isolates`], it is Isolate Selected Object, which also isolates a
|
||||
/// path inside a group that Direct Selection selected. An envelope's contents get no button: the
|
||||
/// envelope's Edit Envelope and Edit Contents buttons show instead.
|
||||
fn isolation_button(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let (icon, tip, id) = if st.isolation.is_some() {
|
||||
if st.doc.pattern_edit.is_some() {
|
||||
return;
|
||||
}
|
||||
("chevron-left", tl!("Exit Isolation Mode"), "object.exitIsolation")
|
||||
if st.doc.symbol_edit.as_ref().is_some_and(|e| st.isolation == Some(e.layer)) {
|
||||
("chevron-left", tl!("Exit Symbol Editing Mode"), "symbol.exitEditing")
|
||||
} else {
|
||||
("chevron-left", tl!("Exit Isolation Mode"), "object.exitIsolation")
|
||||
}
|
||||
} else if let [one] = st.selection.objects[..]
|
||||
&& st.doc.node(one).is_some_and(|n| vectorcraft_tools::select::isolates(&n.kind))
|
||||
&& vectorcraft_doc::live::envelope_of(&st.doc, one).is_none()
|
||||
|
||||
@@ -910,6 +910,7 @@ font-technology Variable Променливи
|
||||
Edit Selection Редактиране на селекцията
|
||||
Edit Selection… Редактиране на селекцията…
|
||||
Edit Symbol Редактиране на символ
|
||||
Exit Symbol Editing Mode Изход от режим на редактиране на символ
|
||||
Edit Text Редактиране на текст
|
||||
Edit the applied effect, or add another one? Да се редактира ли приведеният ефект или да се добави друг?
|
||||
Edit the point Редактиране на точката
|
||||
@@ -4221,6 +4222,8 @@ axis Both И двете
|
||||
@msg select an Intertwine group изберете група за преплитане (Intertwine)
|
||||
@msg select a stroke with a variable width изберете щрих с променлива дебелина
|
||||
@msg select a symbol instance изберете екземпляр на символ
|
||||
@msg select instances of one symbol изберете екземпляри на един символ
|
||||
@msg a symbol can't hold an instance of itself символ не може да съдържа свой собствен екземпляр
|
||||
@msg select an Image Trace object изберете обект за трасиране на изображение (Image Trace)
|
||||
@msg could not decode image: {0} изображението не може да бъде декодирано: {0}
|
||||
@msg image is empty изображението е празно
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3088,6 +3088,7 @@ theme Appearance Izgled
|
||||
Duplicate Brush Dupliciraj četkicu
|
||||
Duplicate Symbol Dupliciraj simbol
|
||||
Edit Symbol Uredi simbol
|
||||
Exit Symbol Editing Mode Izađi iz načina uređivanja simbola
|
||||
Expand Brush Strokes Proširi poteze četkicom
|
||||
New Brush… Nova četkica…
|
||||
New Brush Nova četkica
|
||||
@@ -4619,6 +4620,8 @@ imageTrace Outline Obris
|
||||
@msg a clipping path can't be attached to a path putanja isijecanja ne može se priložiti putanji
|
||||
@msg select a stroke with a variable width odaberite potez s promjenjivom širinom
|
||||
@msg select a symbol instance odaberite instancu simbola
|
||||
@msg select instances of one symbol odaberite instance jednog simbola
|
||||
@msg a symbol can't hold an instance of itself simbol ne može sadržavati instancu samog sebe
|
||||
@msg select an Image Trace object odaberite objekat praćenja slike
|
||||
@msg could not decode image: {0} nije moguće dekodirati sliku: {0}
|
||||
@msg image is empty slika je prazna
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1313,6 +1313,7 @@
|
||||
Edit Selection Edita la selecció
|
||||
Edit Selection… Edita la selecció…
|
||||
Edit Symbol Edita el símbol
|
||||
Exit Symbol Editing Mode Surt del mode d’edició de símbols
|
||||
Edit Text Edita el text
|
||||
Edit the applied effect, or add another one? Vols editar l’efecte aplicat o afegir-ne un altre?
|
||||
Edit the point Edita el punt
|
||||
@@ -5377,6 +5378,9 @@ digits Malayalam Malaiàlam
|
||||
@msg select a repeat selecciona una repetició
|
||||
@msg select a stroke with a variable width selecciona un traç amb amplada variable
|
||||
@msg select a symbol instance selecciona una instància de símbol
|
||||
@msg select instances of one symbol selecciona instàncies d’un sol símbol
|
||||
@msg a symbol can't hold an instance of itself un símbol no pot contenir una instància de si mateix
|
||||
@msg exit isolation mode first surt primer del mode d’aïllament
|
||||
@msg select an envelope selecciona un embolcall
|
||||
@msg select an image selecciona una imatge
|
||||
@msg select an Image Trace object selecciona un objecte d’Image Trace
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3119,6 +3119,7 @@ theme Appearance Motiv
|
||||
Duplicate Brush Duplikovat štětec
|
||||
Duplicate Symbol Duplikovat symbol
|
||||
Edit Symbol Upravit symbol
|
||||
Exit Symbol Editing Mode Ukončit režim úprav symbolu
|
||||
Expand Brush Strokes Rozložit tahy štětcem
|
||||
New Brush… Nový štětec…
|
||||
New Brush Nový štětec
|
||||
@@ -4659,6 +4660,8 @@ imageTrace Outline Obrys
|
||||
@msg a clipping path can't be attached to a path ořezovou cestu nelze připojit na cestu
|
||||
@msg select a stroke with a variable width vyberte tah s proměnnou šířkou
|
||||
@msg select a symbol instance vyberte instanci symbolu
|
||||
@msg select instances of one symbol vyberte instance jediného symbolu
|
||||
@msg a symbol can't hold an instance of itself symbol nemůže obsahovat instanci sebe sama
|
||||
@msg select an Image Trace object vyberte objekt vektorizace obrazu
|
||||
@msg could not decode image: {0} obraz nelze dekódovat: {0}
|
||||
@msg image is empty obraz je prázdný
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2872,6 +2872,7 @@ theme Appearance Udseende
|
||||
Duplicate Brush Duplikér pensel
|
||||
Duplicate Symbol Duplikér symbol
|
||||
Edit Symbol Redigér symbol
|
||||
Exit Symbol Editing Mode Afslut symbolredigeringstilstand
|
||||
Expand Brush Strokes Udvid penselstrøg
|
||||
New Brush… Ny pensel…
|
||||
New Brush Ny pensel
|
||||
@@ -4323,6 +4324,8 @@ imageTrace Outline Kontur
|
||||
@msg select a repeat markér en gentagelse
|
||||
@msg select a stroke with a variable width markér en streg med variabel bredde
|
||||
@msg select a symbol instance markér en symbolforekomst
|
||||
@msg select instances of one symbol markér forekomster af ét symbol
|
||||
@msg a symbol can't hold an instance of itself et symbol kan ikke indeholde en forekomst af sig selv
|
||||
@msg select an Image Trace object markér et Image Trace-objekt
|
||||
@msg could not decode image: {0} kunne ikke afkode billedet: {0}
|
||||
@msg image is empty billedet er tomt
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -904,6 +904,7 @@ font-technology Variable Variabel
|
||||
Edit Selection Auswahl bearbeiten
|
||||
Edit Selection… Auswahl bearbeiten…
|
||||
Edit Symbol Symbol bearbeiten
|
||||
Exit Symbol Editing Mode Symbolbearbeitungsmodus beenden
|
||||
Edit Text Text bearbeiten
|
||||
Edit the applied effect, or add another one? Den angewendeten Effekt bearbeiten oder einen weiteren hinzufügen?
|
||||
Edit the point Punkt bearbeiten
|
||||
@@ -4268,6 +4269,8 @@ theme Appearance Erscheinungsbild
|
||||
@msg a clipping path can't be attached to a path ein Schnittpfad kann nicht an einen Pfad angehängt werden
|
||||
@msg select a stroke with a variable width wählen Sie eine Kontur mit variabler Breite aus
|
||||
@msg select a symbol instance wählen Sie eine Symbolinstanz aus
|
||||
@msg select instances of one symbol wählen Sie Instanzen eines einzigen Symbols aus
|
||||
@msg a symbol can't hold an instance of itself ein Symbol kann keine Instanz von sich selbst enthalten
|
||||
@msg select an Image Trace object wählen Sie ein Bildnachzeichner-Objekt aus
|
||||
@msg could not decode image: {0} das Bild konnte nicht dekodiert werden: {0}
|
||||
@msg image is empty das Bild ist leer
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -880,6 +880,7 @@ font-technology Variable Μεταβλητές
|
||||
Edit Selection Επεξεργασία επιλογής
|
||||
Edit Selection… Επεξεργασία επιλογής…
|
||||
Edit Symbol Επεξεργασία συμβόλου
|
||||
Exit Symbol Editing Mode Έξοδος από τη λειτουργία επεξεργασίας συμβόλου
|
||||
Edit Text Επεξεργασία κειμένου
|
||||
Edit the applied effect, or add another one? Επεξεργασία του εφαρμοσμένου εφέ ή προσθήκη άλλου;
|
||||
Edit the point Επεξεργασία σημείου
|
||||
@@ -4301,6 +4302,8 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg select a repeat επιλέξτε επανάληψη
|
||||
@msg select a stroke with a variable width επιλέξτε περίγραμμα μεταβλητού πλάτους
|
||||
@msg select a symbol instance επιλέξτε στιγμιότυπο συμβόλου
|
||||
@msg select instances of one symbol επιλέξτε στιγμιότυπα ενός μόνο συμβόλου
|
||||
@msg a symbol can't hold an instance of itself ένα σύμβολο δεν μπορεί να περιέχει στιγμιότυπο του εαυτού του
|
||||
@msg select an Image Trace object επιλέξτε αντικείμενο διανυσματοποίησης εικόνας
|
||||
@msg could not decode image: {0} δεν ήταν δυνατή η αποκωδικοποίηση εικόνας: {0}
|
||||
@msg image is empty η εικόνα είναι κενή
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 164 and column 5.
|
@@ -925,6 +925,7 @@ font-technology Variable Variables
|
||||
Edit Selection Editar selección
|
||||
Edit Selection… Editar selección…
|
||||
Edit Symbol Editar símbolo
|
||||
Exit Symbol Editing Mode Salir del modo de edición de símbolos
|
||||
Edit Text Editar texto
|
||||
Edit the applied effect, or add another one? ¿Editar el efecto aplicado o agregar otro?
|
||||
Edit the point Editar el punto
|
||||
@@ -4353,6 +4354,8 @@ axis Both Ambos
|
||||
@msg a clipping path can't be attached to a path un trazado de recorte no se puede adjuntar a un trazado
|
||||
@msg select a stroke with a variable width selecciona un trazo con anchura variable
|
||||
@msg select a symbol instance selecciona una instancia de símbolo
|
||||
@msg select instances of one symbol selecciona instancias de un solo símbolo
|
||||
@msg a symbol can't hold an instance of itself un símbolo no puede contener una instancia de sí mismo
|
||||
@msg select an Image Trace object selecciona un objeto de calco de imagen
|
||||
@msg could not decode image: {0} no se pudo decodificar la imagen: {0}
|
||||
@msg image is empty la imagen está vacía
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3058,6 +3058,7 @@ theme Appearance Ulkoasu
|
||||
Duplicate Brush Monista sivellin
|
||||
Duplicate Symbol Monista symboli
|
||||
Edit Symbol Muokkaa symbolia
|
||||
Exit Symbol Editing Mode Poistu symbolin muokkaustilasta
|
||||
Expand Brush Strokes Laajenna sivellinviivat
|
||||
New Brush… Uusi sivellin…
|
||||
New Brush Uusi sivellin
|
||||
@@ -4561,6 +4562,8 @@ imageTrace Outline Ääriviiva
|
||||
@msg select a repeat valitse toisto
|
||||
@msg select a stroke with a variable width valitse viiva, jolla on vaihteleva leveys
|
||||
@msg select a symbol instance valitse symboli-ilmentymä
|
||||
@msg select instances of one symbol valitse yhden symbolin ilmentymiä
|
||||
@msg a symbol can't hold an instance of itself symboli ei voi sisältää omaa ilmentymäänsä
|
||||
@msg select an Image Trace object valitse kuvan jäljitys -objekti
|
||||
@msg could not decode image: {0} kuvaa ei voitu purkaa: {0}
|
||||
@msg image is empty kuva on tyhjä
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -928,6 +928,7 @@ font-technology Variable Variables
|
||||
Edit Selection Modifier la sélection
|
||||
Edit Selection… Modifier la sélection…
|
||||
Edit Symbol Modifier le symbole
|
||||
Exit Symbol Editing Mode Quitter le mode Modification de symbole
|
||||
Edit Text Modifier le texte
|
||||
Edit the applied effect, or add another one? Modifier l’effet appliqué ou en ajouter un autre ?
|
||||
Edit the point Modifier le point
|
||||
@@ -4343,6 +4344,8 @@ axis Both Les deux
|
||||
@msg a clipping path can't be attached to a path un tracé d’écrêtage ne peut pas être attaché à un tracé
|
||||
@msg select a stroke with a variable width sélectionnez un contour de largeur variable
|
||||
@msg select a symbol instance sélectionnez une instance de symbole
|
||||
@msg select instances of one symbol sélectionnez des instances d’un seul symbole
|
||||
@msg a symbol can't hold an instance of itself un symbole ne peut pas contenir une instance de lui-même
|
||||
@msg select an Image Trace object sélectionnez un objet de vectorisation de l’image
|
||||
@msg could not decode image: {0} impossible de décoder l’image : {0}
|
||||
@msg image is empty l’image est vide
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1193,6 +1193,7 @@ font-technology Variable Változó
|
||||
Edit Selection Kijelölés szerkesztése
|
||||
Edit Selection… Kijelölés szerkesztése…
|
||||
Edit Symbol Szimbólum szerkesztése
|
||||
Exit Symbol Editing Mode Kilépés a szimbólumszerkesztési módból
|
||||
Edit Text Szöveg szerkesztése
|
||||
Edit the applied effect, or add another one? Szerkeszted az alkalmazott effektust, vagy újat adsz hozzá?
|
||||
Edit the point Pont szerkesztése
|
||||
@@ -5076,6 +5077,8 @@ imageTrace Outline Körvonal
|
||||
@msg a clipping path can't be attached to a path vágóútvonal nem csatolható útvonalhoz
|
||||
@msg select a stroke with a variable width jelölj ki egy változó szélességű körvonalat
|
||||
@msg select a symbol instance jelölj ki egy szimbólumpéldányt
|
||||
@msg select instances of one symbol egyetlen szimbólum példányait jelöld ki
|
||||
@msg a symbol can't hold an instance of itself egy szimbólum nem tartalmazhatja a saját példányát
|
||||
@msg select an envelope jelölj ki egy burkolót
|
||||
@msg select an image jelölj ki egy képet
|
||||
@msg select an Image Trace object jelölj ki egy képvektorizálási objektumot
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2872,6 +2872,7 @@ theme Appearance Tampilan
|
||||
Duplicate Brush Duplikatkan kuas
|
||||
Duplicate Symbol Duplikatkan simbol
|
||||
Edit Symbol Edit simbol
|
||||
Exit Symbol Editing Mode Keluar dari mode edit simbol
|
||||
Expand Brush Strokes Perluas guratan kuas
|
||||
New Brush… Kuas baru…
|
||||
New Brush Kuas baru
|
||||
@@ -4323,6 +4324,8 @@ imageTrace Outline Kontur
|
||||
@msg select a repeat pilih pengulangan
|
||||
@msg select a stroke with a variable width pilih goresan dengan lebar variabel
|
||||
@msg select a symbol instance pilih instans simbol
|
||||
@msg select instances of one symbol pilih instans dari satu simbol saja
|
||||
@msg a symbol can't hold an instance of itself simbol tidak dapat memuat instans dirinya sendiri
|
||||
@msg select an Image Trace object pilih objek Image Trace
|
||||
@msg could not decode image: {0} tidak dapat mendekode gambar: {0}
|
||||
@msg image is empty gambar kosong
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2196 and column 5.
|
@@ -925,6 +925,7 @@ font-technology Variable Variabili
|
||||
Edit Selection Modifica selezione
|
||||
Edit Selection… Modifica selezione…
|
||||
Edit Symbol Modifica simbolo
|
||||
Exit Symbol Editing Mode Esci dalla modalità modifica simbolo
|
||||
Edit Text Modifica testo
|
||||
Edit the applied effect, or add another one? Modificare l'effetto applicato o aggiungerne un altro?
|
||||
Edit the point Modifica il punto
|
||||
@@ -4394,6 +4395,8 @@ axis Both Entrambi
|
||||
@msg a clipping path can't be attached to a path un tracciato di ritaglio non può essere agganciato a un tracciato
|
||||
@msg select a stroke with a variable width seleziona una traccia con larghezza variabile
|
||||
@msg select a symbol instance seleziona un'istanza di simbolo
|
||||
@msg select instances of one symbol seleziona istanze di un solo simbolo
|
||||
@msg a symbol can't hold an instance of itself un simbolo non può contenere un'istanza di sé stesso
|
||||
@msg select an Image Trace object seleziona un oggetto di ricalco immagine
|
||||
@msg could not decode image: {0} impossibile decodificare l'immagine: {0}
|
||||
@msg image is empty l'immagine è vuota
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1440,6 +1440,7 @@ digits Malayalam マラヤーラム数字
|
||||
Edit PDF Preset PDF プリセットを編集
|
||||
Edit Print Preset プリントプリセットを編集
|
||||
Edit Symbol シンボルを編集
|
||||
Exit Symbol Editing Mode シンボル編集モードを終了
|
||||
Edit Theme テーマを編集
|
||||
Edit selected paths within: 選択したパスを次の距離以内で編集:
|
||||
Edit or Apply Color Group カラーグループを編集または適用
|
||||
@@ -4034,6 +4035,8 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg brushes move only within their type ブラシは同じ種類の中でのみ移動できます
|
||||
@msg no symbol instances selected シンボルインスタンスが選択されていません
|
||||
@msg select a symbol instance シンボルインスタンスを選択してください
|
||||
@msg select instances of one symbol 1 つのシンボルのインスタンスを選択してください
|
||||
@msg a symbol can't hold an instance of itself シンボルにそれ自体のインスタンスを含めることはできません
|
||||
@msg the art has no bounds アートに境界がありません
|
||||
@msg Image Trace: the traced image is missing 画像トレース: トレース元の画像が見つかりません
|
||||
@msg Live Paint Merge: select a Live Paint group and the paths to add ライブペイントの結合: ライブペイントグループと、追加するパスを選択してください
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2685,6 +2685,7 @@ theme Appearance იერსახე
|
||||
Duplicate Brush ფუნჯის დუბლირება
|
||||
Duplicate Symbol სიმბოლოს დუბლირება
|
||||
Edit Symbol სიმბოლოს რედაქტირება
|
||||
Exit Symbol Editing Mode სიმბოლოს რედაქტირების რეჟიმიდან გამოსვლა
|
||||
Expand Brush Strokes ფუნჯის შტრიხების გაშლა
|
||||
New Brush… ახალი ფუნჯი…
|
||||
New Brush ახალი ფუნჯი
|
||||
@@ -4108,6 +4109,8 @@ font-technology Variable ცვლადი
|
||||
@msg select a repeat მონიშნეთ გამეორება
|
||||
@msg select a stroke with a variable width მონიშნეთ ცვლადი სიგანის კონტური
|
||||
@msg select a symbol instance მონიშნეთ სიმბოლოს ეგზემპლარი
|
||||
@msg select instances of one symbol მონიშნეთ ერთი სიმბოლოს ეგზემპლარები
|
||||
@msg a symbol can't hold an instance of itself სიმბოლო ვერ შეიცავს საკუთარ ეგზემპლარს
|
||||
@msg select an Image Trace object მონიშნეთ გამოსახულების ვექტორიზაციის ობიექტი
|
||||
@msg could not decode image: {0} გამოსახულების დეკოდირება ვერ მოხერხდა: {0}
|
||||
@msg image is empty გამოსახულება ცარიელია
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1430,6 +1430,7 @@ digits Malayalam 말라얄람어
|
||||
Edit PDF Preset PDF 프리셋 편집
|
||||
Edit Print Preset 인쇄 프리셋 편집
|
||||
Edit Symbol 심볼 편집
|
||||
Exit Symbol Editing Mode 심볼 편집 모드 종료
|
||||
Edit Theme 테마 편집
|
||||
Edit selected paths within: 다음 범위 안의 선택한 패스 편집:
|
||||
Edit or Apply Color Group 색상 그룹 편집 또는 적용
|
||||
@@ -3961,6 +3962,8 @@ theme Appearance 외관
|
||||
@msg brushes move only within their type 브러시는 같은 유형 안에서만 이동할 수 있습니다
|
||||
@msg no symbol instances selected 선택한 심볼 인스턴스가 없습니다
|
||||
@msg select a symbol instance 심볼 인스턴스를 선택하세요
|
||||
@msg select instances of one symbol 한 심볼의 인스턴스를 선택하세요
|
||||
@msg a symbol can't hold an instance of itself 심볼에는 자기 자신의 인스턴스를 넣을 수 없습니다
|
||||
@msg the art has no bounds 아트웍에 경계가 없습니다
|
||||
@msg Image Trace: the traced image is missing 이미지 추적: 추적한 이미지가 없습니다
|
||||
@msg Live Paint Merge: select a Live Paint group and the paths to add 라이브 페인트 병합: 라이브 페인트 그룹과 추가할 패스를 선택하세요
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2886,6 +2886,7 @@ theme Appearance Vormgeving
|
||||
Duplicate Brush Kwast dupliceren
|
||||
Duplicate Symbol Symbool dupliceren
|
||||
Edit Symbol Symbool bewerken
|
||||
Exit Symbol Editing Mode Symboolbewerkingsmodus verlaten
|
||||
Expand Brush Strokes Penseelstreken omzetten
|
||||
New Brush… Nieuw penseel…
|
||||
New Brush Nieuw penseel
|
||||
@@ -4335,6 +4336,8 @@ imageTrace Outline Contour
|
||||
@msg select a repeat selecteer een herhaling
|
||||
@msg select a stroke with a variable width selecteer een lijn met een variabele breedte
|
||||
@msg select a symbol instance selecteer een symboolinstantie
|
||||
@msg select instances of one symbol selecteer instanties van één symbool
|
||||
@msg a symbol can't hold an instance of itself een symbool kan geen instantie van zichzelf bevatten
|
||||
@msg select an Image Trace object selecteer een Image Trace-object
|
||||
@msg could not decode image: {0} afbeelding kon niet worden gedecodeerd: {0}
|
||||
@msg image is empty afbeelding is leeg
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2998,6 +2998,7 @@ theme Appearance Wygląd
|
||||
Duplicate Brush Powiel pędzel
|
||||
Duplicate Symbol Powiel symbol
|
||||
Edit Symbol Edytuj symbol
|
||||
Exit Symbol Editing Mode Wyjdź z trybu edycji symbolu
|
||||
Expand Brush Strokes Rozszerz pociągnięcia pędzla
|
||||
New Brush… Nowy pędzel…
|
||||
New Brush Nowy pędzel
|
||||
@@ -4484,6 +4485,8 @@ imageTrace Outline Kontur
|
||||
@msg a clipping path can't be attached to a path ścieżki przycinającej nie można dołączyć do ścieżki
|
||||
@msg select a stroke with a variable width zaznacz obrys o zmiennej szerokości
|
||||
@msg select a symbol instance zaznacz wystąpienie symbolu
|
||||
@msg select instances of one symbol zaznacz wystąpienia jednego symbolu
|
||||
@msg a symbol can't hold an instance of itself symbol nie może zawierać wystąpienia samego siebie
|
||||
@msg select an Image Trace object zaznacz obiekt obrysu obrazu
|
||||
@msg could not decode image: {0} nie udało się zdekodować obrazu: {0}
|
||||
@msg image is empty obraz jest pusty
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -815,6 +815,7 @@
|
||||
Edit selected paths within: Editar caminhos selecionados a menos de:
|
||||
Edit Selection… Editar Seleção…
|
||||
Edit Symbol Editar Símbolo
|
||||
Exit Symbol Editing Mode Sair do Modo de Edição de Símbolo
|
||||
Edit Text Editar Texto
|
||||
Edit Theme Editar Tema
|
||||
Edit Views… Editar Visualizações…
|
||||
@@ -3925,6 +3926,8 @@ axis Both Ambos
|
||||
@msg a clipping path can't be attached to a path um caminho de recorte não pode ser anexado a um caminho
|
||||
@msg select a stroke with a variable width selecione um traço com largura variável
|
||||
@msg select a symbol instance selecione uma instância de símbolo
|
||||
@msg select instances of one symbol selecione instâncias de um único símbolo
|
||||
@msg a symbol can't hold an instance of itself um símbolo não pode conter uma instância de si mesmo
|
||||
@msg select an Image Trace object selecione um objeto de Vetorização de imagem
|
||||
@msg could not decode image: {0} não foi possível decodificar a imagem: {0}
|
||||
@msg image is empty a imagem está vazia
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -923,6 +923,7 @@ font-technology Variable Вариативные
|
||||
Edit Selection Редактировать выделение
|
||||
Edit Selection… Редактировать выделение…
|
||||
Edit Symbol Редактировать символ
|
||||
Exit Symbol Editing Mode Выйти из режима редактирования символа
|
||||
Edit Text Редактировать текст
|
||||
Edit the applied effect, or add another one? Редактировать применённый эффект или добавить другой?
|
||||
Edit the point Редактировать точку
|
||||
@@ -4389,6 +4390,8 @@ axis Both Оба
|
||||
@msg a clipping path can't be attached to a path контур обрезки нельзя прикрепить к контуру
|
||||
@msg select a stroke with a variable width выделите обводку с переменной шириной
|
||||
@msg select a symbol instance выделите экземпляр символа
|
||||
@msg select instances of one symbol выделите экземпляры одного символа
|
||||
@msg a symbol can't hold an instance of itself символ не может содержать собственный экземпляр
|
||||
@msg select an Image Trace object выделите объект трассировки изображения
|
||||
@msg could not decode image: {0} не удалось декодировать изображение: {0}
|
||||
@msg image is empty изображение пусто
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -912,6 +912,7 @@ font-technology Variable Variabla
|
||||
Edit Selection Redigera markering
|
||||
Edit Selection… Redigera markering…
|
||||
Edit Symbol Redigera symbol
|
||||
Exit Symbol Editing Mode Avsluta symbolredigeringsläge
|
||||
Edit Text Redigera text
|
||||
Edit the applied effect, or add another one? Vill du redigera den använda effekten eller lägga till en till?
|
||||
Edit the point Redigera punkten
|
||||
@@ -4252,6 +4253,8 @@ axis Both Båda
|
||||
@msg select a repeat markera en upprepning
|
||||
@msg select a stroke with a variable width markera en kontur med variabel bredd
|
||||
@msg select a symbol instance markera en symbolinstans
|
||||
@msg select instances of one symbol markera instanser av en och samma symbol
|
||||
@msg a symbol can't hold an instance of itself en symbol kan inte innehålla en instans av sig själv
|
||||
@msg select an Image Trace object markera ett bildspårningsobjekt
|
||||
@msg could not decode image: {0} kunde inte avkoda bilden: {0}
|
||||
@msg image is empty bilden är tom
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -904,6 +904,7 @@ font-technology Variable Değişken
|
||||
Edit Selection Seçimi Düzenle
|
||||
Edit Selection… Seçimi Düzenle…
|
||||
Edit Symbol Sembolü Düzenle
|
||||
Exit Symbol Editing Mode Sembol Düzenleme Modundan Çık
|
||||
Edit Text Metni Düzenle
|
||||
Edit the applied effect, or add another one? Uygulanan efekti düzenlemek veya başka bir efekt eklemek mi istiyorsunuz?
|
||||
Edit the point Noktayı düzenle
|
||||
@@ -4105,6 +4106,8 @@ axis Both Her İkisi
|
||||
@msg select an Intertwine group bir Intertwine grubu seçin
|
||||
@msg select a stroke with a variable width değişken genişliğe sahip bir kontur seçin
|
||||
@msg select a symbol instance bir sembol örneği seçin
|
||||
@msg select instances of one symbol tek bir sembolün örneklerini seçin
|
||||
@msg a symbol can't hold an instance of itself bir sembol kendi örneğini içeremez
|
||||
@msg select an Image Trace object bir Görüntü İzleme nesnesi seçin
|
||||
@msg could not decode image: {0} görüntünün kodu çözülemedi: {0}
|
||||
@msg image is empty resim boş
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -919,6 +919,7 @@ font-technology Variable Варіативні
|
||||
Edit Selection Редагувати виділення
|
||||
Edit Selection… Редагувати виділення…
|
||||
Edit Symbol Редагувати символ
|
||||
Exit Symbol Editing Mode Вийти з режиму редагування символу
|
||||
Edit Text Редагувати текст
|
||||
Edit the applied effect, or add another one? Редагувати застосований ефект або додати інший?
|
||||
Edit the point Редагувати точку
|
||||
@@ -4378,6 +4379,8 @@ axis Both Обидві
|
||||
@msg a clipping path can't be attached to a path відсічний контур не можна прикріпити до контуру
|
||||
@msg select a stroke with a variable width виділіть обведення зі змінною шириною
|
||||
@msg select a symbol instance виділіть екземпляр символу
|
||||
@msg select instances of one symbol виділіть екземпляри одного символу
|
||||
@msg a symbol can't hold an instance of itself символ не може містити власний екземпляр
|
||||
@msg select an Image Trace object виділіть об’єкт трасування зображення
|
||||
@msg could not decode image: {0} не вдалося декодувати зображення: {0}
|
||||
@msg image is empty зображення порожнє
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 178 and column 5.
|
@@ -1075,6 +1075,7 @@ font-technology Variable Biến thiên
|
||||
Edit Selection Chỉnh sửa vùng chọn
|
||||
Edit Selection… Chỉnh sửa vùng chọn…
|
||||
Edit Symbol Chỉnh sửa ký hiệu
|
||||
Exit Symbol Editing Mode Thoát chế độ chỉnh sửa ký hiệu
|
||||
Edit Text Chỉnh sửa văn bản
|
||||
Edit the applied effect, or add another one? Chỉnh sửa hiệu ứng đã áp dụng, hay thêm hiệu ứng mới?
|
||||
Edit the point Chỉnh sửa điểm
|
||||
@@ -4730,6 +4731,8 @@ theme Appearance Giao diện
|
||||
@msg a clipping path can't be attached to a path không thể gắn đường dẫn cắt lên đường dẫn
|
||||
@msg select a stroke with a variable width hãy chọn nét có chiều rộng biến đổi
|
||||
@msg select a symbol instance hãy chọn đối tượng ký hiệu
|
||||
@msg select instances of one symbol hãy chọn các đối tượng của cùng một ký hiệu
|
||||
@msg a symbol can't hold an instance of itself ký hiệu không thể chứa đối tượng của chính nó
|
||||
@msg select an envelope hãy chọn một Envelope
|
||||
@msg select an image hãy chọn ảnh
|
||||
@msg select an Image Trace object hãy chọn đối tượng vector hóa ảnh
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 200 and column 5.
|
@@ -865,6 +865,7 @@
|
||||
Edit selected paths within: 在以下范围内编辑所选路径:
|
||||
Edit Selection… 编辑所选对象…
|
||||
Edit Symbol 编辑符号
|
||||
Exit Symbol Editing Mode 退出符号编辑模式
|
||||
Edit Text 编辑文字
|
||||
Edit the applied effect, or add another one? 编辑已应用的效果,还是再添加一个?
|
||||
Edit the point 编辑点
|
||||
@@ -3771,6 +3772,8 @@ imageTrace Outline 轮廓
|
||||
@msg brushes move only within their type 画笔只能在同一类型内移动
|
||||
@msg no symbol instances selected 未选择符号实例
|
||||
@msg select a symbol instance 请选择一个符号实例
|
||||
@msg select instances of one symbol 请选择同一符号的实例
|
||||
@msg a symbol can't hold an instance of itself 符号不能包含自身的实例
|
||||
@msg the art has no bounds 图稿没有边界
|
||||
@msg Image Trace: the traced image is missing 图像描摹:缺少所描摹的图像
|
||||
@msg Live Paint Merge: select a Live Paint group and the paths to add 实时上色合并:请选择一个实时上色组以及要添加的路径
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -887,6 +887,7 @@ font-technology Variable 可變
|
||||
Edit Selection 編輯選取範圍
|
||||
Edit Selection… 編輯選取範圍…
|
||||
Edit Symbol 編輯符號
|
||||
Exit Symbol Editing Mode 結束符號編輯模式
|
||||
Edit Text 編輯文字
|
||||
Edit the applied effect, or add another one? 要編輯已套用的效果,還是新增另一個?
|
||||
Edit the point 編輯點
|
||||
@@ -1696,6 +1697,8 @@ file-filter Templates 範本
|
||||
@msg brushes move only within their type 筆刷只能在同一類型內移動
|
||||
@msg no symbol instances selected 未選取符號實體
|
||||
@msg select a symbol instance 請選取符號實體
|
||||
@msg select instances of one symbol 請選取同一符號的實體
|
||||
@msg a symbol can't hold an instance of itself 符號不能包含自身的實體
|
||||
@msg the art has no bounds 圖稿沒有邊界
|
||||
@msg Image Trace: the traced image is missing 影像描圖:找不到描圖的影像
|
||||
@msg Live Paint Merge: select a Live Paint group and the paths to add 即時上色合併:請選取即時上色群組和要加入的路徑
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -184,6 +184,13 @@ pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let names: Vec<String> = st.doc.symbols.iter().map(|s| s.name.clone()).collect();
|
||||
let shared = symbols.first().filter(|first| symbols.iter().all(|s| s == *first)).cloned().unwrap_or_default();
|
||||
// Edit Symbol: the instances' symbol in symbol editing mode, when they share one.
|
||||
if !shared.is_empty()
|
||||
&& widgets::icon_button(ui, "dc-place-symbol", tl!("Edit Symbol"), false, 24.0).clicked()
|
||||
&& let Err(e) = app.run("symbol.edit", json!({}))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
ui.label(egui::RichText::new(tl!("Replace:")).size(12.0).color(Tokens::get(ui.ctx()).text));
|
||||
let options: Vec<&str> = names.iter().map(String::as_str).collect();
|
||||
if let Some(name) = widgets::dropdown_names(ui, "cb-symbol-replace", &shared, &options, 140.0).and_then(|i| names.get(i))
|
||||
@@ -261,12 +268,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
if let Some((n, place)) = clicked {
|
||||
// A double-click edits the symbol in symbol editing mode.
|
||||
if let Some((n, edit)) = clicked {
|
||||
// A click picks one symbol again.
|
||||
set_pstate(ui.ctx(), MARKED, (uid, Vec::<String>::new()));
|
||||
app.run("symbol.setCurrent", json!({"name": n})).ok();
|
||||
if place {
|
||||
app.run("symbol.place", json!({"name": n})).ok();
|
||||
if edit && let Err(e) = app.run("symbol.edit", json!({"name": n})) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty());
|
||||
@@ -322,7 +330,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
let items: [(&str, bool, &str, Value); 7] = [
|
||||
(tl!("Symbol Options…"), sel.is_some(), "ui.symbolOptions", json!({"edit": true, "name": sel})),
|
||||
(tl!("Edit Symbol"), inst, "symbol.edit", json!({})),
|
||||
// The selected instance, else the symbol selected in the panel.
|
||||
(tl!("Edit Symbol"), inst || sel.is_some(), "symbol.edit", if inst { json!({}) } else { name.clone() }),
|
||||
(tl!("Place Symbol Instance"), sel.is_some(), "symbol.place", name.clone()),
|
||||
(tl!("Replace Symbol"), inst && sel.is_some(), "symbol.replace", name.clone()),
|
||||
(tl!("Break Link to Symbol"), inst, "symbol.breakLink", json!({})),
|
||||
@@ -925,6 +934,51 @@ mod tests {
|
||||
(instances, paths)
|
||||
}
|
||||
|
||||
/// The bounds of the art on the symbol editing layer, rounded to 0.001 pt.
|
||||
fn editing_art(app: &VectorcraftApp) -> Option<[f64; 4]> {
|
||||
let d = &app.session.active()?.doc;
|
||||
let layer = d.node(d.symbol_edit.as_ref()?.layer)?;
|
||||
let ids: Vec<NodeId> = layer.children()?.iter().map(|n| n.id).collect();
|
||||
let r = d.bounds_of(&ids, false)?;
|
||||
Some([r.x0, r.y0, r.x1, r.y1].map(|v| (v * 1000.0).round() / 1000.0))
|
||||
}
|
||||
|
||||
/// Edit Symbol without an instance centers the symbol's art on the active artboard, and in a
|
||||
/// document without artboards on the view's center, in a batch too.
|
||||
#[test]
|
||||
fn edit_symbol_from_the_panel_centers_the_art_on_the_active_artboard_or_the_view() {
|
||||
let mut app = leaf_and_dot();
|
||||
app.run("artboard.new", json!({"x": 500, "y": 0, "width": 200, "height": 100})).unwrap();
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
crate::panels::artboards::select(&mut app, 1);
|
||||
app.run("symbol.edit", json!({"name": "Dot"})).unwrap();
|
||||
assert_eq!(editing_art(&app), Some([585.0, 35.0, 615.0, 65.0]));
|
||||
app.run("symbol.exitEditing", json!({})).unwrap();
|
||||
std::sync::Arc::make_mut(&mut app.session.active_mut().unwrap().doc).artboards.clear();
|
||||
// A view with no artboard to fit to is never fitted.
|
||||
if let Some(v) = app.view_mut() {
|
||||
(v.fitted, v.center) = (false, vectorcraft_geom::Point::new(1000.0, 1000.0));
|
||||
}
|
||||
app.run("symbol.edit", json!({"name": "Dot"})).unwrap();
|
||||
assert_eq!(editing_art(&app), Some([985.0, 985.0, 1015.0, 1015.0]));
|
||||
// A coordinate the call gives stays; the other is the view's.
|
||||
app.run("symbol.exitEditing", json!({})).unwrap();
|
||||
app.run("symbol.edit", json!({"name": "Dot", "x": 2000})).unwrap();
|
||||
assert_eq!(editing_art(&app), Some([1985.0, 985.0, 2015.0, 1015.0]));
|
||||
// A batch's symbol.edit step gets the view's center as well.
|
||||
app.run("symbol.exitEditing", json!({})).unwrap();
|
||||
app.run("command.batch", json!({"commands": [{"command": "symbol.edit", "params": {"name": "Dot"}}]})).unwrap();
|
||||
assert_eq!(editing_art(&app), Some([985.0, 985.0, 1015.0, 1015.0]));
|
||||
// An artboard an earlier step makes is there when the symbol.edit step runs.
|
||||
app.run("symbol.exitEditing", json!({})).unwrap();
|
||||
let steps = json!([
|
||||
{"command": "artboard.new", "params": {"x": 500, "y": 0, "width": 200, "height": 100}},
|
||||
{"command": "symbol.edit", "params": {"name": "Dot"}},
|
||||
]);
|
||||
app.run("command.batch", json!({ "commands": steps })).unwrap();
|
||||
assert_eq!(editing_art(&app), Some([585.0, 35.0, 615.0, 65.0]));
|
||||
}
|
||||
|
||||
fn dialog_kind(app: &VectorcraftApp) -> Option<&str> {
|
||||
app.ui.dialog.as_ref().map(|d| d.kind.as_str())
|
||||
}
|
||||
|
||||
+8
-3
@@ -1568,7 +1568,12 @@ already walks Image Trace. Short agent entry points:
|
||||
9-slice guides on or off; `nineSliceGuides` is `[left, top, right, bottom]`, fractions 0..1 of the symbol size; rows
|
||||
of `symbol.list` carry `nineSlice`: the guides or `null`; `type` is Symbol Type, `static` by default for the command
|
||||
and `dynamic` by default in the Symbol Options dialog),
|
||||
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `symbol.replace`, `symbol.edit`, `symbol.duplicate`,
|
||||
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `symbol.replace`,
|
||||
`symbol.edit {id?, name?, x?, y?, artboard?, viewCenter?}` (symbol editing mode: the symbol's art on a temporary
|
||||
isolated layer, where the instance `id` shows it or centered on (x, y), by default on the center of artboard
|
||||
`artboard`, the active one the app passes, or in a document without artboards on `viewCenter`, the view's center the
|
||||
app passes; every instance follows each edit and stays linked), `symbol.exitEditing`
|
||||
(leaves the mode; without an edit it adds no undo step), `symbol.duplicate`,
|
||||
`symbol.selectInstances {name}`, `symbol.delete {name | names, expandInstances?}` (`names` deletes several in one
|
||||
undo step). `symbol.new` and `symbol.duplicate {name, newName?}` trim the white space around the
|
||||
name they are given, as `symbol.options` trims `newName`; a blank one gives "New Symbol" or "<name> copy".
|
||||
@@ -1580,8 +1585,8 @@ already walks Image Trace. Short agent entry points:
|
||||
0..100 and `blend` a blend mode name; `null` gives one back to the symbol. It is refused for a static symbol and for
|
||||
a path that names no object. `symbol.instance.reset {ids?}` drops the selected instances' changes (the Properties
|
||||
panel's Reset). Redefining the symbol (`symbol.update`) reshapes every instance and keeps each change whose path the
|
||||
new art still has; `symbol.breakLink` bakes the changes into the expanded art; `symbol.edit` expands the symbol's own
|
||||
art; making the symbol static drops them.
|
||||
new art still has; `symbol.breakLink` bakes the changes into the expanded art; `symbol.edit` edits the symbol's own
|
||||
art, and each edit there keeps the changes whose path the art still has; making the symbol static drops them.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"symbol.instance.setAppearance","params":{"id":42,"path":[1],"fill":"#d0021b","opacity":80}}}
|
||||
|
||||
@@ -606,6 +606,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- macOS: Finder opens every format File › Open reads with VectorCraft (double-click, Open With, a drop on the Dock icon, `open -a`), at launch or while it runs; the release and `cargo xtask bundle` apps share one `Info.plist` that declares those document types, owning only VectorCraft's own formats. The DMG opens on a designed Finder window, VectorCraft.app beside the `Applications` link over a background with the app icon (#493).
|
||||
- Swatch and graphic style libraries, Libraries panel files, presets and plug-ins open wherever they are dropped on the window, as File › Open opens them.
|
||||
- **Symbol libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own, as swatch and graphic style libraries do. The built-in library, Basic Shapes, is original art generated in code (`crates/engine/src/cmd/symbollib.rs`). A click on a symbol adds it to the document's Symbols as one undo step, with its Symbol Options (size, registration, export type, 9-slice guides) and every resource it uses (the symbols it nests, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; a symbol the document already has is not added again and another symbol of the same name keeps its name, the library's comes in numbered. Thumbnails, in the library panels and the Symbols panel, draw a symbol with the symbols it nests and the patterns and images it uses. Dragging a library symbol onto the canvas also places an instance there, in the same step ([placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)). Save Symbols (the library menu) and Save Symbol Library… (the Symbols panel menu) write the document's symbols as a `.vectorcraft` document holding only them, into the user library folder (listed under User Defined) or a file; Other Library… reads the symbols of any document VectorCraft opens; a file it can't read is an error and opens no panel. Tests: `crates/engine/src/tests/symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`, `panels::symbols::tests::thumbnails_show_the_symbols_a_symbol_nests`, `canvas::tests::a_library_symbol_dropped_on_the_canvas_is_added_and_placed_there_in_one_step`.
|
||||
- **Symbol editing mode** ([Edit symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html), [Symbols panel options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): a double-click on a symbol instance, the Control bar's Edit Symbol button, a double-click on a symbol in the Symbols panel and the panel menu's Edit Symbol (`symbol.edit`) open the symbol's art on a temporary isolated layer, where the instance shows it (from the panel: centered on the active artboard, or in the view in a document without artboards), with the rest of the document dimmed. Each edit there changes every instance at once; the instances keep their own transforms and stay linked (Break Link to Symbol still expands one), and a dynamic symbol's instances keep their appearance changes to the objects the art still has. Escape, the isolation bar's back button, the Control bar's Exit Symbol Editing Mode and a double-click outside the art leave the mode (`symbol.exitEditing`); leaving without an edit adds no undo step. Saving leaves the editing layer out. Tests: `crates/engine/src/tests/symboledit.rs`, `select::tests::a_double_click_on_a_symbol_instance_edits_its_symbol`, `panels::symbols::tests::edit_symbol_from_the_panel_centers_the_art_on_the_active_artboard_or_the_view`.
|
||||
- **Delete Symbol asks first** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), [symbol libraries](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html), [Alt-click Delete](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): the Symbols panel's Delete Symbol button and menu item ask "Delete the selected symbol?" with Yes and No for a symbol no instance uses. For a symbol with placed instances they ask with Expand Instances (the instances become ordinary art), Delete Instances and Cancel; either Instances button deletes the symbol in one undo step. Alt-click on the button deletes an unused symbol without asking and still asks about one with instances. A question left open while another document became active deletes nothing. `symbol.delete` itself does not ask. Tests: `panels::symbols::tests::delete_symbol_asks_first_and_alt_click_deletes_an_unused_symbol_at_once`, `the_in_use_question_expands_or_deletes_the_instances`, `a_stale_delete_symbol_question_deletes_nothing`, `the_delete_symbol_questions_show_their_buttons`.
|
||||
- **Symbols › Select All Unused** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), #4918): the Symbols panel menu's Select All Unused shows the symbols nothing places selected (`symbol.unused`): instances in the art (opacity masks included), graph designs, pattern tiles and brush art count, and so do instances in a used symbol's art. Delete Symbol, the button or the menu item, then asks "Delete the selected symbols?" once and deletes those still unused in one undo step (`symbol.delete {names}`); Alt-click on the button deletes them without asking, and a click on a symbol picks just it again. Tests: `tests::brushsym::symbol_unused_lists_the_symbols_nothing_places_and_delete_takes_several`, `panels::symbols::tests::select_all_unused_picks_the_unused_symbols_and_delete_deletes_them`, `dialogs::symbol_delete::tests::several_symbols_get_the_plural_question`.
|
||||
- **Brush libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html), [Brushes panel](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/brushes-panel-overview.html)): Window › Brush Libraries, the Brushes panel's Brush Libraries Menu and its menu's Open Brush Library open each library in a library panel of its own, as swatch, graphic style and symbol libraries do. The built-in libraries (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) are original brushes generated in code (`crates/engine/src/cmd/brushlib.rs`); Striped Ribbon's art is filled with the library's own pattern and Sparkles scatters the library's own symbol. A click on a brush adds it to the document's Brushes as one undo step with every resource its art uses (patterns, symbols, swatches, graphic styles, images), makes it the current brush and, with paths selected, applies it to them in the same step; a brush the document already has is not added again and a different brush of the same name comes in numbered ("Wave 2"). Shift/Cmd-clicks select brushes for Add to Brushes; a brush dragged onto a path is added and applied to it, and one dragged onto the Brushes panel is added. Save Brushes… / the Brushes panel's Save Brush Library… write the document's brushes (or the selected ones) as a `.vectorcraft` library into the `Brushes` folder next to the preferences, listed under User Defined, or to a file; Other Library… reads the brushes of any document VectorCraft opens. Thumbnails, in the library panels, the Brushes panel and Brush Options, render brush art with the patterns and symbols it uses. Copying brushed art now carries the patterns and symbols its brush's art uses too. Tests: `crates/engine/src/tests/brushlib.rs`, `crates/ui-egui/src/panels/tests_brush_libraries.rs`, `panels::brushes::tests::library_brush_previews_show_the_patterns_their_art_uses`, `canvas::tests::a_library_brush_dropped_on_a_path_is_added_and_applied_in_one_step`.
|
||||
|
||||
Reference in New Issue
Block a user