mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Merge pull request #452 from storytold/fix/issue-446
Fix #446: copy and paste artboards with their art
This commit is contained in:
15 files changed
+359
-58
No files matched your search
+3
-3
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -122,14 +122,14 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout; with Direct Selection, select corner anchors to show and round just those corners (#381). Each corner keeps its own radius and kind (Round, Inverted Round, Chamfer): Alt-click a widget to cycle the kind, double-click it for the Corners dialog, and the Properties and Transform radius fields set the selected corners (`object.setLiveShape {corners, radius, kind}`). Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. On a rectangle that isn't square every corner is the same circle: a radius past half the shorter side stops there for all of them, the widgets sit on the corners as drawn and turn the edited corners red at that limit, CSS export writes the drawn radii, and rectangles saved stretched by older versions get circular corners when their corners are next set (#442).
|
||||
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead.
|
||||
- **Graphs:** Column, Stacked Column, Bar, Stacked Bar, Line, Area, Scatter, Pie and Radar graph tools with Graph Data and Graph Type.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on), Measure, Type, Hand, Zoom, Rotate View.
|
||||
- **Other:** Eyedropper, Gradient annotator, Artboard (Alt-drag duplicates an artboard, and its art with Move/Copy Artwork with Artboard on; Copy, Cut and Paste take the artboard with its art, Paste right of the last artboard, Paste in Place where it was, into this or another document, #446), Measure, Type, Hand, Zoom, Rotate View.
|
||||
- **Editing type:** double-clicking type with a selection tool switches to the Type tool with the caret where it was clicked.
|
||||
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
|
||||
- **Panel drag and drop:** art dragged off the canvas onto the Symbols panel becomes a symbol, onto the Swatches panel a pattern swatch and onto the Brushes panel an Art brush; a symbol dragged onto the canvas places an instance where it is dropped, and a brush dragged onto a path is applied to it.
|
||||
- **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, the Selection, Direct Selection and Group Selection tools the Move dialog (Enter opens these seven tools' dialogs too), Gradient, Eyedropper, Blend and the Liquify tools their panels or options, and the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools their Tool Options (Fidelity, fill, the tolerances, the brush size).
|
||||
- **Layers panel:** every layer, sublayer, group and object is a clickable row at any depth: a click highlights it and makes its layer current (new art goes there), Shift-click a range, Ctrl/Cmd-click toggles. Sublayers are no longer treated as objects: clicks, marquees, Select All and the Select menu take the art inside them, and each sublayer selects in its own colour. Selection squares on rows with selected art (click selects a row's art, Shift adds or removes; dragging the square moves the selection to another row, Alt copies), the eye and lock on every row with drag-down-the-column (one undo step), Ctrl/Cmd-click an eye for per-layer Outline, Alt-click an eye or lock to hide or lock the other layers (again: show or unlock them all), triangles with Alt to open or close everything inside, drag and drop rows above, below or into layers and groups with a drop line (Alt copies) and a dimmed copy of the row under the pointer (dropped on the trash rows are deleted, on New Layer duplicated, as in Illustrator), inline rename, Delete key on highlighted rows. Buttons and the full panel menu: New Layer/Sublayer (Alt: options, Ctrl: on top), Duplicate, Delete, Options for Selection, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New Layer, Release to Layers (Sequence and Build), Reverse Order, Template, Hide/Outline/Lock Others and Show/Preview/Unlock All, Paste Remembers Layers, Panel Options (row size, thumbnails per kind, Show Layers Only). Layer Options: name, colour (presets, each with a chip of its colour, or custom), Template, Lock, Show, Print, Preview (outline drawing and clicking) and Dim Images to %. Every operation is a command (`layer.move`, `layer.highlight`, `layer.merge`…) with validation against cycles and invalid nesting.
|
||||
- **Drawing aids:** Smart Guides and snapping (moving, with the Selection, Group Selection or Direct Selection tool, and resizing by the bounding-box handles: the dragged edges align with the other objects; anchors dragged with Direct Selection: the one pressed on lands on other anchors, its own path's that stay put among them, on path segments or in line with them, the others following, Shift keeps 45° steps, and with Smart Guides off Snap to Point pulls it onto anchors; the Artboard tool snaps artboards, and their bleed, to other artboards, bleeds and art, the grid or whole pixels while moving, copying and resizing them, and the Transform panel shows and edits the active artboard's X/Y/W/H), guides dragged out of the rulers (Shift snaps them to the ruler's ticks) that the Selection, Direct Selection and Group Selection tools then pick and drag (#414: the selected guides highlight and move together, Alt copies them, Shift snaps the dragged one to the ticks, otherwise it snaps to pixels, the grid or the art's edges, centres and anchors; the arrow keys nudge them, Delete deletes them, dragging one back onto its ruler deletes it, Lock Guides and Hide Guides leave them alone; `guide.list`, `guide.select`, `guide.move` and `guide.remove` reach agents), and Draw Normal / Behind / Inside modes.
|
||||
- **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard. The Artboards panel highlights that active artboard: clicking a row makes it active (the Artboard tool's too, while it is chosen) as does clicking an artboard with the Artboard tool, double-clicking its number also fits it in the window and double-clicking its name renames it.
|
||||
- **Views:** the status bar's artboard navigator (first, previous, next, last; `view.goToArtboard`) goes to an artboard and fits it, and Fit Artboard in Window and Actual Size show the navigator's artboard. The Artboards panel highlights that active artboard: clicking a row makes it active (the Artboard tool's too, while it is chosen) as does clicking an artboard with the Artboard tool, double-clicking its number also fits it in the window and double-clicking its name renames it. Duplicate Artboards, or a row dragged onto New Artboard, copies the artboard with its art (#446).
|
||||
- **Geometry and effects:**
|
||||
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected.
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
|
||||
|
||||
@@ -87,11 +87,14 @@ pub struct Clipboard {
|
||||
/// The open document the objects came from ([`DocState::uid`]): pasting back into it uses its
|
||||
/// own resources as they are now.
|
||||
pub source_doc: Option<u64>,
|
||||
/// An artboard copied with the Artboard tool (`artboard.copy`); `nodes` are then its art. Pasting
|
||||
/// adds a copy of it with the art.
|
||||
pub artboard: Option<vectorcraft_doc::Artboard>,
|
||||
}
|
||||
|
||||
impl Clipboard {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.nodes.is_empty()
|
||||
self.nodes.is_empty() && self.artboard.is_none()
|
||||
}
|
||||
|
||||
/// Objects `roots` of `st`'s document with their resources, source artboard and layers.
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Document, Node, NodeId, Unit};
|
||||
use vectorcraft_geom::Affine;
|
||||
use vectorcraft_geom::{Affine, Vec2};
|
||||
|
||||
use super::clipboard::SwatchChoices;
|
||||
use super::*;
|
||||
@@ -39,14 +39,14 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
),
|
||||
cmd!("edit.undo", "Undo", ["Edit"], Some("Cmd+Z"), "{}", can_undo, undo),
|
||||
cmd!("edit.redo", "Redo", ["Edit"], Some("Cmd+Shift+Z"), "{}", can_redo, redo),
|
||||
cmd!("edit.cut", "Cut", ["Edit"], Some("Cmd+X"), "{}", has_selection, cut),
|
||||
cmd!("edit.copy", "Copy", ["Edit"], Some("Cmd+C"), "{}", has_selection, copy),
|
||||
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.paste",
|
||||
"Paste",
|
||||
["Edit"],
|
||||
Some("Cmd+V"),
|
||||
"{center?: [x, y], dx?, dy?, swatchConflict?} paste centred on `center` (the app passes the view centre), else offset by dx/dy (default: the Paste Offset preference). Pasting brings the image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used), gradient swatches, graphic styles, character and paragraph styles and brushes the objects use; one of the same name that differs comes in renamed. swatchConflict, for a swatch whose name the document gives another colour (clipboard.conflicts): \"merge\" (default: the objects take the document's swatch) | \"add\" (the pasted swatch comes in renamed) | {name: \"merge\"|\"add\"}. With Paste Remembers Layers on (layer.pasteRemembersLayers), objects go back into the layers they came from (by name; made when missing) → {ids, added: resources added, merged: conflicts merged, renamed: [{kind, from, to}]}",
|
||||
"{center?: [x, y], dx?, dy?, swatchConflict?} paste centred on `center` (the app passes the view centre), else offset by dx/dy (default: the Paste Offset preference). Pasting brings the image blobs, symbols, patterns, global and spot swatches (with the tint swatches of the tints used), gradient swatches, graphic styles, character and paragraph styles and brushes the objects use; one of the same name that differs comes in renamed. swatchConflict, for a swatch whose name the document gives another colour (clipboard.conflicts): \"merge\" (default: the objects take the document's swatch) | \"add\" (the pasted swatch comes in renamed) | {name: \"merge\"|\"add\"}. With Paste Remembers Layers on (layer.pasteRemembersLayers), objects go back into the layers they came from (by name; made when missing). An artboard copied with artboard.copy comes with its art, right of the last artboard (the other paste commands but Paste on All Artboards: where it was), its art back in the layers it came from → {ids, artboard: the new artboard's index or null, added: resources added, merged: conflicts merged, renamed: [{kind, from, to}]}",
|
||||
has_clipboard,
|
||||
|s, p| paste(s, p, PasteMode::Offset)
|
||||
),
|
||||
@@ -237,12 +237,19 @@ pub(crate) fn roots_of(doc: &Document, ids: Vec<NodeId>) -> Vec<NodeId> {
|
||||
}
|
||||
|
||||
fn copy(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
// The Artboard tool copies its artboard (and the art on it).
|
||||
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() }))
|
||||
}
|
||||
|
||||
fn cut(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if s.tool_id() == "artboard" {
|
||||
return super::panelcmds::artboard_copy(s, &json!({}), true);
|
||||
}
|
||||
copy(s, p)?;
|
||||
clear(s, &json!({}))
|
||||
}
|
||||
@@ -308,8 +315,16 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
let st = s.doc()?;
|
||||
let same_doc = clip.source_doc == Some(st.uid);
|
||||
let parent = st.insertion_parent();
|
||||
// Paste Remembers Layers (not while isolating a group: pastes stay in it).
|
||||
let remember = st.doc.paste_remembers_layers && parent.is_some_and(|p| st.doc.node(p).is_some_and(Node::is_layer));
|
||||
// A copied artboard comes back with its art: right of the last artboard, or where it was
|
||||
// (Paste in Place, in Front, in Back). Paste on All Artboards pastes only the art.
|
||||
let board = clip
|
||||
.artboard
|
||||
.as_ref()
|
||||
.filter(|_| mode != PasteMode::AllArtboards)
|
||||
.map(|a| (a, if mode == PasteMode::Offset { super::panelcmds::beside_artboards(&st.doc, a.rect) } else { Vec2::ZERO }));
|
||||
// Paste Remembers Layers (not while isolating a group: pastes stay in it); an artboard's art
|
||||
// always goes back into its layers.
|
||||
let remember = (st.doc.paste_remembers_layers || board.is_some()) && parent.is_some_and(|p| st.doc.node(p).is_some_and(Node::is_layer));
|
||||
// Front/back: relative to the selection (top-most / bottom-most selected object); with
|
||||
// nothing selected, the top / bottom of the current layer.
|
||||
let anchor = match mode {
|
||||
@@ -320,19 +335,21 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let placements: Vec<Affine> = match mode {
|
||||
PasteMode::Offset => vec![match point_param(p, "center") {
|
||||
let placements: Vec<Affine> = match (mode, board) {
|
||||
(_, Some((_, dv))) => vec![Affine::translate(dv)],
|
||||
(PasteMode::Offset, None) => vec![match point_param(p, "center") {
|
||||
Some(c) => clip.bounds().map_or(Affine::IDENTITY, |b| Affine::translate(c - b.center())),
|
||||
None => Affine::translate((f64_or(p, "dx", off), f64_or(p, "dy", off))),
|
||||
}],
|
||||
PasteMode::AllArtboards => {
|
||||
(PasteMode::AllArtboards, None) => {
|
||||
let src = clip.source_artboard.or_else(|| st.doc.artboards.first().map(|a| a.rect)).map(|r| r.origin()).unwrap_or_default();
|
||||
st.doc.artboards.iter().map(|a| Affine::translate(a.rect.origin() - src)).collect()
|
||||
}
|
||||
_ => vec![Affine::IDENTITY],
|
||||
};
|
||||
let (ids, imported) = s.edit(mode.label(), |d, sel| {
|
||||
let (ids, imported, artboard) = s.edit(mode.label(), |d, sel| {
|
||||
let imported = clip.import_into(d, choices, same_doc);
|
||||
let artboard = board.map(|(a, dv)| super::panelcmds::push_artboard_copy(d, a, a.rect + dv));
|
||||
let mut layers: BTreeMap<&str, NodeId> = BTreeMap::new();
|
||||
// Objects pasted into each parent so far (keeps their order in front and back pastes).
|
||||
let mut placed: BTreeMap<Option<NodeId>, usize> = BTreeMap::new();
|
||||
@@ -364,10 +381,15 @@ fn paste_clip(s: &mut Session, p: &Value, mode: PasteMode, clip: &Clipboard, cho
|
||||
}
|
||||
}
|
||||
sel.set(new_ids.iter().copied());
|
||||
Ok((new_ids, imported))
|
||||
Ok((new_ids, imported, artboard))
|
||||
})?;
|
||||
// The Artboard tool takes the pasted artboard.
|
||||
if let Some(i) = artboard.filter(|_| s.tool_id() == "artboard") {
|
||||
s.set_tool_option("active", &json!(i));
|
||||
}
|
||||
Ok(json!({
|
||||
"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(),
|
||||
"artboard": artboard,
|
||||
"added": imported.added,
|
||||
"merged": imported.merged,
|
||||
"renamed": imported.renamed,
|
||||
|
||||
@@ -144,6 +144,10 @@ 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 (what Delete and the arrow keys act on).
|
||||
pub fn has_selection_or_guides(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
//! Commands the panels need beyond the core set: artboard reorder/duplicate and the advanced
|
||||
//! Commands the panels need beyond the core set: artboard reorder, duplicate, copy and cut, and the advanced
|
||||
//! Character/Paragraph attributes.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::NodeKind;
|
||||
use vectorcraft_geom::{Rect, Vec2};
|
||||
|
||||
use super::*;
|
||||
use crate::EngineError;
|
||||
use crate::{Clipboard, EngineError};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
@@ -15,10 +16,28 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Duplicate Artboard",
|
||||
["Window", "Artboards"],
|
||||
None,
|
||||
"{index} copy placed right of the last artboard",
|
||||
"{index?: n (default: the Artboard tool's artboard), art?: bool (default: the Artboard tool's moveArt option)} copy placed right of the last artboard, with copies of the art fully inside it when `art` (locked and hidden art only with prefs moveLockedWithArtboard) → {index, ids: the art's copies}",
|
||||
has_doc,
|
||||
artboard_duplicate
|
||||
),
|
||||
cmd!(
|
||||
"artboard.copy",
|
||||
"Copy Artboard",
|
||||
[],
|
||||
None,
|
||||
"{index?, art?} (defaults as artboard.duplicate) put the artboard, and the art fully inside it when `art`, on the clipboard: edit.paste adds a copy right of the last artboard, edit.pasteInPlace/InFront/InBack where it was, in this or another document (one undo step; edit.pasteOnAllArtboards pastes only the art); edit.copy runs this while the Artboard tool is chosen → {copied: objects}",
|
||||
has_doc,
|
||||
|s, p| artboard_copy(s, p, false)
|
||||
),
|
||||
cmd!(
|
||||
"artboard.cut",
|
||||
"Cut Artboard",
|
||||
[],
|
||||
None,
|
||||
"{index?, art?} artboard.copy, then delete the artboard and that art in one undo step (not the only artboard); edit.cut runs this while the Artboard tool is chosen → {copied: objects}",
|
||||
has_doc,
|
||||
|s, p| artboard_copy(s, p, true)
|
||||
),
|
||||
cmd!(
|
||||
"text.setFormat",
|
||||
"Character / Paragraph",
|
||||
@@ -49,24 +68,101 @@ fn artboard_reorder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
fn artboard_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = p.get("index").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
let index = s.edit("Duplicate Artboard", |d, _| {
|
||||
let src = d.artboards.get(i).cloned().ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let right = d.artboards.iter().map(|a| a.rect.x1).fold(f64::MIN, f64::max);
|
||||
let dx = right + 20.0 - src.rect.x0;
|
||||
Ok(push_artboard_copy(d, &src, vectorcraft_geom::Rect::new(src.rect.x0 + dx, src.rect.y0, src.rect.x1 + dx, src.rect.y1)))
|
||||
})?;
|
||||
Ok(json!({"index": index}))
|
||||
/// The artboard (`index`) and whether its art comes along (`art`) for the artboard copy
|
||||
/// commands: by default the Artboard tool's artboard and its Move/Copy Artwork with Artboard option.
|
||||
fn board_and_art(s: &Session, p: &Value) -> (usize, bool) {
|
||||
let t = s.tool_options_of("artboard");
|
||||
let i = p.get("index").and_then(Value::as_u64).or_else(|| t["active"].as_u64()).unwrap_or(0) as usize;
|
||||
(i, bool_or(p, "art", t["moveArt"].as_bool().unwrap_or(true)))
|
||||
}
|
||||
|
||||
/// Add a copy of artboard `src` at `rect`, named `<name> copy` (`<name> copy 2`… when taken),
|
||||
/// with an id of its own → its index.
|
||||
pub(crate) fn push_artboard_copy(d: &mut vectorcraft_doc::Document, src: &vectorcraft_doc::Artboard, rect: vectorcraft_geom::Rect) -> usize {
|
||||
/// The art that goes with artboard `i` when `art`: the objects fully inside it (locked and hidden
|
||||
/// ones only with prefs moveLockedWithArtboard), in paint order.
|
||||
pub(crate) fn artboard_art(s: &Session, i: usize, art: bool) -> Result<Vec<NodeId>> {
|
||||
let d = &s.doc()?.doc;
|
||||
let rect = d.artboards.get(i).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
Ok(if art { d.art_on_artboard(rect, s.prefs.move_locked_with_artboard) } else { vec![] })
|
||||
}
|
||||
|
||||
/// How far a copy of `rect` moves to sit right of the last artboard (Duplicate Artboards, Paste).
|
||||
pub(crate) fn beside_artboards(d: &vectorcraft_doc::Document, rect: Rect) -> Vec2 {
|
||||
let right = d.artboards.iter().map(|a| a.rect.x1).fold(f64::MIN, f64::max);
|
||||
Vec2::new(right + 20.0 - rect.x0, 0.0)
|
||||
}
|
||||
|
||||
fn artboard_duplicate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (i, art) = board_and_art(s, p);
|
||||
let art = artboard_art(s, i, art)?;
|
||||
let scale_strokes = s.prefs.scale_strokes;
|
||||
let (index, ids) = s.edit("Duplicate Artboard", |d, _| {
|
||||
let dv = d.artboards.get(i).map(|a| beside_artboards(d, a.rect)).unwrap_or_default();
|
||||
copy_artboard(d, i, dv, &art, scale_strokes)
|
||||
})?;
|
||||
Ok(json!({"index": index, "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
/// Artboard Copy / Cut: the artboard and its art onto the clipboard (pasting brings both); a cut
|
||||
/// then deletes them.
|
||||
pub(crate) fn artboard_copy(s: &mut Session, p: &Value, cut: bool) -> Result<Value> {
|
||||
let (i, art) = board_and_art(s, p);
|
||||
let art = artboard_art(s, i, art)?;
|
||||
let st = s.doc()?;
|
||||
if cut && st.doc.artboards.len() <= 1 {
|
||||
return Err(EngineError::Other("a document needs at least one artboard".into()));
|
||||
}
|
||||
let board = st.doc.artboards.get(i).cloned().ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let clip = Clipboard { source_artboard: Some(board.rect), artboard: Some(board), ..Clipboard::copy(st, &art) };
|
||||
let copied = clip.nodes.len();
|
||||
if cut {
|
||||
s.edit("Cut Artboard", |d, sel| {
|
||||
if i >= d.artboards.len() {
|
||||
return Err(EngineError::Other("no such artboard".into()));
|
||||
}
|
||||
d.artboards.remove(i);
|
||||
for id in &art {
|
||||
// Always there: none of the art is inside another of it.
|
||||
let _ = d.remove(*id);
|
||||
}
|
||||
sel.prune(d);
|
||||
Ok(())
|
||||
})?;
|
||||
if s.tool_id() == "artboard" {
|
||||
s.set_tool_option("active", &json!(i.saturating_sub(1)));
|
||||
}
|
||||
}
|
||||
s.clipboard = clip;
|
||||
Ok(json!({"copied": copied}))
|
||||
}
|
||||
|
||||
/// Add a copy of artboard `i` moved by `dv`, with copies of its `art` (each just above its
|
||||
/// original) moved along → (the copy's index, the art's copies). Duplicate Artboards and Alt-drag.
|
||||
pub(crate) fn copy_artboard(
|
||||
d: &mut vectorcraft_doc::Document,
|
||||
i: usize,
|
||||
dv: Vec2,
|
||||
art: &[NodeId],
|
||||
scale_strokes: bool,
|
||||
) -> Result<(usize, Vec<NodeId>)> {
|
||||
let src = d.artboards.get(i).cloned().ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let index = push_artboard_copy(d, &src, src.rect + dv);
|
||||
let mut copies = Vec::with_capacity(art.len());
|
||||
for id in art {
|
||||
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);
|
||||
copies.push(d.insert(parent, at + 1, c)?);
|
||||
}
|
||||
Ok((index, copies))
|
||||
}
|
||||
|
||||
/// Add a copy of artboard `src` at `rect`, with an id of its own and its name, or `<name> copy`
|
||||
/// (`<name> copy 2`…) when that is taken → its index.
|
||||
pub(crate) fn push_artboard_copy(d: &mut vectorcraft_doc::Document, src: &vectorcraft_doc::Artboard, rect: Rect) -> usize {
|
||||
let mut a = src.clone();
|
||||
a.id = d.artboards.iter().map(|a| a.id).max().unwrap_or(0).saturating_add(1);
|
||||
let taken = |name: &str| d.artboards.iter().any(|a| a.name == name);
|
||||
a.name = std::iter::once(format!("{} copy", src.name))
|
||||
a.name = [src.name.clone(), format!("{} copy", src.name)]
|
||||
.into_iter()
|
||||
.chain((2u64..).map(|i| format!("{} copy {i}", src.name)))
|
||||
.find(|name| !taken(name))
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -371,35 +371,22 @@ fn sample_color(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn artboard_move(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("artboard.move", "missing index"))? as usize;
|
||||
let dv = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
|
||||
let move_art = bool_or(p, "moveArt", false);
|
||||
let st = s.doc()?;
|
||||
let rect = st.doc.artboards.get(i).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let art = if move_art { st.doc.art_on_artboard(rect, s.prefs.move_locked_with_artboard) } else { vec![] };
|
||||
let art = super::panelcmds::artboard_art(s, i, bool_or(p, "moveArt", false))?;
|
||||
let scale_strokes = s.prefs.scale_strokes;
|
||||
let copy = bool_or(p, "copy", false);
|
||||
let (index, moved) = 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.
|
||||
if !copy {
|
||||
if let Some(a) = d.artboards.get_mut(i) {
|
||||
a.rect = src.rect + dv;
|
||||
}
|
||||
for id in &art {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.transform(Affine::translate(dv), scale_strokes);
|
||||
}
|
||||
}
|
||||
return Ok((i, art.clone()));
|
||||
if copy {
|
||||
return super::panelcmds::copy_artboard(d, i, dv, &art, scale_strokes);
|
||||
}
|
||||
let index = super::panelcmds::push_artboard_copy(d, &src, src.rect + dv);
|
||||
let mut copies = Vec::with_capacity(art.len());
|
||||
let a = d.artboards.get_mut(i).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
a.rect = a.rect + dv;
|
||||
for id in &art {
|
||||
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);
|
||||
copies.push(d.insert(parent, at + 1, c)?);
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.transform(Affine::translate(dv), scale_strokes);
|
||||
}
|
||||
}
|
||||
Ok((index, copies))
|
||||
Ok((i, art.clone()))
|
||||
})?;
|
||||
Ok(json!({ "index": index, "moved": moved.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
@@ -87,6 +87,109 @@ fn artboards_reorder_and_duplicate() {
|
||||
assert_eq!(s.doc().unwrap().doc.artboards[0].name, "Second");
|
||||
}
|
||||
|
||||
/// The artboards' left edges and names.
|
||||
fn boards(s: &Session) -> Vec<(f64, String)> {
|
||||
s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.name.clone())).collect()
|
||||
}
|
||||
|
||||
/// The left edge of object `id`.
|
||||
fn left(s: &Session, id: u64) -> f64 {
|
||||
s.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap().x0
|
||||
}
|
||||
|
||||
fn ids(v: &serde_json::Value) -> Vec<u64> {
|
||||
v.as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
|
||||
}
|
||||
|
||||
/// Duplicate Artboards (#446) copies the art on the artboard too, as the Artboard tool's Move/Copy
|
||||
/// Artwork with Artboard says, in one undo step.
|
||||
#[test]
|
||||
fn duplicate_artboard_copies_its_art() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s);
|
||||
let out = s.execute("artboard.duplicate", &json!({"index": 0})).unwrap();
|
||||
assert_eq!(out["index"], 1);
|
||||
let copies = ids(&out["ids"]);
|
||||
assert_eq!(copies.len(), 1);
|
||||
assert_ne!(copies[0], r.0);
|
||||
assert_eq!(boards(&s), vec![(0.0, "Artboard 1".into()), (820.0, "Artboard 1 copy".into())]);
|
||||
assert_eq!(left(&s, copies[0]), 830.0, "the copy sits on the new artboard as the original on its own");
|
||||
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), "one undo step");
|
||||
// Off (the tool's option or the param): the artboard alone.
|
||||
s.set_tool_options(Some("artboard"), json!({"moveArt": false}).as_object().unwrap());
|
||||
assert_eq!(s.execute("artboard.duplicate", &json!({})).unwrap()["ids"], json!([]));
|
||||
assert_eq!(ids(&s.execute("artboard.duplicate", &json!({"index": 0, "art": true})).unwrap()["ids"]).len(), 1);
|
||||
}
|
||||
|
||||
/// With the Artboard tool, Copy and Paste copy its artboard with the art on it (#446): Paste places
|
||||
/// it right of the last artboard, Paste in Place where it was, also in another document; each paste
|
||||
/// is one undo step, and the tool takes the pasted artboard.
|
||||
#[test]
|
||||
fn artboard_tool_copy_and_paste_artboards_with_their_art() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s);
|
||||
s.execute("artboard.new", &json!({"x": 0, "y": 700, "width": 100, "height": 100, "name": "Small"})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
// Nothing selected: Copy is off, until the Artboard tool is chosen.
|
||||
let copy_enabled = |s: &Session| (crate::find_command("edit.copy").unwrap().enabled)(s).is_ok();
|
||||
assert!(!copy_enabled(&s));
|
||||
s.select_tool("artboard", ViewInfo::default()).unwrap();
|
||||
assert!(copy_enabled(&s));
|
||||
assert_eq!(s.execute("edit.copy", &json!({})).unwrap()["copied"], 1, "artboard 1 and its rectangle");
|
||||
let out = s.execute("edit.paste", &json!({"center": [5, 5]})).unwrap();
|
||||
assert_eq!(out["artboard"], 2);
|
||||
let pasted = ids(&out["ids"]);
|
||||
assert_eq!(left(&s, pasted[0]), 830.0, "beside the artboards, not at the view centre");
|
||||
assert_eq!(boards(&s)[2], (820.0, "Artboard 1 copy".into()));
|
||||
assert_eq!(s.tool_options()["active"], 2);
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.selection.in_paint_order(&st.doc), vec![NodeId(pasted[0])]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(boards(&s).len(), 2);
|
||||
assert!(s.doc().unwrap().doc.node(NodeId(pasted[0])).is_none(), "one undo step");
|
||||
// In place: on top of the original.
|
||||
let out = s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
assert_eq!(left(&s, ids(&out["ids"])[0]), 10.0);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!(d.artboards[2].rect, d.artboards[0].rect);
|
||||
// Into another document.
|
||||
s.execute("file.new", &json!({"width": 300, "height": 300})).unwrap();
|
||||
let out = s.execute("edit.paste", &json!({})).unwrap();
|
||||
assert_eq!(boards(&s), vec![(0.0, "Artboard 1".into()), (320.0, "Artboard 1 copy".into())]);
|
||||
let n = s.doc().unwrap().doc.node(NodeId(ids(&out["ids"])[0])).unwrap().geometric_bounds().unwrap();
|
||||
assert_eq!((n.x0, n.width()), (330.0, 100.0));
|
||||
// Paste on All Artboards pastes the art alone.
|
||||
let out = s.execute("edit.pasteOnAllArtboards", &json!({})).unwrap();
|
||||
assert_eq!((out["artboard"].clone(), ids(&out["ids"]).len(), boards(&s).len()), (json!(null), 2, 2));
|
||||
// The clipboard still holds the artboard for the next document, and the original is untouched.
|
||||
s.execute("document.activate", &json!({"index": 0})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.node(r).is_some());
|
||||
}
|
||||
|
||||
/// With the Artboard tool, Cut takes its artboard and the art on it away in one undo step (#446),
|
||||
/// never the only artboard; pasting it back keeps its name.
|
||||
#[test]
|
||||
fn artboard_tool_cut_removes_the_artboard_and_its_art() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s);
|
||||
let outside = s.execute("shape.rectangle", &json!({"x": 2000, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||||
s.select_tool("artboard", ViewInfo::default()).unwrap();
|
||||
assert!(s.execute("edit.cut", &json!({})).is_err(), "the only artboard");
|
||||
s.execute("artboard.new", &json!({"x": 1000, "y": 0, "width": 100, "height": 100})).unwrap();
|
||||
s.set_tool_option("active", &json!(0));
|
||||
s.execute("edit.cut", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!((d.artboards.len(), d.node(r).is_none(), d.node(NodeId(outside)).is_some()), (1, true, true));
|
||||
s.execute("edit.pasteInPlace", &json!({})).unwrap();
|
||||
assert_eq!(boards(&s), vec![(1000.0, "Artboard 2".into()), (0.0, "Artboard 1".into())]);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
assert_eq!((d.artboards.len(), d.node(r).is_some()), (2, true), "the cut is one undo step");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swatch_groups_duplicate_sort() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! 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. Moving and resizing snap like drawing
|
||||
//! the active one (Copy, Cut and Paste take it with its art: `artboard.copy`) and Escape returns to
|
||||
//! the Selection tool. Moving and resizing snap like drawing
|
||||
//! does: to whole pixels, to the grid, or with Smart Guides to other artboards, their bleed and
|
||||
//! objects (never to the dragged artboard or the art moving with it); the artboard's own bleed edges
|
||||
//! snap too.
|
||||
|
||||
@@ -695,6 +695,8 @@
|
||||
Duplicate Duplicar
|
||||
Duplicate and Offset 10 pt Duplicar y desplazar 10 pt
|
||||
Duplicate Artboard Duplicar mesa de trabajo
|
||||
Copy Artboard Copiar mesa de trabajo
|
||||
Cut Artboard Cortar mesa de trabajo
|
||||
Duplicate Artboards Duplicar mesas de trabajo
|
||||
Duplicate Brush Duplicar pincel
|
||||
Duplicate Character Style Duplicar estilo de carácter
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1116,6 +1116,8 @@
|
||||
Drawing Mode 描画モード
|
||||
Duplicate 複製
|
||||
Duplicate Artboard アートボードを複製
|
||||
Copy Artboard アートボードをコピー
|
||||
Cut Artboard アートボードをカット
|
||||
Duplicate Artboards アートボードを複製
|
||||
Duplicate Brush ブラシを複製
|
||||
Duplicate Character Style 文字スタイルを複製
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -666,6 +666,8 @@
|
||||
Duotone Gradients Gradientes Duotônicos
|
||||
Duplicate Duplicar
|
||||
Duplicate Artboard Duplicar Mesa de Trabalho
|
||||
Copy Artboard Copiar Mesa de Trabalho
|
||||
Cut Artboard Recortar Mesa de Trabalho
|
||||
Duplicate Artboards Duplicar Mesas de Trabalho
|
||||
Duplicate Brush Duplicar Pincel
|
||||
Duplicate Character Style Duplicar Estilo de Caractere
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -690,6 +690,8 @@
|
||||
Duplicate 复制
|
||||
Duplicate and Offset 10 pt 复制并偏移 10 pt
|
||||
Duplicate Artboard 复制画板
|
||||
Copy Artboard 拷贝画板
|
||||
Cut Artboard 剪切画板
|
||||
Duplicate Artboards 复制画板
|
||||
Duplicate Brush 复制画笔
|
||||
Duplicate Character Style 制作字符样式副本
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -708,6 +708,8 @@
|
||||
Duplicate 製作複本
|
||||
Duplicate and Offset 10 pt 製作複本並位移 10 pt
|
||||
Duplicate Artboard 製作工作區域複本
|
||||
Copy Artboard 複製工作區域
|
||||
Cut Artboard 剪下工作區域
|
||||
Duplicate Artboards 製作工作區域複本
|
||||
Duplicate Brush 製作筆刷複本
|
||||
Duplicate Character Style 製作字元樣式複本
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1,5 +1,6 @@
|
||||
//! Artboards panel: numbered list with inline rename (double-click the name), move up / down, new,
|
||||
//! delete. The highlighted row is the active artboard, the one the status bar's navigator shows and
|
||||
//! delete; a row dragged onto New Artboard duplicates its artboard (as Duplicate Artboards does, with
|
||||
//! its art when the Artboard tool's Move/Copy Artwork with Artboard is on). The highlighted row is the active artboard, the one the status bar's navigator shows and
|
||||
//! Fit Artboard in Window fits: clicking a row makes it active, double-clicking its number also
|
||||
//! fits it in the window.
|
||||
|
||||
@@ -27,6 +28,19 @@ fn select(app: &mut VectorcraftApp, i: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A row dragged in the list: the artboard's index.
|
||||
#[derive(Clone, Copy)]
|
||||
struct RowDrag(usize);
|
||||
|
||||
/// Duplicate artboard `i` (`artboard.duplicate`) and make the copy the active artboard.
|
||||
fn duplicate(app: &mut VectorcraftApp, i: usize) {
|
||||
if let Ok(r) = app.run("artboard.duplicate", json!({ "index": i }))
|
||||
&& let Some(copy) = r["index"].as_u64()
|
||||
{
|
||||
select(app, copy as usize);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let abs: Vec<String> = app.session.active().map(|d| d.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default();
|
||||
@@ -41,7 +55,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.spacing_mut().item_spacing.y = 0.0;
|
||||
egui::ScrollArea::vertical().id_salt("ab-scroll").max_height(220.0).show(ui, |ui| {
|
||||
for (i, name) in abs.iter().enumerate() {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click_and_drag());
|
||||
if resp.drag_started() {
|
||||
egui::DragAndDrop::set_payload(ui.ctx(), RowDrag(i));
|
||||
}
|
||||
if i == sel {
|
||||
ui.painter().rect_filled(r, 0.0, t.row_selected);
|
||||
} else if resp.hovered() {
|
||||
@@ -108,7 +125,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
{
|
||||
select(app, sel + 1);
|
||||
}
|
||||
if widgets::icon_button(ui, "dc-new-item", tl!("New Artboard"), false, 24.0).clicked() && app.run("artboard.new", json!({})).is_ok() {
|
||||
let new = widgets::icon_button(ui, "dc-new-item", tl!("New Artboard"), false, 24.0);
|
||||
if new.dnd_hover_payload::<RowDrag>().is_some() {
|
||||
ui.painter().rect_stroke(new.rect, 3.0, egui::Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
if let Some(row) = new.dnd_release_payload::<RowDrag>() {
|
||||
duplicate(app, row.0);
|
||||
} else if new.clicked() && app.run("artboard.new", json!({})).is_ok() {
|
||||
select(app, n);
|
||||
}
|
||||
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Artboard"), false, n > 1, 24.0).clicked() {
|
||||
@@ -124,7 +147,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
app.run("artboard.new", json!({})).ok();
|
||||
}
|
||||
if menu_item(ui, tl!("Duplicate Artboards"), n > 0, false) {
|
||||
app.run("artboard.duplicate", json!({"index": sel})).ok();
|
||||
duplicate(app, sel);
|
||||
}
|
||||
if menu_item(ui, tl!("Delete Artboards"), n > 1, false) {
|
||||
app.run("artboard.delete", json!({"index": sel})).ok();
|
||||
@@ -240,4 +263,42 @@ mod tests {
|
||||
}
|
||||
assert_eq!((app.session.tool_options()["active"].clone(), selected(&app, 2)), (json!(0), 0));
|
||||
}
|
||||
|
||||
/// A row dragged onto New Artboard duplicates its artboard with its art (#446), outlining the
|
||||
/// button while it is held over it, and the copy becomes the active artboard.
|
||||
#[test]
|
||||
fn dragging_a_row_onto_new_artboard_duplicates_it() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 150})).unwrap();
|
||||
app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let screen = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(240.0, 400.0)));
|
||||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: screen, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes
|
||||
};
|
||||
let shapes = frame(&mut app, vec![]);
|
||||
let t = Tokens::get(&ctx);
|
||||
let rect_of = |fill| {
|
||||
shapes.iter().find_map(|c| match &c.shape {
|
||||
egui::Shape::Rect(r) if r.fill == fill => Some(r.rect),
|
||||
_ => None,
|
||||
})
|
||||
};
|
||||
let row = rect_of(t.row_selected).unwrap().center();
|
||||
// The bottom bar's buttons sit under its divider, New Artboard second from the right.
|
||||
let bar = rect_of(t.divider).unwrap();
|
||||
let new = pos2(bar.right() - 24.0 - 4.0 - 12.0, bar.bottom() + 2.0 + 12.0);
|
||||
let button =
|
||||
|at, pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(row), button(row, true)]);
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(row + vec2(0.0, 12.0))]);
|
||||
let held = frame(&mut app, vec![egui::Event::PointerMoved(new)]);
|
||||
assert!(held.iter().any(|c| matches!(&c.shape, egui::Shape::Rect(r) if r.stroke.color == t.accent)), "New Artboard is outlined");
|
||||
frame(&mut app, vec![button(new, false)]);
|
||||
let d = &app.session.active().unwrap().doc;
|
||||
assert_eq!((d.artboards.len(), d.layers[0].children().unwrap().len()), (2, 2), "the artboard and its art");
|
||||
assert_eq!(app.view().unwrap().artboard, 1);
|
||||
}
|
||||
}
|
||||
+14
@@ -1328,7 +1328,21 @@ on top / at the bottom of the current layer. `edit.pasteOnAllArtboards` keeps th
|
||||
copied from. `layer.pasteRemembersLayers {on?}` (a document option, `document.inspect` → `pasteRemembersLayers`)
|
||||
pastes objects back into the layers they came from, by name, making missing ones.
|
||||
|
||||
Artboards copy with their art. `artboard.copy {index?, art?}` puts an artboard on the clipboard together with the
|
||||
art fully inside it (`art` defaults to the Artboard tool's `moveArt` option, Move/Copy Artwork with Artboard; locked
|
||||
and hidden art only with prefs `moveLockedWithArtboard`); with the Artboard tool chosen, `edit.copy` does this for
|
||||
the tool's artboard and `edit.cut` runs `artboard.cut`, which also deletes them (never the only artboard). Then
|
||||
`edit.paste` adds a copy of the artboard right of the last one, with its art in the layers it came from, in one undo
|
||||
step (`edit.pasteInPlace`, `edit.pasteInFront` and `edit.pasteInBack` put it where it was; `edit.pasteOnAllArtboards`
|
||||
pastes only the art), in this document or another; the result's `artboard` is the new artboard's index.
|
||||
`artboard.duplicate {index?, art?}` (Window › Artboards › Duplicate Artboards, or a row dragged onto New Artboard)
|
||||
does the same in one step → `{index, ids}`, and `artboard.move {copy: true, moveArt: true}` is the Artboard tool's
|
||||
Alt-drag.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"artboard.copy","params":{"index":0}}}
|
||||
{"name":"run_command","arguments":{"command":"edit.paste","params":{}}}
|
||||
{"name":"run_command","arguments":{"command":"artboard.duplicate","params":{"index":0,"art":true}}}
|
||||
{"name":"run_command","arguments":{"command":"clipboard.conflicts","params":{}}}
|
||||
{"name":"run_command","arguments":{"command":"edit.paste","params":{"center":[300,200],"swatchConflict":{"Brand":"add"}}}}
|
||||
{"name":"run_command","arguments":{"command":"layer.pasteRemembersLayers","params":{"on":true}}}
|
||||
|
||||
Reference in new issue
Block a user