mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2024 from Muratv07/feature/move-art-and-guides-together
feat: move and copy ruler guides and art together when selected
This commit is contained in:
@@ -960,7 +960,7 @@ fn persp_edit(
|
||||
return Err(EngineError::Other("select the objects to transform in perspective".into()));
|
||||
}
|
||||
let mut g = grid_of(&s.doc()?.doc);
|
||||
let kept = if copy && !stores_attachments(&s.doc()?.doc) { super::object::preview_copies(s, ids)? } else { None };
|
||||
let kept = if copy && !stores_attachments(&s.doc()?.doc) { super::object::preview_copies(s, ids, false)? } else { None };
|
||||
s.edit(label, |d, sel| {
|
||||
g.adopt_stored_attachments(d);
|
||||
let targets = match &kept {
|
||||
@@ -1164,7 +1164,7 @@ fn plane_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// lists attachments of its own (`stores_attachments`).
|
||||
let kept = if objects == "copy" && s.in_interaction() && !stores_attachments(&s.doc()?.doc) {
|
||||
let on = on_plane(&s.doc()?.doc);
|
||||
super::object::preview_copies(s, &on)?
|
||||
super::object::preview_copies(s, &on, false)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
@@ -659,6 +659,17 @@ pub(crate) fn guide_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad("guide.move", "no guide selected (or give an index)"));
|
||||
}
|
||||
let (dx, dy, copy) = (f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0), bool_or(p, "copy", false));
|
||||
if !s.doc()?.selection.is_empty() {
|
||||
let ids = super::edit::selected_roots(s)?;
|
||||
return super::object::apply_transform(
|
||||
s,
|
||||
if copy { "Copy Guide" } else { "Move Guide" },
|
||||
ids,
|
||||
vectorcraft_geom::Affine::translate((dx, dy)),
|
||||
p,
|
||||
true,
|
||||
);
|
||||
}
|
||||
s.edit(if copy { "Copy Guide" } else { "Move Guide" }, |d, sel| {
|
||||
let mut copies = vec![];
|
||||
for &i in &moving {
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
["Object", "Transform"],
|
||||
Some("Cmd+Shift+M"),
|
||||
"{dx, dy, copy?, objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
has_selection_or_guides,
|
||||
move_cmd
|
||||
),
|
||||
cmd!(
|
||||
@@ -297,11 +297,16 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
||||
let objects = transforms_objects(p);
|
||||
let mut sc = if Scaling::factor(xf).is_some() { scaling(s, p) } else { Scaling::default() };
|
||||
sc.patterns = transform_patterns(s, p, &ids)?;
|
||||
let c = xf.as_coeffs();
|
||||
let is_translation = (c[0] - 1.0).abs() < 1e-9 && c[1].abs() < 1e-9 && c[2].abs() < 1e-9 && (c[3] - 1.0).abs() < 1e-9;
|
||||
let delta = Vec2::new(c[4], c[5]);
|
||||
let with_guides = objects && ids_param(p, "ids").is_none() && is_translation;
|
||||
let has_guides = with_guides && !s.doc()?.selection.guides.is_empty();
|
||||
// A transform of ids that name no object, or an identity transform without a copy, moves
|
||||
// nothing: the document and its undo history stay as they are, and Transform Again repeats it.
|
||||
// With a copy, the reply lists the copies, and there are none.
|
||||
let d = &s.doc()?.doc;
|
||||
let none = d.index_paths(ids.iter().copied()).is_empty();
|
||||
let none = d.index_paths(ids.iter().copied()).is_empty() && !has_guides;
|
||||
let ids = if anchors && objects && !copy && !s.doc()?.selection.anchors.is_empty() {
|
||||
if xf != Affine::IDENTITY {
|
||||
super::path::transform_anchors(s, label, xf, sc)?;
|
||||
@@ -310,7 +315,7 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
||||
} else if none || (!copy && xf == Affine::IDENTITY) {
|
||||
if copy { vec![] } else { ids }
|
||||
} else {
|
||||
let kept = if copy { preview_copies(s, &ids)? } else { None };
|
||||
let kept = if copy { preview_copies(s, &ids, with_guides)? } else { None };
|
||||
s.edit(label, |d, sel| {
|
||||
let targets = match &kept {
|
||||
// The document becomes the snapshot with the kept copies, untransformed, and the
|
||||
@@ -320,7 +325,21 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
||||
sel.clone_from(&k.selection);
|
||||
k.ids.clone()
|
||||
}
|
||||
None if copy => duplicate_in(d, sel, &ids, Affine::IDENTITY)?,
|
||||
None if copy => {
|
||||
let guide_copies = if with_guides {
|
||||
let copies: Vec<_> = sel.guides.iter().filter_map(|&i| d.guides.get(i).cloned()).collect();
|
||||
(!copies.is_empty()).then_some(copies)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let new_ids = duplicate_in(d, sel, &ids, Affine::IDENTITY)?;
|
||||
if let Some(copies) = guide_copies {
|
||||
let first = d.guides.len();
|
||||
d.guides.extend(copies);
|
||||
sel.guides = (first..d.guides.len()).collect();
|
||||
}
|
||||
new_ids
|
||||
}
|
||||
None => ids.clone(),
|
||||
};
|
||||
// Transforms don't move nodes in the tree, so the paths found up front stay right.
|
||||
@@ -332,6 +351,13 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
||||
transform_node(n, xf, sc, objects);
|
||||
}
|
||||
}
|
||||
if with_guides && (delta.x != 0.0 || delta.y != 0.0) {
|
||||
for i in sel.guides.clone() {
|
||||
if let Some(g) = d.guides.get_mut(i) {
|
||||
*g = g.moved(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(targets)
|
||||
})?
|
||||
};
|
||||
@@ -355,21 +381,30 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
||||
/// untransformed, and the interaction keeps that clone. It returns `None` outside an interaction,
|
||||
/// and when the document is no longer the snapshot, as for a command run in a typing session or a
|
||||
/// script batch after an earlier edit there: the command then copies the current document.
|
||||
pub(crate) fn preview_copies(s: &mut Session, roots: &[NodeId]) -> Result<Option<Arc<crate::PreviewCopies>>> {
|
||||
pub(crate) fn preview_copies(s: &mut Session, roots: &[NodeId], with_guides: bool) -> Result<Option<Arc<crate::PreviewCopies>>> {
|
||||
let st = s.doc_mut()?;
|
||||
let Some(it) = &mut st.interaction else { return Ok(None) };
|
||||
if !Arc::ptr_eq(&st.doc, &it.doc) {
|
||||
return Ok(None);
|
||||
}
|
||||
let has_guide_copies = with_guides && !st.selection.guides.is_empty();
|
||||
if let Some(k) = &it.copies
|
||||
&& Arc::ptr_eq(&k.from, &it.doc)
|
||||
&& k.roots == roots
|
||||
&& (!k.selection.guides.is_empty() == has_guide_copies)
|
||||
{
|
||||
return Ok(Some(k.clone()));
|
||||
}
|
||||
let mut doc = it.doc.clone();
|
||||
let mut selection = st.selection.clone();
|
||||
let ids = duplicate_in(Arc::make_mut(&mut doc), &mut selection, roots, Affine::IDENTITY)?;
|
||||
if has_guide_copies {
|
||||
let d = Arc::make_mut(&mut doc);
|
||||
let first = d.guides.len();
|
||||
let copies: Vec<_> = st.selection.guides.iter().filter_map(|&i| d.guides.get(i).cloned()).collect();
|
||||
d.guides.extend(copies);
|
||||
selection.guides = (first..d.guides.len()).collect();
|
||||
}
|
||||
let k = Arc::new(crate::PreviewCopies { from: it.doc.clone(), roots: roots.to_vec(), doc, selection, ids });
|
||||
it.copies = Some(k.clone());
|
||||
Ok(Some(k))
|
||||
|
||||
@@ -181,6 +181,9 @@ pub(crate) fn move_anchors(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// objects selected whole (those with no entry in the selection's `anchors`).
|
||||
pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_geom::Affine, sc: Scaling) -> Result<()> {
|
||||
let sel = s.doc()?.selection.clone();
|
||||
let c = xf.as_coeffs();
|
||||
let is_translation = (c[0] - 1.0).abs() < 1e-9 && c[1].abs() < 1e-9 && c[2].abs() < 1e-9 && (c[3] - 1.0).abs() < 1e-9;
|
||||
let delta = vectorcraft_geom::Vec2::new(c[4], c[5]);
|
||||
s.edit(label, |doc, _| {
|
||||
for id in &sel.objects {
|
||||
match sel.anchors.get(id) {
|
||||
@@ -199,6 +202,13 @@ pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_ge
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_translation && (delta.x != 0.0 || delta.y != 0.0) {
|
||||
for &i in &sel.guides {
|
||||
if let Some(g) = doc.guides.get_mut(i) {
|
||||
*g = g.moved(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1579,6 +1579,119 @@ fn guides_and_art_are_shift_selected_together() {
|
||||
assert_eq!(guides(&s).len(), 2);
|
||||
}
|
||||
|
||||
/// When ruler guides and art are selected together, moving the art moves the guides as well;
|
||||
/// copy leaves both in place and moves the copies; single undo reverts both.
|
||||
#[test]
|
||||
fn guides_and_art_move_together_when_selected() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 10.0, 10.0, 40.0, 20.0);
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 200})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": false, "pos": 300})).unwrap();
|
||||
|
||||
// Select art and both guides.
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("guide.select", &json!({"indexes": [0, 1], "toggle": true})).unwrap();
|
||||
assert_eq!((selected(&s), selected_guides(&s)), (vec![a], vec![0, 1]));
|
||||
|
||||
// 1. Moving via object.transform moves both art and guides in one step.
|
||||
let n = undo_len(&s);
|
||||
s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 25, 35]})).unwrap();
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!((b.x0, b.y0), (35.0, 45.0));
|
||||
assert_eq!(guides(&s), vec![(true, 225.0), (false, 335.0)]);
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
|
||||
// Undo restores both in one step.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!((b.x0, b.y0), (10.0, 10.0));
|
||||
assert_eq!(guides(&s), vec![(true, 200.0), (false, 300.0)]);
|
||||
|
||||
// Redo reapplies both.
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!((b.x0, b.y0), (35.0, 45.0));
|
||||
assert_eq!(guides(&s), vec![(true, 225.0), (false, 335.0)]);
|
||||
|
||||
// Undo back to start.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
// 2. object.move also moves both.
|
||||
s.execute("object.move", &json!({"dx": 15, "dy": 20})).unwrap();
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!((b.x0, b.y0), (25.0, 30.0));
|
||||
assert_eq!(guides(&s), vec![(true, 215.0), (false, 320.0)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
// 3. Arrow-key nudge moves both.
|
||||
s.execute("object.nudge", &json!({"dx": 1, "dy": 0})).unwrap();
|
||||
let k = s.prefs.keyboard_increment;
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!(b.x0, 10.0 + k);
|
||||
assert_eq!(guides(&s)[0], (true, 200.0 + k));
|
||||
assert_eq!(guides(&s)[1], (false, 300.0));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
// 4. Copy (e.g. Alt-drag) duplicates both art and guides, leaving originals in place.
|
||||
let res = s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 50, 60], "copy": true})).unwrap();
|
||||
let copied_id = NodeId(res["ids"][0].as_u64().unwrap());
|
||||
assert_ne!(copied_id, a);
|
||||
let b_orig = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
let b_copy = s.doc().unwrap().doc.bounds_of(&[copied_id], false).unwrap();
|
||||
assert_eq!((b_orig.x0, b_orig.y0), (10.0, 10.0));
|
||||
assert_eq!((b_copy.x0, b_copy.y0), (60.0, 70.0));
|
||||
assert_eq!(guides(&s), vec![(true, 200.0), (false, 300.0), (true, 250.0), (false, 360.0)]);
|
||||
assert_eq!((selected(&s), selected_guides(&s)), (vec![copied_id], vec![2, 3]));
|
||||
|
||||
// Undo takes back both copies in one step.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(guides(&s), vec![(true, 200.0), (false, 300.0)]);
|
||||
assert!(s.doc().unwrap().doc.node(copied_id).is_none());
|
||||
assert!(s.doc().unwrap().doc.node(a).is_some());
|
||||
|
||||
// 5. Explicit ids: only the specified art moves, guides stay put.
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("guide.select", &json!({"indexes": [0, 1], "toggle": true})).unwrap();
|
||||
s.execute("object.transform", &json!({"ids": [a.0], "matrix": [1, 0, 0, 1, 40, 50]})).unwrap();
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!((b.x0, b.y0), (50.0, 60.0));
|
||||
assert_eq!(guides(&s), vec![(true, 200.0), (false, 300.0)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
|
||||
// 6. Interactive preview copies: dragging with Alt previews the copies and commit keeps them.
|
||||
s.begin_interaction("Move").unwrap();
|
||||
s.preview("object.transform", &json!({"matrix": [1, 0, 0, 1, 10, 20], "copy": true})).unwrap();
|
||||
s.preview("object.transform", &json!({"matrix": [1, 0, 0, 1, 30, 40], "copy": true})).unwrap();
|
||||
s.commit_interaction().unwrap();
|
||||
assert_eq!(guides(&s).len(), 4);
|
||||
assert_eq!(guides(&s)[2], (true, 230.0));
|
||||
assert_eq!(guides(&s)[3], (false, 340.0));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(guides(&s), vec![(true, 200.0), (false, 300.0)]);
|
||||
|
||||
// 7. Dragging a guide (guide.move) when art is also selected moves both art and guides.
|
||||
sel(&mut s, &[a]);
|
||||
s.execute("guide.select", &json!({"indexes": [0, 1], "toggle": true})).unwrap();
|
||||
s.execute("guide.move", &json!({"dx": 10, "dy": 20})).unwrap();
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!((b.x0, b.y0), (20.0, 30.0));
|
||||
assert_eq!(guides(&s), vec![(true, 210.0), (false, 320.0)]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let b = s.doc().unwrap().doc.bounds_of(&[a], false).unwrap();
|
||||
assert_eq!((b.x0, b.y0), (10.0, 10.0));
|
||||
assert_eq!(guides(&s), vec![(true, 200.0), (false, 300.0)]);
|
||||
|
||||
// 8. Copy-dragging a guide (guide.move { copy: true }) when art is also selected duplicates both.
|
||||
s.execute("guide.move", &json!({"dx": 10, "dy": 20, "copy": true})).unwrap();
|
||||
assert_eq!(guides(&s).len(), 4);
|
||||
assert_eq!(guides(&s)[2], (true, 210.0));
|
||||
assert_eq!(guides(&s)[3], (false, 320.0));
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 2);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(guides(&s).len(), 2);
|
||||
assert_eq!(s.doc().unwrap().doc.layers[0].children().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
/// #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