mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
Fix #3226: copy and paste ruler guides
Edit > Copy and Cut put the selected ruler guides on the clipboard, with the selected objects or on their own. Pasting puts them on the layer new art goes to, selected, in the same undo step: with objects they move as the objects do; alone they keep their places, while Paste in Place / in Front / in Back put them on the active artboard as they lay on theirs, and Paste on All Artboards on every artboard.
This commit is contained in:
@@ -168,9 +168,9 @@ pub fn has_selection(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
if st.selection.is_empty() { Err("nothing selected".into()) } else { Ok(()) }
|
||||
}
|
||||
/// Objects are selected, or the Artboard tool is chosen (Copy and Cut take its artboard).
|
||||
pub fn has_selection_or_artboard_tool(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.tool_id() == "artboard" { has_doc(s) } else { has_selection(s) }
|
||||
/// Objects or ruler guides are selected, or the Artboard tool is chosen (what Copy and Cut take).
|
||||
pub fn has_selection_guides_or_artboard_tool(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.tool_id() == "artboard" { has_doc(s) } else { has_selection_or_guides(s) }
|
||||
}
|
||||
/// Objects or ruler guides are selected (what Delete and the arrow keys act on).
|
||||
pub fn has_selection_or_guides(s: &Session) -> std::result::Result<(), String> {
|
||||
|
||||
@@ -93,11 +93,14 @@ pub struct Clipboard {
|
||||
/// The artboards copied with the Artboard tool (`artboard.copy`); `nodes` are then their art.
|
||||
/// Pasting adds a copy of each with the art.
|
||||
pub artboards: Vec<vectorcraft_doc::Artboard>,
|
||||
/// The selected ruler guides copied with (or without) the objects (#3226), as canvas guides on
|
||||
/// no layer: pasting puts them on the layer new art goes to, moved as the objects are.
|
||||
pub guides: Vec<vectorcraft_doc::Guide>,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.nodes.is_empty() && self.artboards.is_empty()
|
||||
self.nodes.is_empty() && self.artboards.is_empty() && self.guides.is_empty()
|
||||
}
|
||||
|
||||
/// Objects `roots` of `st`'s document with their resources, source artboard and layers.
|
||||
|
||||
@@ -10,8 +10,8 @@ use vectorcraft_geom::{Affine, Point, Vec2};
|
||||
use super::clipboard::SwatchChoices;
|
||||
use super::{
|
||||
CommandSpec, Result, Session, always, bad, bool_or, can_clear_history, can_redo, can_undo, checked_ids_param, cmd, f64_or, f64_req,
|
||||
finite_numbers, has_clipboard, has_doc, has_selection, has_selection_or_artboard_tool, has_selection_or_guides, id_param, index, ok, point_param,
|
||||
str_param,
|
||||
finite_numbers, has_clipboard, has_doc, has_selection, has_selection_guides_or_artboard_tool, has_selection_or_guides, id_param, index, ok,
|
||||
point_param, str_param,
|
||||
};
|
||||
use crate::{Clipboard, EngineError, inspect};
|
||||
|
||||
@@ -125,8 +125,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always,
|
||||
undo_group_end
|
||||
),
|
||||
cmd!("edit.cut", "Cut", ["Edit"], Some("Cmd+X"), "{} (the Artboard tool chosen: artboard.cut)", has_selection_or_artboard_tool, cut),
|
||||
cmd!("edit.copy", "Copy", ["Edit"], Some("Cmd+C"), "{} (the Artboard tool chosen: artboard.copy)", has_selection_or_artboard_tool, copy),
|
||||
cmd!(
|
||||
"edit.cut",
|
||||
"Cut",
|
||||
["Edit"],
|
||||
Some("Cmd+X"),
|
||||
"{} (selected ruler guides go too; the Artboard tool chosen: artboard.cut)",
|
||||
has_selection_guides_or_artboard_tool,
|
||||
cut
|
||||
),
|
||||
cmd!(
|
||||
"edit.copy",
|
||||
"Copy",
|
||||
["Edit"],
|
||||
Some("Cmd+C"),
|
||||
"{} the selection with what it uses, and the selected ruler guides (pasted on the layer new art goes to, moved as the objects are; alone, where they were); the Artboard tool chosen: artboard.copy → {copied, guides}",
|
||||
has_selection_guides_or_artboard_tool,
|
||||
copy
|
||||
),
|
||||
cmd!(
|
||||
"edit.paste",
|
||||
"Paste",
|
||||
@@ -652,9 +668,35 @@ fn copy(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
if s.tool_id() == "artboard" {
|
||||
return super::panelcmds::artboard_copy(s, &json!({}), false);
|
||||
}
|
||||
let roots = selected_roots(s)?;
|
||||
s.clipboard = Clipboard::copy(s.doc()?, &roots);
|
||||
Ok(json!({ "copied": s.clipboard.nodes.len() }))
|
||||
let st = s.doc()?;
|
||||
// Selected ruler guides come along (#3226), as canvas guides: their layer and artboard stay
|
||||
// behind.
|
||||
let guides: Vec<vectorcraft_doc::Guide> = st
|
||||
.selection
|
||||
.guides
|
||||
.iter()
|
||||
.filter_map(|i| st.doc.guides.get(*i))
|
||||
.map(|g| vectorcraft_doc::Guide { layer: None, artboard: None, ..g.clone() })
|
||||
.collect();
|
||||
let roots = if st.selection.is_empty() && !guides.is_empty() { vec![] } else { selected_roots(s)? };
|
||||
let st = s.doc()?;
|
||||
let mut clip = Clipboard::copy(st, &roots);
|
||||
// Guides alone: the artboard the first one belongs to, or crosses, is where in-place pastes
|
||||
// measure from, as the objects' artboard is.
|
||||
if clip.nodes.is_empty()
|
||||
&& let Some(g) = st.selection.guides.first().and_then(|i| st.doc.guides.get(*i))
|
||||
{
|
||||
let board = g.artboard.and_then(|id| st.doc.artboards.iter().find(|a| a.id == id)).or_else(|| {
|
||||
st.doc
|
||||
.artboards
|
||||
.iter()
|
||||
.find(|a| if g.vertical { (a.rect.x0..=a.rect.x1).contains(&g.pos) } else { (a.rect.y0..=a.rect.y1).contains(&g.pos) })
|
||||
});
|
||||
clip.source_artboard = board.map(|a| a.rect);
|
||||
}
|
||||
clip.guides = guides;
|
||||
s.clipboard = clip;
|
||||
Ok(json!({ "copied": s.clipboard.nodes.len(), "guides": s.clipboard.guides.len() }))
|
||||
}
|
||||
|
||||
fn cut(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -663,6 +705,11 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
// Copy promotes a compound member to its compound. Cut must remove the same objects it
|
||||
// copied, while Clear on its own deletes exactly the selected members.
|
||||
// Ruler guides selected on their own: Copy takes them, Clear removes them.
|
||||
if s.doc()?.selection.is_empty() {
|
||||
copy(s, p)?;
|
||||
return clear(s, &json!({}));
|
||||
}
|
||||
let ids = selected_roots(s)?;
|
||||
copy(s, p)?;
|
||||
if s.doc()?.selection.anchors.is_empty() { clear(s, &json!({ "ids": ids })) } else { clear(s, &json!({})) }
|
||||
@@ -850,6 +897,13 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
// In place, in front, in back: onto the active artboard when the UI names it.
|
||||
_ => vec![onto_artboard(&st.doc, p, clip)],
|
||||
};
|
||||
// Guides alone don't follow a plain paste's offset or the view centre: they stay where they
|
||||
// were. With objects they move as the objects do.
|
||||
let guide_placements: Vec<Affine> = if !clip.nodes.is_empty() || !matches!(mode, PasteMode::Offset | PasteMode::Library | PasteMode::Drag) {
|
||||
placements.clone()
|
||||
} else {
|
||||
vec![Affine::IDENTITY]
|
||||
};
|
||||
let (ids, imported, artboards, into) = s.edit(mode.label(), |d, sel| {
|
||||
let imported = clip.import_into(d, choices, same_doc);
|
||||
// Draw Inside: the clip group of the object drawn into, made on the first paste.
|
||||
@@ -897,6 +951,20 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
}
|
||||
}
|
||||
sel.set(new_ids.iter().copied());
|
||||
// Copied ruler guides (#3226): on the layer new art goes to, moved as the objects are (a
|
||||
// paste of guides alone keeps them where they were, but onto another artboard or every
|
||||
// artboard as the in-place pastes put art); they are selected with the objects.
|
||||
let guide_layer = parent.and_then(|p| if d.node(p).is_some_and(Node::is_layer) { Some(p) } else { d.layer_containing(p) });
|
||||
for xf in &guide_placements {
|
||||
let dv = xf.translation();
|
||||
for g in &clip.guides {
|
||||
let pos = g.pos + if g.vertical { dv.x } else { dv.y };
|
||||
if pos.is_finite() {
|
||||
sel.guides.push(d.guides.len());
|
||||
d.guides.push(vectorcraft_doc::Guide { pos, layer: guide_layer, ..g.clone() });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pasted type arrives without the layout bounds cache: fill it so boxes and alignment are
|
||||
// right away (opening the file later would, but paste must not depend on a reopen).
|
||||
super::typecmd::refresh_bounds_of(d, &new_ids);
|
||||
@@ -911,6 +979,7 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
}
|
||||
Ok(json!({
|
||||
"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(),
|
||||
"guides": clip.guides.len() * guide_placements.len(),
|
||||
"artboard": artboards.first(),
|
||||
"artboards": artboards,
|
||||
"added": imported.added,
|
||||
|
||||
@@ -67,6 +67,7 @@ mod gradient;
|
||||
mod gradpanel;
|
||||
mod gradpresets;
|
||||
mod gradspan;
|
||||
mod guide_clipboard;
|
||||
mod halftone;
|
||||
mod history_mem;
|
||||
mod ids_without_selection;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! Copying and pasting ruler guides (#3226): selected guides go on the clipboard with the objects
|
||||
//! or alone, and paste into another document or onto another artboard.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeId;
|
||||
|
||||
use crate::*;
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
|
||||
}
|
||||
|
||||
/// The active document's guides: (vertical, pos, layer).
|
||||
fn guides(s: &Session) -> Vec<(bool, f64, Option<NodeId>)> {
|
||||
s.doc().unwrap().doc.guides.iter().map(|g| (g.vertical, g.pos, g.layer)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_guides_copy_and_paste_into_another_document() {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 400, "height": 300}));
|
||||
run(&mut s, "guide.add", json!({"vertical": true, "pos": 120}));
|
||||
run(&mut s, "guide.add", json!({"vertical": false, "pos": 80}));
|
||||
run(&mut s, "guide.select", json!({"indexes": [0, 1]}));
|
||||
let r = run(&mut s, "edit.copy", json!({}));
|
||||
assert_eq!(r["guides"], 2);
|
||||
// Into another document: the same places, on its current layer, selected, one undo step.
|
||||
run(&mut s, "file.new", json!({"width": 400, "height": 300}));
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let r = run(&mut s, "edit.paste", json!({}));
|
||||
assert_eq!(r["guides"], 2);
|
||||
assert_eq!(guides(&s), [(true, 120.0, Some(layer)), (false, 80.0, Some(layer))], "where they were, not offset");
|
||||
assert_eq!(s.doc().unwrap().selection.guides, [0, 1]);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(guides(&s).is_empty());
|
||||
// With an object, they move as it does (Paste offsets it by the Paste Offset preference).
|
||||
run(&mut s, "document.activate", json!({"index": 0}));
|
||||
let rect = run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}))["id"].as_u64().unwrap();
|
||||
run(&mut s, "select.set", json!({"ids": [rect]}));
|
||||
run(&mut s, "guide.select", json!({"indexes": [0], "toggle": true}));
|
||||
run(&mut s, "edit.copy", json!({}));
|
||||
run(&mut s, "document.activate", json!({"index": 1}));
|
||||
run(&mut s, "edit.paste", json!({"dx": 5, "dy": 7}));
|
||||
assert_eq!(guides(&s).iter().map(|g| (g.0, g.1)).collect::<Vec<_>>(), [(true, 125.0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guides_paste_in_place_onto_another_artboard_and_cut_removes_them() {
|
||||
let mut s = Session::new();
|
||||
run(&mut s, "file.new", json!({"width": 100, "height": 100, "artboards": 2}));
|
||||
let [a, b] = [0, 1].map(|i| s.doc().unwrap().doc.artboards[i].rect);
|
||||
run(&mut s, "guide.add", json!({"vertical": true, "pos": a.x0 + 30.0}));
|
||||
run(&mut s, "guide.select", json!({"indexes": [0]}));
|
||||
// Cut takes the guide away and keeps it on the clipboard.
|
||||
run(&mut s, "edit.cut", json!({}));
|
||||
assert!(guides(&s).is_empty());
|
||||
// Paste in Place onto the second artboard: 30 pt into it.
|
||||
run(&mut s, "edit.pasteInPlace", json!({"artboard": 1}));
|
||||
assert_eq!(guides(&s).iter().map(|g| g.1).collect::<Vec<_>>(), [b.x0 + 30.0]);
|
||||
// Paste on All Artboards: one on each.
|
||||
run(&mut s, "edit.pasteOnAllArtboards", json!({}));
|
||||
let mut at: Vec<f64> = guides(&s).iter().map(|g| g.1).collect();
|
||||
at.sort_by(f64::total_cmp);
|
||||
assert_eq!(at, [a.x0 + 30.0, b.x0 + 30.0, b.x0 + 30.0]);
|
||||
// Nothing selected: Copy is off.
|
||||
run(&mut s, "select.none", json!({}));
|
||||
assert!(s.execute("edit.copy", &json!({})).is_err());
|
||||
}
|
||||
@@ -1594,10 +1594,15 @@ A double-click on a Repeat (Radial, Grid or Mirror) with the Selection tool ente
|
||||
|
||||
With area type selected, the Properties panel's Paragraph section and the Control panel show Align Top, Align Center, Align Bottom and Justify Vertically buttons, as the public documentation places them ([Align text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/align-text.html)). They set the type's vertical alignment in its frame (`text.areaOptions {verticalAlign}`, as Area Type Options does) on every selected frame in one undo step, each frame keeping its other options; the shared alignment shows selected, none when the frames differ (#3759). As that page limits vertical alignment to area type without rows, columns or threads, the buttons are turned off while a selected frame has rows or columns or is threaded; Area Type Options and `text.areaOptions` still align each row/column cell (a VectorCraft extension, unchanged here). Tests: `panels::properties::tests::vertical_alignment_buttons_align_the_selected_area_type`.
|
||||
|
||||
## Copy and paste ruler guides (2026-11-01)
|
||||
|
||||
Edit › Copy and Cut put the selected ruler guides on the clipboard, with the selected objects or on their own, so they paste into another artboard or document (#3226). Pasted guides go on the layer new art goes to and are selected; with objects they move as the objects do, and alone they keep their places, while Paste in Place, in Front and in Back put them on the active artboard as they lay on theirs and Paste on All Artboards on every artboard. Tests: `tests::guide_clipboard`.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-01 | minor | Copy and paste ruler guides (#3226); evidence added; scores unchanged. |
|
||||
| 2026-11-04 | minor | Area type vertical alignment buttons in the Properties panel's Paragraph section and the Control panel, off for columns, rows and threaded frames (#3759); evidence added; scores unchanged. |
|
||||
| 2026-11-03 | minor | Enclosing Lasso is an add-on with no default shortcut or Basic toolbar slot. Scores and estimates remain unchanged. |
|
||||
| 2026-11-02 | minor | Wheel input over closed font-family fields changes selected text (#5253); public documentation and application choices distinguished; scores and estimates unchanged. |
|
||||
|
||||
@@ -420,6 +420,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-01 | minor | Edit › Copy and Cut take the selected ruler guides, with the objects or alone; pasting puts them on the layer new art goes to, moved as the objects are (alone: where they were), Paste in Place onto the active artboard and Paste on All Artboards onto each (#3226); scores and estimates unchanged |
|
||||
| 2026-11-04 | minor | Area type: Align Top / Center / Bottom / Justify Vertically buttons in the Properties panel (Paragraph section) and the Control panel set its vertical alignment in the frame, as Area Type Options does; turned off for frames with rows or columns and threaded frames (#3759); scores and estimates unchanged |
|
||||
| 2026-11-03 | minor | Enclosing Lasso selects fully enclosed objects with a freehand loop under Object › Plug-ins; scores and estimates unchanged. |
|
||||
| 2026-11-03 | minor | Type on a Path Options has a Preview checkbox (#5360, as public tutorials of the dialog show); scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user