mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
Fix #346: Artboard tool: Alt-drag duplicates the artboard
Alt-dragging an artboard moved it like a plain drag. artboard.move now takes copy: it leaves the artboard (and the art it carries) where it is and moves a copy, named "<name> copy", with copies of that art. The Artboard tool previews it while Alt is held, as "Duplicate Artboard" (Alt pressed or released mid-drag switches between copy and move), and makes the copy the active artboard, as in the reference app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
3b2376af9c
commit
41c7495844
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Move Artboard",
|
||||
[],
|
||||
None,
|
||||
"{index, dx, dy, moveArt?: bool} move an artboard (and the unlocked art fully inside it)",
|
||||
"{index, dx, dy, moveArt?: bool, copy?: bool} move an artboard (and the unlocked art fully inside it); copy leaves them and moves a duplicate → {index}",
|
||||
has_doc,
|
||||
artboard_move
|
||||
),
|
||||
@@ -399,15 +399,33 @@ fn artboard_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
let scale_strokes = s.prefs.scale_strokes;
|
||||
s.edit("Move Artboard", |d, _| {
|
||||
let a = d.artboards.get_mut(i).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
a.rect = a.rect + dv;
|
||||
let copy = bool_or(p, "copy", false);
|
||||
let index = s.edit(if copy { "Duplicate Artboard" } else { "Move Artboard" }, |d, _| {
|
||||
let src = d.artboards.get(i).cloned().ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
// A copy keeps the artboard (and its art) where they are and moves duplicates instead.
|
||||
let target = if copy {
|
||||
let mut a = src.clone();
|
||||
a.id = d.artboards.iter().map(|a| a.id).max().unwrap_or(0) + 1;
|
||||
a.name = format!("{} copy", src.name);
|
||||
d.artboards.push(a);
|
||||
d.artboards.len() - 1
|
||||
} else {
|
||||
i
|
||||
};
|
||||
if let Some(a) = d.artboards.get_mut(target) {
|
||||
a.rect = src.rect + dv;
|
||||
}
|
||||
for id in &art {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
if copy {
|
||||
let (Some((parent, at, _)), Some(n)) = (d.position(*id), d.node(*id).cloned()) else { continue };
|
||||
let mut c = d.reid(&n);
|
||||
c.transform(Affine::translate(dv), scale_strokes);
|
||||
d.insert(parent, at + 1, c)?;
|
||||
} else if let Some(n) = d.node_mut(*id) {
|
||||
n.transform(Affine::translate(dv), scale_strokes);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(target)
|
||||
})?;
|
||||
Ok(json!({ "index": i, "moved": art.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
Ok(json!({ "index": index, "moved": art.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
@@ -265,6 +265,33 @@ fn artboard_resize_via_handle_and_move_command() {
|
||||
assert!(s.execute("artboard.move", &json!({"index": 7, "dx": 1})).is_err());
|
||||
}
|
||||
|
||||
/// Alt-dragging an artboard with the Artboard tool leaves it and its art in place and moves
|
||||
/// copies of both.
|
||||
#[test]
|
||||
fn artboard_alt_drag_duplicates_it_with_its_art() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
gesture(&mut s, "artboard", &[(400.0, 300.0), (600.0, 300.0), (1300.0, 300.0), (1300.0, 300.0)], alt);
|
||||
let st = s.doc().unwrap();
|
||||
let d = &st.doc;
|
||||
assert_eq!(d.artboards.len(), 2);
|
||||
assert_eq!(d.artboards[0].rect, Rect::new(0.0, 0.0, 800.0, 600.0), "the original stays");
|
||||
assert_eq!((d.artboards[1].rect, d.artboards[1].name.as_str()), (Rect::new(900.0, 0.0, 1700.0, 600.0), "Artboard 1 copy"));
|
||||
assert_ne!(d.artboards[1].id, d.artboards[0].id);
|
||||
let kids = d.layers[0].children().unwrap();
|
||||
assert_eq!(kids.len(), 2, "the rectangle and its copy");
|
||||
assert_eq!(d.node(r).unwrap().geometric_bounds().unwrap().x0, 100.0);
|
||||
let copy = kids.iter().find(|n| n.id != r).unwrap();
|
||||
assert_eq!(copy.geometric_bounds().unwrap().x0, 1000.0);
|
||||
assert_eq!(st.history.undo.last().unwrap().label, "Duplicate Artboard");
|
||||
// One undo takes the copies away.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!((d.artboards.len(), d.layers[0].children().unwrap().len()), (1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn magic_wand_selects_same_fill() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Artboard tool (Shift+O).
|
||||
//!
|
||||
//! Click an artboard to make it active (dashed bounds, 8 handles and its name). Drag inside moves
|
||||
//! it (with its artwork when the `moveArt` option is on, Shift constrains), drag a handle resizes it
|
||||
//! it (with its artwork when the `moveArt` option is on, Shift constrains; Alt moves a copy and
|
||||
//! leaves the artboard where it was), drag a handle resizes it
|
||||
//! (Shift proportional, Alt from centre), drag on the pasteboard draws a new artboard, Delete removes
|
||||
//! the active one and Escape returns to the Selection tool.
|
||||
|
||||
@@ -14,9 +15,23 @@ use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, Tool
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Drag {
|
||||
Move { index: usize, start: Point, began: bool },
|
||||
Resize { index: usize, handle: Handle, rect: Rect, began: bool },
|
||||
Create { start: Point, cur: Point },
|
||||
/// `copy`: Alt was held at the last drag event (the step under way is a duplicate).
|
||||
Move {
|
||||
index: usize,
|
||||
start: Point,
|
||||
began: bool,
|
||||
copy: bool,
|
||||
},
|
||||
Resize {
|
||||
index: usize,
|
||||
handle: Handle,
|
||||
rect: Rect,
|
||||
began: bool,
|
||||
},
|
||||
Create {
|
||||
start: Point,
|
||||
cur: Point,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ArtboardTool {
|
||||
@@ -66,25 +81,33 @@ impl Tool for ArtboardTool {
|
||||
}
|
||||
if let Some(i) = (0..cx.doc.artboards.len()).rev().find(|i| cx.doc.artboards[*i].rect.contains(p)) {
|
||||
self.active = i;
|
||||
self.drag = Some(Drag::Move { index: i, start: p, began: false });
|
||||
self.drag = Some(Drag::Move { index: i, start: p, began: false, copy: false });
|
||||
} else {
|
||||
let (p, _) = crate::guides::snap_draw(cx, p, &[]);
|
||||
self.drag = Some(Drag::Create { start: p, cur: p });
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
(PointerKind::Drag, Some(Drag::Move { index, start, began })) => {
|
||||
(PointerKind::Drag, Some(Drag::Move { index, start, began, copy })) => {
|
||||
let mut out = vec![];
|
||||
let label = |copy: bool| if copy { "Duplicate Artboard" } else { "Move Artboard" };
|
||||
if !began {
|
||||
if p.distance(start) < cx.tol(3.0) {
|
||||
return out;
|
||||
}
|
||||
out.push(Action::Begin("Move Artboard".into()));
|
||||
out.push(Action::Begin(label(m.alt).into()));
|
||||
} else if m.alt != copy {
|
||||
// Alt pressed or released mid-drag: start over as a copy or a move.
|
||||
out.push(Action::Cancel);
|
||||
out.push(Action::Begin(label(m.alt).into()));
|
||||
}
|
||||
self.drag = Some(Drag::Move { index, start, began: true });
|
||||
self.drag = Some(Drag::Move { index, start, began: true, copy: m.alt });
|
||||
let d = move_delta(start, p, m.shift);
|
||||
self.preview = cx.doc.artboards.get(index).map(|a| a.rect + d);
|
||||
out.push(Action::Preview("artboard.move".into(), json!({ "index": index, "dx": d.x, "dy": d.y, "moveArt": self.move_art })));
|
||||
out.push(Action::Preview(
|
||||
"artboard.move".into(),
|
||||
json!({ "index": index, "dx": d.x, "dy": d.y, "moveArt": self.move_art, "copy": m.alt }),
|
||||
));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, Some(Drag::Resize { index, handle, rect, began })) => {
|
||||
@@ -112,7 +135,14 @@ impl Tool for ArtboardTool {
|
||||
self.drag = None;
|
||||
self.preview = None;
|
||||
match d {
|
||||
Drag::Move { began: true, .. } | Drag::Resize { began: true, .. } => vec![Action::Commit],
|
||||
Drag::Move { began: true, copy, .. } => {
|
||||
// The copy, last in the previewed document, becomes the active artboard.
|
||||
if copy {
|
||||
self.active = cx.doc.artboards.len().saturating_sub(1);
|
||||
}
|
||||
vec![Action::Commit]
|
||||
}
|
||||
Drag::Resize { began: true, .. } => vec![Action::Commit],
|
||||
Drag::Create { start, cur } => {
|
||||
let r = Rect::from_points(start, cur);
|
||||
if r.width() < cx.tol(3.0) || r.height() < cx.tol(3.0) {
|
||||
@@ -251,7 +281,7 @@ mod tests {
|
||||
assert_eq!(t.active, 1);
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 710.0, 120.0));
|
||||
assert_eq!(a[0], Action::Begin("Move Artboard".into()));
|
||||
assert_eq!(a[1], Action::Preview("artboard.move".into(), json!({"index": 1, "dx": 10.0, "dy": 20.0, "moveArt": true})));
|
||||
assert_eq!(a[1], Action::Preview("artboard.move".into(), json!({"index": 1, "dx": 10.0, "dy": 20.0, "moveArt": true, "copy": false})));
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 710.0, 120.0)), vec![Action::Commit]);
|
||||
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "02 - Artboard 2")));
|
||||
// Handle drag resizes.
|
||||
@@ -270,4 +300,25 @@ mod tests {
|
||||
assert_eq!(t.key(&cx, ToolKey::Delete, Mods::default()), vec![Action::Exec("artboard.delete".into(), json!({"index": 1}))]);
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![Action::SwitchTool("selection".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_drag_moves_a_copy_of_the_artboard() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let alt = Mods { alt: true, ..Mods::default() };
|
||||
let moved =
|
||||
|dx: f64, copy: bool| Action::Preview("artboard.move".into(), json!({"index": 0, "dx": dx, "dy": 0.0, "moveArt": true, "copy": copy}));
|
||||
let mut t = ArtboardTool::default();
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 100.0, 100.0).with_mods(alt));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 150.0, 100.0).with_mods(alt));
|
||||
assert_eq!(a, vec![Action::Begin("Duplicate Artboard".into()), moved(50.0, true)]);
|
||||
// Alt released mid-drag: a plain move after all, and back.
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 160.0, 100.0));
|
||||
assert_eq!(a, vec![Action::Cancel, Action::Begin("Move Artboard".into()), moved(60.0, false)]);
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 170.0, 100.0).with_mods(alt));
|
||||
assert_eq!(a, vec![Action::Cancel, Action::Begin("Duplicate Artboard".into()), moved(70.0, true)]);
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 170.0, 100.0).with_mods(alt)), vec![Action::Commit]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user