feat: move and copy ruler guides and art together when selected

This commit is contained in:
Murat Vural
2026-10-16 04:19:06 +07:00
committed by bflatastic
parent 2abc1d61f7
commit 1ab3940b3d
5 changed files with 176 additions and 7 deletions
+2 -2
View File
@@ -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
};
+11
View File
@@ -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 {
+40 -5
View File
@@ -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))
+10
View File
@@ -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(())
})
}