mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:40:14 -06:00
Symbols: a refused switch to another symbol keeps the current edit, and isolation stays on the editing layer
symbol.edit checks the new placement before it ends the symbol being edited, so a call with an out-of-range x or y fails and leaves the current edit open. While a symbol is edited, object.isolate refuses art outside the editing layer, and object.exitIsolation goes back to the editing layer only from a group inside it; from any other isolation it leaves symbol editing mode.
This commit is contained in:
@@ -315,6 +315,12 @@ pub(super) fn isolate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if guides_locked && crate::is_guide_path(&st.doc, isolation) {
|
||||
return Err(EngineError::Other("guides are locked".into()));
|
||||
}
|
||||
// While a symbol is edited, only its art on the editing layer is isolated.
|
||||
if let Some(layer) = st.doc.symbol_edit.as_ref().map(|e| e.layer)
|
||||
&& !st.doc.ancestry(isolation).is_some_and(|a| a.contains(&layer))
|
||||
{
|
||||
return Err(bad("object.isolate", "the object is outside the isolated group"));
|
||||
}
|
||||
(isolation, st.isolation)
|
||||
};
|
||||
if let Some(prev) = prev
|
||||
@@ -339,7 +345,7 @@ pub(super) fn exit_isolation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// 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)
|
||||
&& let Some(inner) = st.isolation.filter(|i| *i != layer && st.doc.ancestry(*i).is_some_and(|a| a.contains(&layer)))
|
||||
{
|
||||
crate::cmd::settle_interaction(s)?;
|
||||
let st = s.doc_mut()?;
|
||||
|
||||
@@ -112,11 +112,20 @@ pub(crate) fn enter(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
};
|
||||
// Editing another symbol (one from the Symbols panel, or through the API): the current edit
|
||||
// ends first.
|
||||
// ends first, once the new placement is known to be valid, so a refused switch keeps it open.
|
||||
if st.doc.symbol_edit.is_some() {
|
||||
if instance.is_some_and(|i| st.doc.symbol_edit.as_ref().is_some_and(|e| st.doc.ancestry(i).unwrap_or_default().contains(&e.layer))) {
|
||||
return Err(bad(C, "exit isolation mode first"));
|
||||
}
|
||||
let mut trial = Document::clone(&st.doc);
|
||||
let mut sel = Selection::default();
|
||||
begin(&mut trial, &mut sel, &name, xf, instance)?;
|
||||
if trial.too_deep(Some(&st.doc)) {
|
||||
return Err(EngineError::Other(crate::TOO_DEEP.into()));
|
||||
}
|
||||
if !crate::sanity::doc_sane(&trial, &sel) {
|
||||
return Err(EngineError::Other("result would exceed the canvas (coordinates out of range)".into()));
|
||||
}
|
||||
leave(s, &json!({}))?;
|
||||
}
|
||||
if let Some(i) = s.doc()?.isolation {
|
||||
|
||||
@@ -372,6 +372,41 @@ fn other_editing_modes_and_symbol_editing_do_not_overlap() {
|
||||
assert_eq!(s.doc().unwrap().doc.layers.iter().filter(|l| l.name.as_deref() == Some("Box")).count(), 1);
|
||||
}
|
||||
|
||||
/// Editing another symbol at a place off the canvas fails and keeps the current edit open.
|
||||
#[test]
|
||||
fn a_switch_to_another_symbol_that_fails_keeps_the_current_edit() {
|
||||
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"}));
|
||||
let layer = NodeId(run(&mut s, "symbol.edit", json!({"id": first.0}))["layer"].as_u64().unwrap());
|
||||
let e = s.execute("symbol.edit", &json!({"name": "Dot", "x": 1e300})).unwrap_err().to_string();
|
||||
assert!(e.contains("exceed the canvas"), "{e}");
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.doc.symbol_edit.as_ref().map(|e| (e.symbol.as_str(), e.layer)), Some(("Box", layer)));
|
||||
assert_eq!((st.isolation, st.active_layer), (Some(layer), Some(layer)));
|
||||
assert!(st.doc.node(layer).is_some());
|
||||
}
|
||||
|
||||
/// While a symbol is edited, art outside the editing layer is not isolated, so leaving isolation
|
||||
/// mode leaves symbol editing mode.
|
||||
#[test]
|
||||
fn art_outside_the_editing_layer_is_not_isolated_while_a_symbol_is_edited() {
|
||||
let (mut s, first, _) = 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": [first.0]}));
|
||||
let layer = NodeId(run(&mut s, "symbol.edit", json!({"id": first.0}))["layer"].as_u64().unwrap());
|
||||
let e = s.execute("object.isolate", &json!({"id": other.0})).unwrap_err().to_string();
|
||||
assert!(e.contains("outside the isolated group"), "{e}");
|
||||
assert_eq!(s.doc().unwrap().isolation, Some(layer));
|
||||
// Isolation that names art outside the layer (a stale state) still leaves the mode.
|
||||
s.doc_mut().unwrap().isolation = Some(other);
|
||||
run(&mut s, "object.exitIsolation", 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.selection.objects, vec![first]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renaming_deleting_and_saving_while_a_symbol_is_edited() {
|
||||
let (mut s, first, _) = two_instances();
|
||||
|
||||
+4
-3
@@ -1575,7 +1575,8 @@ already walks Image Trace. Short agent entry points:
|
||||
`symbol.edit {id?, ids?, 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`
|
||||
app passes; every instance follows each edit and stays linked; while another symbol is edited, that edit ends first,
|
||||
and a call that fails leaves it open), `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
|
||||
@@ -2425,8 +2426,8 @@ and `layerRows`, the rows highlighted in the panel.
|
||||
Preview All Layers when the others are in Outline already), `layer.lockOthers` / `layer.unlockAll` (Alt-clicking an eye or a lock
|
||||
runs Hide or Lock Others for the row's layer, or Show or Unlock All when the others already are), `layer.pasteRemembersLayers`,
|
||||
`object.isolate {id?}` (a group, a layer or a single object, by default the first selected object; not type, not a
|
||||
hidden or locked object or one in a hidden or locked group or layer, and not a guide path while Lock Guides is on) /
|
||||
`object.exitIsolation`.
|
||||
hidden or locked object or one in a hidden or locked group or layer, not a guide path while Lock Guides is on, and
|
||||
while a symbol is edited only the editing layer or art on it) / `object.exitIsolation`.
|
||||
- **Dialogs and panel state** (control channel and the app's MCP): `ui.layerOptions {ids?}` and `ui.newLayer
|
||||
{sublayer?}` open Layer Options (dialog `layerOptions`: name, color, template, locked, visible, printable, preview,
|
||||
dimImages, dimPercent; OK is one undo step; Cmd/Ctrl+Alt+L runs `ui.newLayer` for a new layer), `ui.layersPanelOptions` opens Panel Options (`layersPanelOptions`:
|
||||
|
||||
Reference in New Issue
Block a user