mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Guides: Shift selects ruler guides and art together
A Shift-click on a guide adds it to the selected art (guide.select toggle keeps the art), and Shift-adding or -removing art keeps the selected guides; a new selection still clears them. With both selected, Delete deletes the art and the guides in one undo step (not when ids are given). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
This commit is contained in:
1 parent
a9724275cc
commit
a22bced150
4 files changed
+66
-13
No files matched your search
@@ -29,8 +29,9 @@ pub struct Selection {
|
||||
/// clears them.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub slices: Vec<NodeId>,
|
||||
/// Ruler guides selected with a selection tool, as indexes into [`Document::guides`]. They are
|
||||
/// selected on their own: any change to the object selection clears them.
|
||||
/// Ruler guides selected with a selection tool, as indexes into [`Document::guides`]. A new
|
||||
/// selection ([`Self::set`]) clears them; Shift-adding or -removing objects keeps them, so
|
||||
/// guides and objects can be selected together, in either order.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub guides: Vec<usize>,
|
||||
}
|
||||
@@ -66,7 +67,6 @@ impl Selection {
|
||||
pub fn add(&mut self, id: NodeId) {
|
||||
self.target = None;
|
||||
self.slices.clear();
|
||||
self.guides.clear();
|
||||
if !self.objects.contains(&id) {
|
||||
self.objects.push(id);
|
||||
}
|
||||
@@ -74,7 +74,6 @@ impl Selection {
|
||||
pub fn remove(&mut self, id: NodeId) {
|
||||
self.target = None;
|
||||
self.slices.clear();
|
||||
self.guides.clear();
|
||||
self.objects.retain(|x| *x != id);
|
||||
self.anchors.remove(&id);
|
||||
if self.key == Some(id) {
|
||||
@@ -99,6 +98,15 @@ impl Selection {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Add ruler guide `i` to the selection, or take it out, keeping the selected objects (a
|
||||
/// Shift-click on a guide).
|
||||
pub fn toggle_guide(&mut self, i: usize) {
|
||||
if let Some(k) = self.guides.iter().position(|g| *g == i) {
|
||||
self.guides.remove(k);
|
||||
} else {
|
||||
self.guides.push(i);
|
||||
}
|
||||
}
|
||||
pub fn toggle(&mut self, id: NodeId) {
|
||||
if self.contains(id) { self.remove(id) } else { self.add(id) }
|
||||
}
|
||||
@@ -180,8 +188,16 @@ mod tests {
|
||||
s.set_guides([2, 0, 2]);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![], vec![2, 0]));
|
||||
assert!(s.has_objects_or_guides() && s.is_empty());
|
||||
// Shift-adding an object keeps them; a new selection doesn't.
|
||||
s.add(NodeId(1));
|
||||
assert_eq!(s.guides, vec![2, 0]);
|
||||
s.set([NodeId(1)]);
|
||||
assert!(s.guides.is_empty());
|
||||
// Toggled, a guide joins (or leaves) the selected objects.
|
||||
s.toggle_guide(0);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![0]));
|
||||
s.toggle_guide(0);
|
||||
assert_eq!((s.objects.clone(), s.guides.clone()), (vec![NodeId(1)], vec![]));
|
||||
// Pruning drops guides the document no longer has.
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
d.guides.push(crate::Guide::new(true, 10.0));
|
||||
|
||||
@@ -178,7 +178,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Select Guides",
|
||||
[],
|
||||
None,
|
||||
"{indexes: [index…], toggle?: bool} select ruler guides on their own (deselecting the art); toggle adds or removes them instead → {selected: [index…]}",
|
||||
"{indexes: [index…], toggle?: bool} select ruler guides on their own (deselecting the art); toggle adds or removes them instead, keeping the selected art (a Shift-click on a guide) → {selected: [index…]}",
|
||||
guides_unlocked,
|
||||
guide_select
|
||||
),
|
||||
@@ -539,15 +539,12 @@ fn guide_select(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let list = p.get("indexes").and_then(Value::as_array).ok_or_else(|| bad("guide.select", "missing indexes"))?;
|
||||
let picked = list.iter().map(|v| guide_index(s, Some(v), "guide.select")).collect::<Result<Vec<_>>>()?;
|
||||
if bool_or(p, "toggle", false) {
|
||||
let mut now = s.doc()?.selection.guides.clone();
|
||||
for i in picked {
|
||||
if let Some(k) = now.iter().position(|g| *g == i) {
|
||||
now.remove(k);
|
||||
} else {
|
||||
now.push(i);
|
||||
// Added to (or taken from) what is selected, the art included.
|
||||
s.select(|_, sel| {
|
||||
for i in picked {
|
||||
sel.toggle_guide(i);
|
||||
}
|
||||
}
|
||||
s.select(|_, sel| sel.set_guides(now))?;
|
||||
})?;
|
||||
} else {
|
||||
s.select(|_, sel| sel.set_guides(picked))?;
|
||||
}
|
||||
|
||||
@@ -268,6 +268,8 @@ fn clear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return super::docmenu::guide_remove(s, &json!({}));
|
||||
}
|
||||
let st = s.doc()?;
|
||||
// Guides selected with the art go with it (not when ids are given).
|
||||
let with_guides = explicit.is_none();
|
||||
let targets = explicit.unwrap_or_else(|| st.selection.in_paint_order(&st.doc));
|
||||
// Keep the selected ids themselves, without promoting compound members. Delete a selected
|
||||
// ancestor just once; its selected descendants leave with it. Layers themselves are kept.
|
||||
@@ -284,6 +286,10 @@ fn clear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
for id in &ids {
|
||||
d.remove(*id)?;
|
||||
}
|
||||
if with_guides {
|
||||
let gone = sel.guides.clone();
|
||||
d.retain_guides(sel, |i, _| !gone.contains(&i));
|
||||
}
|
||||
sel.clear();
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
@@ -823,6 +823,40 @@ fn ruler_guides_select_move_and_delete() {
|
||||
assert!(matches!(s.execute("edit.clear", &json!({})), Err(EngineError::Disabled(..))));
|
||||
}
|
||||
|
||||
/// Shift selects ruler guides and art together, in either order; Delete takes both in one step.
|
||||
#[test]
|
||||
fn guides_and_art_are_shift_selected_together() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0, 10.0, 40.0, 20.0);
|
||||
let b = rect(&mut s, 70.0, 50.0, 60.0, 30.0);
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 300})).unwrap();
|
||||
// Art first, then a Shift-click on a guide: the art stays selected.
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("guide.select", &json!({"indexes": [0], "toggle": true})).unwrap();
|
||||
assert_eq!((selected(&s), selected_guides(&s)), (vec![a], vec![0]));
|
||||
// A guide first, then Shift-added art: the guide stays selected.
|
||||
s.execute("guide.select", &json!({"indexes": [1]})).unwrap();
|
||||
s.execute("select.add", &json!({"ids": [b.0]})).unwrap();
|
||||
assert_eq!((selected(&s), selected_guides(&s)), (vec![b], vec![1]));
|
||||
// A new selection drops them.
|
||||
sel(&mut s, &[a]);
|
||||
assert!(selected_guides(&s).is_empty());
|
||||
// Delete takes the art and the guides, in one step; undo brings both back.
|
||||
s.execute("guide.select", &json!({"indexes": [1], "toggle": true})).unwrap();
|
||||
let n = undo_len(&s);
|
||||
s.execute("edit.clear", &json!({})).unwrap();
|
||||
assert_eq!((guides(&s), undo_len(&s)), (vec![(true, 200.0)], n + 1));
|
||||
assert!(s.doc().unwrap().doc.node(a).is_none() && s.doc().unwrap().doc.node(b).is_some());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(guides(&s).len(), 2);
|
||||
// Ids given: only those go, the guides stay.
|
||||
s.execute("guide.select", &json!({"indexes": [0]})).unwrap();
|
||||
s.execute("select.add", &json!({"ids": [b.0]})).unwrap();
|
||||
s.execute("edit.clear", &json!({"ids": [b.0]})).unwrap();
|
||||
assert_eq!(guides(&s).len(), 2);
|
||||
}
|
||||
|
||||
/// #451: an artboard guide (`guide.add {artboard}`) runs across its artboard only, and is moved,
|
||||
/// copied and deleted with it; canvas guides stay put.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user