mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Artboards: Width, Height, Preset and Orientation apply to every selected artboard (#2565)
This commit is contained in:
@@ -143,7 +143,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Artboard Options…",
|
||||
["Window", "Artboards"],
|
||||
None,
|
||||
"{index, name?, x?, y?, width?, height?, moveArt?: bool (default false; a position-only change moves the contained art with its artboard), scaleArt?: bool (Scale Artwork with Artboard: resized, the artboard takes the art fully inside it (locked and hidden art only with lockedAndHidden?: bool, default prefs moveLockedWithArtboard) and its guides from its old rectangle onto the new one, each side by its own ratio; strokes/corners/patterns as object.scale), strokes?: bool, corners?: bool, patterns?: bool (the three as object.scale, for art that moves or scales with the artboard), lockedAndHidden?: bool (locked and hidden art moves along; default: the preference; recorded in the journal so a replay matches)} → null, or with scaleArt {scaled: the art scaled}",
|
||||
"{index | artboards: [{index, name?, x?, y?, width?, height?}…] (several artboards in one undo step, each with its own fields; the art inside each before any of them changes goes with it, and an object inside several goes with the first listed that moves or scales its art), name?, x?, y?, width?, height?, moveArt?: bool (default false; a position-only change moves the contained art with its artboard), scaleArt?: bool (Scale Artwork with Artboard: resized, the artboard takes the art fully inside it (locked and hidden art only with lockedAndHidden?: bool, default prefs moveLockedWithArtboard) and its guides from its old rectangle onto the new one, each side by its own ratio; strokes/corners/patterns as object.scale), strokes?: bool, corners?: bool, patterns?: bool (the three as object.scale, for art that moves or scales with the artboard), lockedAndHidden?: bool (locked and hidden art moves along; default: the preference; recorded in the journal so a replay matches)} → null, or with scaleArt {scaled: the art scaled}",
|
||||
has_doc,
|
||||
artboard_set
|
||||
),
|
||||
@@ -834,59 +834,94 @@ fn artboard_delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
/// One artboard's part of `artboard.setProps`: its index, new rectangle and name, how far it moved,
|
||||
/// whether it was resized, the map its art and guides take when they go with it (`fit` when it
|
||||
/// scales them), and that art.
|
||||
struct ArtboardEdit<'a> {
|
||||
index: usize,
|
||||
now: Rect,
|
||||
name: Option<&'a str>,
|
||||
delta: vectorcraft_geom::Vec2,
|
||||
resized: bool,
|
||||
fit: Option<Affine>,
|
||||
transform: Option<Affine>,
|
||||
art: Vec<NodeId>,
|
||||
}
|
||||
|
||||
fn artboard_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = super::panelcmds::artboard_index(&s.doc()?.doc, p, "artboard.setProps")?;
|
||||
let was = s.doc()?.doc.artboards.get(i).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let now = rect_from(p, was);
|
||||
let resized = (now.width() - was.width()).abs() > 1e-6 || (now.height() - was.height()).abs() > 1e-6;
|
||||
// Scale Artwork with Artboard: resized, it takes the art fully inside it (as Move Artwork with
|
||||
// Artboard gathers it) and its guides from the old rectangle onto the new one.
|
||||
let scale_art = bool_or(p, "scaleArt", false);
|
||||
let fit = if scale_art && resized { rect_map(was, now) } else { None };
|
||||
// Position fields honor Move Artwork independently of Scale Artwork. A resize still
|
||||
// leaves the art alone unless scaling was requested. Fit-to-art callers keep the default.
|
||||
let delta = now.origin() - was.origin();
|
||||
let moving = !resized && bool_or(p, "moveArt", false) && delta != vectorcraft_geom::Vec2::ZERO;
|
||||
let transform = fit.or_else(|| moving.then(|| Affine::translate(delta)));
|
||||
let art = if transform.is_some() {
|
||||
// Locked and hidden art too? Noted in the journal, so a replay transforms the same objects
|
||||
// whatever the preference is then.
|
||||
let all = bool_or(p, "lockedAndHidden", s.prefs.move_locked_with_artboard);
|
||||
s.note_journal("lockedAndHidden", json!(all));
|
||||
s.doc()?.doc.art_on_artboard(was, all)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let mut sc = match fit {
|
||||
Some(xf) if Scaling::factor(xf).is_some() => super::object::scaling(s, p),
|
||||
_ => Scaling::default(),
|
||||
};
|
||||
sc.patterns = super::object::transform_patterns(s, p, &art)?;
|
||||
s.edit("Artboard Options", |d, _| {
|
||||
let a = d.artboards.get_mut(i).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
a.rect = now;
|
||||
if let Some(n) = str_param(p, "name").map(str::trim) {
|
||||
// A blank name gives the artboard its default name, Artboard N for the Nth artboard.
|
||||
a.name = if n.is_empty() { format!("Artboard {}", i + 1) } else { n.to_string() };
|
||||
// One artboard (`index` and its fields), or several (`artboards`, each with its own fields).
|
||||
let entries: Vec<&Value> = match p.get("artboards") {
|
||||
Some(list) => {
|
||||
list.as_array().filter(|l| !l.is_empty()).ok_or_else(|| bad("artboard.setProps", "artboards must be a non-empty list"))?.iter().collect()
|
||||
}
|
||||
let id = a.id;
|
||||
if let Some(xf) = transform {
|
||||
d.map_artboard_guides(id, xf);
|
||||
for id in &art {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.transform(xf, sc);
|
||||
None => vec![p],
|
||||
};
|
||||
// Scale Artwork with Artboard: resized, an artboard takes the art fully inside it (as Move
|
||||
// Artwork with Artboard gathers it) and its guides from the old rectangle onto the new one.
|
||||
let scale_art = bool_or(p, "scaleArt", false);
|
||||
// Locked and hidden art too?
|
||||
let all = bool_or(p, "lockedAndHidden", s.prefs.move_locked_with_artboard);
|
||||
let d = &s.doc()?.doc;
|
||||
// Each artboard's art is the art inside it before any of them changes; an object inside
|
||||
// several of them goes with the first one listed that moves or scales its art.
|
||||
let mut taken = HashSet::new();
|
||||
let mut edits: Vec<ArtboardEdit> = Vec::with_capacity(entries.len());
|
||||
for q in entries {
|
||||
let index = super::panelcmds::artboard_index(d, q, "artboard.setProps")?;
|
||||
if edits.iter().any(|e| e.index == index) {
|
||||
return Err(bad("artboard.setProps", "an artboard is listed twice"));
|
||||
}
|
||||
let was = d.artboards.get(index).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
let now = rect_from(q, was);
|
||||
let resized = (now.width() - was.width()).abs() > 1e-6 || (now.height() - was.height()).abs() > 1e-6;
|
||||
let fit = if scale_art && resized { rect_map(was, now) } else { None };
|
||||
// Position fields honor Move Artwork independently of Scale Artwork. A resize still
|
||||
// leaves the art alone unless scaling was requested. Fit-to-art callers keep the default.
|
||||
let delta = now.origin() - was.origin();
|
||||
let moving = !resized && bool_or(p, "moveArt", false) && delta != vectorcraft_geom::Vec2::ZERO;
|
||||
let transform = fit.or_else(|| moving.then(|| Affine::translate(delta)));
|
||||
let art = if transform.is_some() { d.art_on_artboard(was, all).into_iter().filter(|id| taken.insert(*id)).collect() } else { vec![] };
|
||||
edits.push(ArtboardEdit { index, now, name: str_param(q, "name").map(str::trim), delta, resized, fit, transform, art });
|
||||
}
|
||||
// Noted in the journal when art goes along, so a replay transforms the same objects whatever
|
||||
// the preference is then.
|
||||
if edits.iter().any(|e| e.transform.is_some()) {
|
||||
s.note_journal("lockedAndHidden", json!(all));
|
||||
}
|
||||
let scaling =
|
||||
if edits.iter().any(|e| e.fit.is_some_and(|xf| Scaling::factor(xf).is_some())) { super::object::scaling(s, p) } else { Scaling::default() };
|
||||
let art: Vec<NodeId> = edits.iter().flat_map(|e| e.art.iter().copied()).collect();
|
||||
let patterns = super::object::transform_patterns(s, p, &art)?;
|
||||
s.edit("Artboard Options", |d, _| {
|
||||
for e in &edits {
|
||||
let a = d.artboards.get_mut(e.index).ok_or_else(|| EngineError::Other("no such artboard".into()))?;
|
||||
a.rect = e.now;
|
||||
if let Some(n) = e.name {
|
||||
// A blank name gives the artboard its default name, Artboard N for the Nth artboard.
|
||||
a.name = if n.is_empty() { format!("Artboard {}", e.index + 1) } else { n.to_string() };
|
||||
}
|
||||
let id = a.id;
|
||||
if let Some(xf) = e.transform {
|
||||
let mut sc = if e.fit.is_some_and(|xf| Scaling::factor(xf).is_some()) { scaling } else { Scaling::default() };
|
||||
sc.patterns = patterns;
|
||||
d.map_artboard_guides(id, xf);
|
||||
for id in &e.art {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.transform(xf, sc);
|
||||
}
|
||||
}
|
||||
} else if !e.resized {
|
||||
// Moved (not resized), it takes its guides along.
|
||||
d.move_artboard_guides(id, e.delta);
|
||||
}
|
||||
} else if !resized {
|
||||
// Moved (not resized), it takes its guides along.
|
||||
d.move_artboard_guides(id, delta);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if !scale_art {
|
||||
return ok();
|
||||
}
|
||||
Ok(json!({ "scaled": if fit.is_some() { art.iter().map(|i| i.0).collect::<Vec<_>>() } else { vec![] } }))
|
||||
let scaled: Vec<_> = edits.iter().filter(|e| e.fit.is_some()).flat_map(|e| e.art.iter().map(|i| i.0)).collect();
|
||||
Ok(json!({ "scaled": scaled }))
|
||||
}
|
||||
|
||||
/// The map taking artboard rectangle `from` onto `to`, each side scaled by its own ratio. None
|
||||
|
||||
@@ -816,6 +816,50 @@ fn artboard_set_props_scales_art_only_when_asked_and_resized() {
|
||||
assert!(s.execute("artboard.setProps", &json!({"index": 9, "width": 10, "scaleArt": true})).is_err());
|
||||
}
|
||||
|
||||
/// #1482: `artboard.setProps {artboards}` resizes several artboards in one undo step, and with
|
||||
/// `scaleArt` each object scales once, with the artboard it was inside before the change: art that
|
||||
/// the first artboard's new size moves into the second stays as the first one put it, and art
|
||||
/// inside two overlapping artboards goes with the first one listed. An empty list, an artboard
|
||||
/// listed twice, an entry without an index and an index that names no artboard are refused.
|
||||
#[test]
|
||||
fn artboard_set_props_resizes_several_artboards_and_scales_each_object_once() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 100, "artboards": 2, "created": null})).unwrap();
|
||||
s.execute("artboard.setProps", &json!({"index": 0, "x": 0, "y": 0})).unwrap();
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "x": 120, "y": 0})).unwrap();
|
||||
let art = rect(&mut s, 70.0, 10.0, 10.0, 10.0);
|
||||
let before = s.doc().unwrap().doc.clone();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let out = s
|
||||
.execute(
|
||||
"artboard.setProps",
|
||||
&json!({"artboards": [{"index": 0, "width": 200}, {"index": 1, "width": 200, "name": " Back\t"}], "scaleArt": true}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(out["scaled"], json!([art.0]));
|
||||
let boards = &s.doc().unwrap().doc.artboards;
|
||||
assert_eq!((boards[0].rect, boards[1].rect), (Rect::new(0.0, 0.0, 200.0, 100.0), Rect::new(120.0, 0.0, 320.0, 100.0)));
|
||||
assert_eq!(boards[1].name, "Back");
|
||||
assert_eq!(bounds(&s, art), Rect::new(140.0, 10.0, 160.0, 20.0), "scaled once, with Artboard 1");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(*s.doc().unwrap().doc, *before);
|
||||
// Artboard 2 over Artboard 1's right half: the art inside both goes with Artboard 2, listed first.
|
||||
s.execute("artboard.setProps", &json!({"index": 1, "x": 50})).unwrap();
|
||||
s.execute("artboard.setProps", &json!({"artboards": [{"index": 1, "width": 200}, {"index": 0, "width": 200}], "scaleArt": true})).unwrap();
|
||||
assert_eq!(bounds(&s, art), Rect::new(90.0, 10.0, 110.0, 20.0));
|
||||
let was = s.doc().unwrap().doc.clone();
|
||||
for bad in [
|
||||
json!({"artboards": []}),
|
||||
json!({"artboards": [{"index": 0, "width": 50}, {"index": 0, "height": 50}]}),
|
||||
json!({"artboards": [{"index": 0, "width": 50}, {"height": 50}]}),
|
||||
json!({"artboards": [{"index": 0, "width": 50}, {"index": 2, "height": 50}]}),
|
||||
] {
|
||||
assert!(s.execute("artboard.setProps", &bad).is_err(), "{bad}");
|
||||
assert_eq!(*s.doc().unwrap().doc, *was, "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Move Artwork is independent of scaling and takes only fully contained art and board guides.
|
||||
#[test]
|
||||
fn coordinate_moves_honor_move_art_and_keep_resize_behavior() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Artboard Options (double-click with the Artboard tool, double-click the tool, or the Artboards
|
||||
//! panel's options icon or menu): the artboard's fields, sizes parsed with units, and its position
|
||||
//! from a reference point.
|
||||
//! from a reference point. A new size also goes to the other selected artboards.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
@@ -31,10 +31,12 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
form::grid_then(ui, d, unit, |ui, d| {
|
||||
let (w, h) = (d.f64("width", 0.0), d.f64("height", 0.0));
|
||||
let n = artboard_rect(was, refi, None, None, Some(w), Some(h), false);
|
||||
// Only the side that changed moves, so an untouched coordinate stays exactly as it was.
|
||||
for (key, changed, at) in [("x", w != w0, n.x0), ("y", h != h0, n.y0)] {
|
||||
// Only the side that changed moves, so an untouched coordinate stays exactly as it was. The
|
||||
// reference point's column or row is kept for the other selected artboards (`confirm`).
|
||||
for (key, changed, at, part) in [("x", w != w0, n.x0, refi % 3), ("y", h != h0, n.y0, refi / 3)] {
|
||||
if changed {
|
||||
d.fields.insert(key.into(), json!(at));
|
||||
d.fields.insert(format!("__ref{key}"), json!(part));
|
||||
}
|
||||
}
|
||||
position_rows(ui, d, refi, unit, origin);
|
||||
@@ -92,6 +94,10 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
Ok(json!({ "dialog": KIND }))
|
||||
}
|
||||
|
||||
/// OK sets the artboard's fields. A Width or Height that differs from the artboard's also goes to
|
||||
/// the other selected artboards (when this one is among them), each resized about the reference
|
||||
/// point the form's last Width or Height change used (fields `__refx` and `__refy`, its column and
|
||||
/// row; the top-left corner when the form set none), all in one undo step.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
if d.bool("new") {
|
||||
return confirm_new(app, d);
|
||||
@@ -102,7 +108,27 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
p[k] = json!(d.f64(k, 0.0));
|
||||
}
|
||||
}
|
||||
p["moveArt"] = json!(crate::panels::artboards::move_art(app));
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
o.retain(|k, _| !k.starts_with("__"));
|
||||
}
|
||||
let index = p["index"].as_u64().and_then(|i| usize::try_from(i).ok()).unwrap_or(0);
|
||||
let was = app.session.active().and_then(|st| st.doc.artboards.get(index)).map(|a| a.rect);
|
||||
let changed = |k: &str, old: Option<f64>| p[k].as_f64().filter(|v| old.is_some_and(|old| (v - old).abs() > 1e-6));
|
||||
let (w, h) = (changed("width", was.map(|r| r.width())), changed("height", was.map(|r| r.height())));
|
||||
let part = |k: &str| d.fields.get(k).and_then(Value::as_u64).map_or(0, |v| v.min(2) as usize);
|
||||
let refi = part("__refy") * 3 + part("__refx");
|
||||
let mut entries = vec![];
|
||||
if w.is_some() || h.is_some() {
|
||||
for i in crate::panels::artboards::resized_with(app, index).into_iter().filter(|i| *i != index) {
|
||||
if let Some(r) = app.session.active().and_then(|st| st.doc.artboards.get(i)).map(|a| a.rect) {
|
||||
let n = artboard_rect(r, refi, None, None, w, h, false);
|
||||
entries.push(json!({"index": i, "x": n.x0, "y": n.y0, "width": n.width(), "height": n.height()}));
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.insert(0, p);
|
||||
let common = json!({"moveArt": crate::panels::artboards::move_art(app)});
|
||||
let p = crate::panels::artboards::set_props_params(common, entries).ok_or("no artboard")?;
|
||||
run_and_close(app, "artboard.setProps", p)
|
||||
}
|
||||
|
||||
|
||||
@@ -1031,6 +1031,72 @@ fn artboard_options_position_follows_the_tool_option_with_another_tool() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #1482: with Artboards 1 and 3 selected with the Artboard tool, a new Width in Artboard Options
|
||||
/// goes to both, each keeping its top-left corner and its height, in one undo step. The new name
|
||||
/// goes only to the artboard the dialog shows, and Artboard 2 keeps its size. A name alone resizes
|
||||
/// nothing.
|
||||
#[test]
|
||||
fn artboard_options_width_resizes_every_selected_artboard() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||||
app.run("artboard.setProps", json!({"index": 2, "width": 300, "height": 150})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
crate::panels::artboards::select(&mut app, 0);
|
||||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||||
let boards = |app: &VectorcraftApp| app.session.doc().unwrap().doc.artboards.clone();
|
||||
let before = boards(&app);
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||||
let fields = &mut app.ui.dialog.as_mut().unwrap().fields;
|
||||
fields.insert("width".into(), json!("250 pt"));
|
||||
fields.insert("name".into(), json!("Cover"));
|
||||
frame(&mut app, Default::default());
|
||||
super::confirm(&mut app).unwrap();
|
||||
let mut want = before.clone();
|
||||
want[0].name = "Cover".into();
|
||||
for i in [0, 2] {
|
||||
want[i].rect.x1 = want[i].rect.x0 + 250.0;
|
||||
}
|
||||
assert_eq!(boards(&app), want);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(boards(&app), before);
|
||||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Cover"));
|
||||
frame(&mut app, Default::default());
|
||||
super::confirm(&mut app).unwrap();
|
||||
want = before.clone();
|
||||
want[0].name = "Cover".into();
|
||||
assert_eq!(boards(&app), want, "a name alone");
|
||||
}
|
||||
|
||||
/// #1482 with #1876: a Width typed in Artboard Options resizes each selected artboard about the
|
||||
/// reference point the form uses, the center by default, as the Transform panel's W does.
|
||||
#[test]
|
||||
fn artboard_options_width_resizes_every_selected_artboard_about_the_reference_point() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300, "artboards": 2})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
crate::panels::artboards::select(&mut app, 0);
|
||||
app.session.set_tool_option("selected", &json!([0, 1]));
|
||||
let before: Vec<vectorcraft_geom::Rect> = app.session.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect();
|
||||
crate::dialogs::artboard_options::open(&mut app).unwrap();
|
||||
let mut f = Frames::new();
|
||||
let shown = f.texts(&mut app);
|
||||
let (width, height) = (field_after(&shown, "Width:"), field_after(&shown, "Height:"));
|
||||
f.press(&mut app, width, true);
|
||||
f.press(&mut app, width, false);
|
||||
f.run(&mut app, vec![egui::Event::Text("500".into())]);
|
||||
// A click on Height ends the Width field's editing.
|
||||
f.press(&mut app, height, true);
|
||||
f.press(&mut app, height, false);
|
||||
f.key(&mut app, egui::Key::Enter, egui::Modifiers::NONE);
|
||||
assert!(app.ui.dialog.is_none(), "Enter applies the dialog");
|
||||
let after: Vec<vectorcraft_geom::Rect> = app.session.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect();
|
||||
let want: Vec<vectorcraft_geom::Rect> = before.iter().map(|r| vectorcraft_geom::Rect::new(r.x0 - 50.0, r.y0, r.x1 + 50.0, r.y1)).collect();
|
||||
assert_eq!(after, want);
|
||||
}
|
||||
|
||||
/// The strings a frame painted, each with its rectangle.
|
||||
fn painted(out: &egui::FullOutput) -> Vec<(String, egui::Rect)> {
|
||||
fn walk(s: &egui::Shape, v: &mut Vec<(String, egui::Rect)>) {
|
||||
|
||||
@@ -472,7 +472,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"tool.options",
|
||||
"Tool Options…",
|
||||
"",
|
||||
"{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height; OK runs artboard.setProps), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values})",
|
||||
"{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values})",
|
||||
),
|
||||
(
|
||||
"ui.colorGuideOptions",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
use egui::{Sense, Ui, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::Rect;
|
||||
|
||||
use super::{alt_held, pstate, set_pstate};
|
||||
use crate::theme::Tokens;
|
||||
@@ -110,7 +111,7 @@ pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) -> bool {
|
||||
"width" => [None, None, Some(nv), None],
|
||||
_ => [None, None, None, Some(nv)],
|
||||
};
|
||||
super::transform::set_artboard(app, i, r, refi, xywh, link);
|
||||
super::transform::set_artboard(app, i, refi, xywh, link);
|
||||
}
|
||||
}
|
||||
true
|
||||
@@ -153,6 +154,53 @@ pub(crate) fn picked(app: &VectorcraftApp, n: usize) -> Vec<usize> {
|
||||
all
|
||||
}
|
||||
|
||||
/// The artboards a new size for artboard `i` goes to: the selected artboards ([`picked`]) when
|
||||
/// `i` is one of them, else `i` alone.
|
||||
pub(crate) fn resized_with(app: &VectorcraftApp, i: usize) -> Vec<usize> {
|
||||
let all = picked(app, app.session.active().map_or(0, |d| d.doc.artboards.len()));
|
||||
if all.contains(&i) { all } else { vec![i] }
|
||||
}
|
||||
|
||||
/// The `artboard.setProps` params that set `entries` (each an artboard's `index` and its fields)
|
||||
/// with `common` (an object: `scaleArt`, `moveArt`), in one undo step: the one entry's fields
|
||||
/// beside `common`, or an `artboards` list of several beside it; None for no entry.
|
||||
pub(crate) fn set_props_params(common: Value, mut entries: Vec<Value>) -> Option<Value> {
|
||||
let mut p = common;
|
||||
let o = p.as_object_mut()?;
|
||||
match entries.len() {
|
||||
0 => return None,
|
||||
1 => {
|
||||
if let Value::Object(e) = entries.remove(0) {
|
||||
o.extend(e);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
o.insert("artboards".into(), Value::Array(entries));
|
||||
}
|
||||
}
|
||||
Some(p)
|
||||
}
|
||||
|
||||
/// Run `artboard.setProps` with `common` on each artboard of `indices` that `entry` gives fields for
|
||||
/// (from its index and rectangle), in one undo step ([`set_props_params`]). Nothing runs when it
|
||||
/// gives none.
|
||||
pub(crate) fn set_props(
|
||||
app: &mut VectorcraftApp,
|
||||
indices: &[usize],
|
||||
common: Value,
|
||||
entry: impl Fn(usize, Rect) -> Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
let entries: Vec<Value> = app
|
||||
.session
|
||||
.active()
|
||||
.map(|d| indices.iter().filter_map(|&i| d.doc.artboards.get(i).and_then(|a| entry(i, a.rect))).collect())
|
||||
.unwrap_or_default();
|
||||
match set_props_params(common, entries) {
|
||||
Some(p) => app.run("artboard.setProps", p),
|
||||
None => Ok(Value::Null),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the picked artboards and select a remaining one.
|
||||
pub(crate) fn delete_selected(app: &mut VectorcraftApp) {
|
||||
let n = app.session.active().map_or(0, |d| d.doc.artboards.len());
|
||||
|
||||
@@ -202,7 +202,8 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
|
||||
|
||||
/// Edit Artboards (the Artboard tool): the active artboard's name, which can be edited to rename
|
||||
/// it, and the Artboard Options icon; its position and size, its preset and
|
||||
/// orientation (#671) (with Scale Artwork with Artboard on, a new size scales its art, #602), Move
|
||||
/// orientation (#671) (with Scale Artwork with Artboard on, a new size scales its art, #602; a new
|
||||
/// W or H, preset or orientation goes to every selected artboard, in one undo step), Move
|
||||
/// and Scale Artwork with Artboard, New Artboard and Delete Artboard, and Quick Actions: Rearrange All (#681) and
|
||||
/// Exit back to the Selection tool (#530).
|
||||
fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
@@ -241,7 +242,10 @@ fn artboard_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
_ => 0.0,
|
||||
};
|
||||
if let Some(v) = widgets::num_field(ui, ("ab", key, row), Some(v - offset), units, fw) {
|
||||
app.run("artboard.setProps", json!({"index": i, key: v + offset, "scaleArt": scale_art, "moveArt": move_art})).ok();
|
||||
// A new X or Y moves the active artboard; a new W or H goes to every selected one.
|
||||
let indices = if matches!(key, "width" | "height") { super::artboards::resized_with(app, i) } else { vec![i] };
|
||||
let common = json!({"scaleArt": scale_art, "moveArt": move_art});
|
||||
super::artboards::set_props(app, &indices, common, |j, _| Some(json!({"index": j, key: v + offset}))).ok();
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
@@ -278,16 +282,21 @@ fn rearrange_button(app: &mut VectorcraftApp, ui: &mut Ui, artboards: usize, w:
|
||||
}
|
||||
}
|
||||
|
||||
/// A preset `pw` × `ph` turned to an artboard's orientation (`landscape`, or portrait; a square
|
||||
/// preset as it is).
|
||||
fn turned(landscape: bool, pw: f64, ph: f64) -> (f64, f64) {
|
||||
if (pw > ph) == landscape || pw == ph { (pw, ph) } else { (ph, pw) }
|
||||
}
|
||||
|
||||
/// Artboard `i`'s size preset (New Document's saved and built-in presets, by category, either way
|
||||
/// round; Custom when its `w` × `h` points match none) and its orientation, here and in the Control
|
||||
/// bar. A preset sizes it in its current orientation; the other orientation swaps its width and
|
||||
/// height. Its top-left corner stays put either way.
|
||||
/// bar. A preset sizes each selected artboard in its own orientation; an orientation swaps the
|
||||
/// width and height of each selected artboard that is not square and not already in it. Each
|
||||
/// top-left corner stays put.
|
||||
pub(crate) fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize, w: f64, h: f64) {
|
||||
let landscape = w > h;
|
||||
// A preset `pw` × `ph` turned to the artboard's orientation.
|
||||
let turned = |pw: f64, ph: f64| if (pw > ph) == landscape || pw == ph { (pw, ph) } else { (ph, pw) };
|
||||
let same = |s: &newdoc::DocSettings| {
|
||||
let (pw, ph) = turned(s.width, s.height);
|
||||
let (pw, ph) = turned(landscape, s.width, s.height);
|
||||
(pw - w).abs() < 0.01 && (ph - h).abs() < 0.01
|
||||
};
|
||||
// Recent holds documents made, not presets.
|
||||
@@ -297,17 +306,19 @@ pub(crate) fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize,
|
||||
.filter(|(_, l)| !l.is_empty())
|
||||
.collect();
|
||||
let current = cats.iter().flat_map(|(_, l)| l).find(|s| same(s)).map_or(tl!("Custom"), |s| crate::dialogs::preset_name(&s.name)).to_string();
|
||||
let mut size = None;
|
||||
// The preset's width and height as listed, or the orientation clicked.
|
||||
let mut preset = None;
|
||||
let mut orientation = None;
|
||||
ui.horizontal(|ui| {
|
||||
dim_label(ui, tl!("Preset:"));
|
||||
size = widgets::combo(ui, "ab-preset", ¤t, 140.0, false, |ui| {
|
||||
preset = widgets::combo(ui, "ab-preset", ¤t, 140.0, false, |ui| {
|
||||
let mut chosen = None;
|
||||
for (cat, list) in &cats {
|
||||
ui.menu_button(tl!(cat), |ui| {
|
||||
widgets::menu_scroll(ui, |ui| {
|
||||
for s in list {
|
||||
if widgets::menu_item_name(ui, crate::dialogs::preset_name(&s.name), true, same(s)) {
|
||||
chosen = Some(turned(s.width, s.height));
|
||||
chosen = Some((s.width, s.height));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -317,15 +328,26 @@ pub(crate) fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize,
|
||||
});
|
||||
ui.spacing_mut().item_spacing.x = 2.0;
|
||||
for (is_landscape, tip) in [(false, tl!("Portrait")), (true, tl!("Landscape"))] {
|
||||
if widgets::orientation_button(ui, is_landscape, landscape == is_landscape, tip) && landscape != is_landscape {
|
||||
size = Some((h, w));
|
||||
if widgets::orientation_button(ui, is_landscape, landscape == is_landscape, tip) {
|
||||
orientation = Some(is_landscape);
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some((width, height)) = size {
|
||||
let scale_art = super::artboards::scale_art(app);
|
||||
app.run("artboard.setProps", json!({"index": i, "width": width, "height": height, "scaleArt": scale_art})).ok();
|
||||
if preset.is_none() && orientation.is_none() {
|
||||
return;
|
||||
}
|
||||
let scale_art = super::artboards::scale_art(app);
|
||||
let indices = super::artboards::resized_with(app, i);
|
||||
super::artboards::set_props(app, &indices, json!({"scaleArt": scale_art}), |j, r| {
|
||||
let (w, h) = (r.width(), r.height());
|
||||
let (width, height) = match (preset, orientation) {
|
||||
(Some((pw, ph)), _) => turned(w > h, pw, ph),
|
||||
(None, Some(l)) if (w > h) != l && w != h => (h, w),
|
||||
_ => return None,
|
||||
};
|
||||
Some(json!({"index": j, "width": width, "height": height}))
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
@@ -799,6 +821,59 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Three landscape artboards, with Artboards 1 and 3 selected with the Artboard tool (Artboard
|
||||
/// 1 active): 200 × 100, 200 × 100 and 300 × 150.
|
||||
fn two_selected_artboards() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
|
||||
app.run("artboard.setProps", json!({"index": 2, "width": 300, "height": 150})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
super::super::artboards::select(&mut app, 0);
|
||||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||||
app
|
||||
}
|
||||
|
||||
/// Each artboard's (x0, y0, width, height).
|
||||
fn boards(app: &VectorcraftApp) -> Vec<(f64, f64, f64, f64)> {
|
||||
app.session.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.rect.y0, a.rect.width(), a.rect.height())).collect()
|
||||
}
|
||||
|
||||
/// #1482: with Artboards 1 and 3 selected, a W typed into Properties or the Transform panel
|
||||
/// resizes both in one undo step, and Artboard 2 keeps its size. Properties keeps each top-left
|
||||
/// corner; the Transform panel resizes each about its own center, its default reference point.
|
||||
#[test]
|
||||
fn artboard_width_resizes_every_selected_artboard_in_properties_and_transform() {
|
||||
for (draw, centered) in [(show as fn(&mut VectorcraftApp, &mut Ui), false), (super::super::transform::show, true)] {
|
||||
let mut app = two_selected_artboards();
|
||||
let before = boards(&app);
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let ctx = egui::Context::default();
|
||||
panel_frame(&mut app, &ctx, vec![], draw);
|
||||
let texts = panel_frame(&mut app, &ctx, vec![], draw);
|
||||
let label = texts.iter().find(|(t, _)| t == "W:").unwrap().1;
|
||||
let at = egui::pos2(label.right() + 40.0, label.center().y);
|
||||
let press = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
let key = |key, modifiers| Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
|
||||
panel_frame(&mut app, &ctx, vec![Event::PointerMoved(at), press(true)], draw);
|
||||
panel_frame(&mut app, &ctx, vec![press(false)], draw);
|
||||
panel_frame(&mut app, &ctx, vec![key(egui::Key::A, egui::Modifiers::COMMAND)], draw);
|
||||
panel_frame(&mut app, &ctx, vec![Event::Text("250".into())], draw);
|
||||
panel_frame(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::NONE)], draw);
|
||||
panel_frame(&mut app, &ctx, vec![], draw);
|
||||
let mut want = before.clone();
|
||||
for (i, shift) in [(0, 25.0), (2, -25.0)] {
|
||||
want[i].2 = 250.0;
|
||||
if centered {
|
||||
want[i].0 -= shift;
|
||||
}
|
||||
}
|
||||
assert_eq!(boards(&app), want, "centered: {centered}");
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(boards(&app), before);
|
||||
}
|
||||
}
|
||||
|
||||
/// #530: Edit Artboards shows the active artboard, with New Artboard, Delete Artboard and Exit.
|
||||
/// #775: type's Paragraph section has Hyphenate, and its checkbox turns it on.
|
||||
#[test]
|
||||
@@ -1363,6 +1438,106 @@ mod tests {
|
||||
assert!(frame(&mut app, &ctx, vec![]).iter().any(|(t, _)| t == "Custom"));
|
||||
}
|
||||
|
||||
/// The center of Edit Artboards' Portrait (`landscape` false) or Landscape button.
|
||||
fn orientation_button(ctx: &egui::Context, texts: &[(String, Rect)], landscape: bool) -> Pos2 {
|
||||
let row = texts.iter().find(|(t, _)| t == "Preset:").map(|(_, r)| *r).unwrap();
|
||||
ctx.viewport(|vp| {
|
||||
let mut r: Vec<Rect> = vp
|
||||
.prev_pass
|
||||
.widgets
|
||||
.layers()
|
||||
.flat_map(|(_, w)| w.iter())
|
||||
.filter(|w| w.sense.senses_click() && w.rect.y_range().contains(row.center().y) && w.rect.left() > row.right())
|
||||
.map(|w| w.rect)
|
||||
.collect();
|
||||
r.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
r[r.len() - if landscape { 1 } else { 2 }].center()
|
||||
})
|
||||
}
|
||||
|
||||
/// #1482: with Artboards 1 and 3 selected, Portrait swaps the width and height of both in one
|
||||
/// undo step, each keeping its top-left corner; Artboard 2 stays landscape. An artboard already
|
||||
/// in that orientation keeps its size.
|
||||
#[test]
|
||||
fn edit_artboards_orientation_turns_every_selected_artboard() {
|
||||
let mut app = two_selected_artboards();
|
||||
let before = boards(&app);
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let ctx = egui::Context::default();
|
||||
let portrait = |app: &mut VectorcraftApp| {
|
||||
frame(app, &ctx, vec![]);
|
||||
let texts = frame(app, &ctx, vec![]);
|
||||
let at = orientation_button(&ctx, &texts, false);
|
||||
let press = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(app, &ctx, vec![Event::PointerMoved(at), press(true)]);
|
||||
frame(app, &ctx, vec![press(false)]);
|
||||
};
|
||||
portrait(&mut app);
|
||||
let mut want = before.clone();
|
||||
for i in [0, 2] {
|
||||
(want[i].2, want[i].3) = (before[i].3, before[i].2);
|
||||
}
|
||||
assert_eq!(boards(&app), want);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(boards(&app), before);
|
||||
// Artboard 3 portrait already: Portrait turns Artboard 1 alone.
|
||||
app.run("artboard.setProps", json!({"index": 2, "width": 150, "height": 300})).unwrap();
|
||||
let before = boards(&app);
|
||||
portrait(&mut app);
|
||||
let mut want = before.clone();
|
||||
(want[0].2, want[0].3) = (100.0, 200.0);
|
||||
assert_eq!(boards(&app), want);
|
||||
}
|
||||
|
||||
/// #1482: with Artboards 1 and 3 selected, a preset chosen in Edit Artboards sizes both in one
|
||||
/// undo step, each in its own orientation and keeping its top-left corner: Letter (612 × 792 pt)
|
||||
/// stays portrait for Artboard 1 and turns landscape for Artboard 3. Artboard 2 keeps its size.
|
||||
#[test]
|
||||
fn edit_artboards_preset_sizes_every_selected_artboard() {
|
||||
let mut app = two_selected_artboards();
|
||||
app.run("artboard.setProps", json!({"index": 0, "width": 100, "height": 200})).unwrap();
|
||||
let before = boards(&app);
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let ctx = egui::Context::default();
|
||||
// A frame on a wide screen, with the texts of nested shapes too (the open list and its
|
||||
// submenu), and the center of `label` among them (menu items carry a check-mark column).
|
||||
let run = |app: &mut VectorcraftApp, events: Vec<Event>| {
|
||||
fn walk(s: &egui::Shape, v: &mut Vec<(String, Rect)>) {
|
||||
match s {
|
||||
egui::Shape::Text(t) => v.push((t.galley.text().to_string(), Rect::from_min_size(t.pos, t.galley.size()))),
|
||||
egui::Shape::Vec(s) => s.iter().for_each(|s| walk(s, v)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 700.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
let mut v = vec![];
|
||||
out.shapes.iter().for_each(|c| walk(&c.shape, &mut v));
|
||||
v
|
||||
};
|
||||
let at = |texts: &[(String, Rect)], label: &str| {
|
||||
texts.iter().find(|(t, _)| t.trim() == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"))
|
||||
};
|
||||
let press = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
run(&mut app, vec![]);
|
||||
let custom = at(&run(&mut app, vec![]), "Custom");
|
||||
run(&mut app, vec![Event::PointerMoved(custom), press(custom, true)]);
|
||||
run(&mut app, vec![press(custom, false)]);
|
||||
// Hovering Print opens its submenu.
|
||||
let print = at(&run(&mut app, vec![]), "Print");
|
||||
run(&mut app, vec![Event::PointerMoved(print)]);
|
||||
let letter = at(&run(&mut app, vec![]), "Letter");
|
||||
run(&mut app, vec![Event::PointerMoved(letter), press(letter, true)]);
|
||||
run(&mut app, vec![press(letter, false)]);
|
||||
let mut want = before.clone();
|
||||
(want[0].2, want[0].3) = (612.0, 792.0);
|
||||
(want[2].2, want[2].3) = (792.0, 612.0);
|
||||
assert_eq!(boards(&app), want);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
}
|
||||
|
||||
/// #602: Edit Artboards shows Move and Scale Artwork with Artboard; with Scale on, a new size
|
||||
/// (here the other orientation) takes the art on the artboard along.
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Transform panel: reference point, X/Y/W/H with the constrain link, rotate and shear, flips,
|
||||
//! live-shape properties and the Scale Corners / Scale Strokes & Effects options. While the
|
||||
//! Artboard tool is in use it shows and edits the active artboard's X/Y/W/H instead, and with only
|
||||
//! ruler guides selected their X (vertical guides) and Y (horizontal ones).
|
||||
//! Artboard tool is in use it shows and edits the active artboard's X/Y/W/H instead (a new W or H
|
||||
//! resizes every selected artboard), and with only ruler guides selected their X (vertical guides)
|
||||
//! and Y (horizontal ones).
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::json;
|
||||
@@ -62,28 +63,34 @@ pub fn reference_locator(ui: &mut Ui, refi: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set artboard `index` (now at `r`) to a new X, Y, W or H from reference point `refi`
|
||||
/// ([`artboard_rect`]) with `artboard.setProps`, passing the Artboard tool's Move and Scale Artwork
|
||||
/// with Artboard options as `moveArt` and `scaleArt`; an error is shown in the status bar.
|
||||
pub fn set_artboard(app: &mut VectorcraftApp, index: usize, r: Rect, refi: usize, xywh: [Option<f64>; 4], link: bool) {
|
||||
/// Set artboard `index` to a new X, Y, W or H from reference point `refi` ([`artboard_rect`]) with
|
||||
/// `artboard.setProps`, passing the Artboard tool's Move and Scale Artwork with Artboard options as
|
||||
/// `moveArt` and `scaleArt`; an error is shown in the status bar. A new X or Y moves artboard
|
||||
/// `index` alone; a new W or H resizes every selected artboard about its own reference point
|
||||
/// ([`crate::panels::artboards::resized_with`]), in one undo step.
|
||||
pub fn set_artboard(app: &mut VectorcraftApp, index: usize, refi: usize, xywh: [Option<f64>; 4], link: bool) {
|
||||
let [x, y, w, h] = xywh;
|
||||
let n = artboard_rect(r, refi, x, y, w, h, link);
|
||||
let scale_art = crate::panels::artboards::scale_art(app);
|
||||
let move_art = crate::panels::artboards::move_art(app);
|
||||
let p = json!({"index": index, "x": n.x0, "y": n.y0, "width": n.width(), "height": n.height(), "scaleArt": scale_art, "moveArt": move_art});
|
||||
if let Err(e) = app.run("artboard.setProps", p) {
|
||||
let indices = if x.is_none() && y.is_none() { crate::panels::artboards::resized_with(app, index) } else { vec![index] };
|
||||
let common = json!({"scaleArt": scale_art, "moveArt": move_art});
|
||||
let r = crate::panels::artboards::set_props(app, &indices, common, |i, r| {
|
||||
let n = artboard_rect(r, refi, x, y, w, h, link);
|
||||
Some(json!({"index": i, "x": n.x0, "y": n.y0, "width": n.width(), "height": n.height()}))
|
||||
});
|
||||
if let Err(e) = r {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// The Artboard tool's view of the panel: the active artboard's position and size.
|
||||
/// The Artboard tool's view of the panel: the active artboard's position and size ([`set_artboard`]).
|
||||
fn artboard_fields(app: &mut VectorcraftApp, ui: &mut Ui, index: usize, r: Rect) {
|
||||
let units = app.session.general_unit();
|
||||
let origin = app.ruler_origin();
|
||||
let refi = reference_index(ui);
|
||||
let link = proportional(app, ui);
|
||||
let rp = vectorcraft_geom::reference_point(r, refi);
|
||||
let set = |app: &mut VectorcraftApp, x, y, w, h| set_artboard(app, index, r, refi, [x, y, w, h], link);
|
||||
let set = |app: &mut VectorcraftApp, x, y, w, h| set_artboard(app, index, refi, [x, y, w, h], link);
|
||||
ui.horizontal(|ui| {
|
||||
reference_locator(ui, refi);
|
||||
ui.add_space(4.0);
|
||||
|
||||
+20
@@ -2702,6 +2702,26 @@ value: true}`; the Control bar's and Properties' Scale Artwork with Artboard che
|
||||
Artboard) makes its handle drags proportional and passes `scaleArt`, as do Properties' and the Transform panel's
|
||||
artboard size fields while the tool is in use.
|
||||
|
||||
`artboard.setProps {artboards: [{index, name?, x?, y?, width?, height?}…], scaleArt?, moveArt?, …}` sets several
|
||||
artboards in one undo step, each with its own fields; the other params apply to all of them. The art that moves or
|
||||
scales with each artboard is the art inside it before any of them changes, and an object inside several of them goes
|
||||
with the first one listed that moves or scales its art. An empty list and a list that names an artboard twice are
|
||||
refused.
|
||||
|
||||
With several artboards selected with the Artboard tool, the desktop app gives a new size to each of them in one such
|
||||
call. Properties' W and H and the Transform panel's W and H do this, and so do Properties' Preset and orientation
|
||||
buttons, the Control bar's preset, orientation, W and H, and a new Width or Height in Artboard Options. The W and H of
|
||||
the Transform panel and the Control bar resize each artboard about its own reference point, and with the proportions
|
||||
linked they keep each artboard's own proportions. Artboard Options resizes each artboard about the reference point its
|
||||
form used for the new Width or Height. Properties' W and H and the preset and orientation buttons keep each artboard's
|
||||
top-left corner. A preset sizes each artboard in its own orientation, and an orientation
|
||||
swaps the width and height of each artboard that is not square and not already in it. A new X or Y moves the active
|
||||
artboard only, and Artboard Options' Name renames only the artboard it shows.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"artboard.setProps","params":{"artboards":[{"index":0,"width":300},{"index":2,"width":300}],"scaleArt":true}}}
|
||||
```
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"artboard.copy","params":{"index":0}}}
|
||||
{"name":"run_command","arguments":{"command":"edit.paste","params":{}}}
|
||||
|
||||
@@ -431,6 +431,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Names of new and renamed artboards, brushes and patterns:** `artboard.new` and `artboard.setProps` trim the white space around the name they are given, as the Artboards panel's rename does, and a blank name gives the artboard its default name, Artboard N for the Nth artboard. `brush.new`, `brush.duplicate`, `brush.options`, `object.pattern.make`, `object.pattern.saveCopy` and `pattern.options` trim the name as the Brush Options dialog and the Pattern Options panel do, and compare the trimmed name with the names there are; a blank name gives the name each command gives when there is none, and Brush Options and Pattern Options keep the name. Tests in `tests_panelcmds.rs`, `tests_brushsym.rs` and `tests_pattern.rs` cover each command. Public references: [Using multiple artboards](https://web.archive.org/web/20210925094818/https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html), [Brushes](https://web.archive.org/web/20251018101754/https://helpx.adobe.com/illustrator/using/brushes.html) and [Create and edit patterns](https://web.archive.org/web/20211031180358/https://helpx.adobe.com/illustrator/using/create-edit-patterns.html) (archived copies).
|
||||
- **Pattern names and swatch names:** `pattern.options` refuses a new name that another pattern, a swatch or a color group has. It takes a name that only the pattern's own swatches have. The reference page does not say what happens when the new name is taken; the refusal follows VectorCraft's rule that swatches and color groups share one namespace (`Document::swatch_name_taken`), which `swatch.new` and `swatch.edit` also follow. A rename gives the new name only to the pattern's swatches that had the old one; any other swatch with the old name keeps it, and art linked to that swatch stays linked. `object.pattern.make` and `object.pattern.saveCopy` number a color group's name as they number a swatch's. Tests in `tests_pattern.rs` cover each command. Public reference: [Create and edit patterns](https://web.archive.org/web/20211031180358/https://helpx.adobe.com/illustrator/using/create-edit-patterns.html) (archived copy).
|
||||
- **Edit Artboards in Properties:** the active artboard's name is a field. Typing a new name and pressing Enter or clicking elsewhere renames that artboard in one undo step, as the Artboards panel's rename does (`artboard.setProps {index, name}`); pressing Escape or leaving the field empty keeps the old name. A click on the Artboard Options icon beside the field opens Artboard Options for the active artboard, as double-clicking the Artboard tool's button does (`tool.options {tool: "artboard"}`). Pointer and key tests in `panels/properties.rs` cover both. Public references: [Resize artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/resize-artboards.html) and [Create and add new artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/add-new-artboards.html).
|
||||
- **Resizing several artboards:** with several artboards selected with the Artboard tool, a new W or H in Properties, the Transform panel or the Control bar, a preset or an orientation in Properties or the Control bar, and a new Width or Height in Artboard Options resize every selected artboard in one undo step (`artboard.setProps {artboards}`); with Scale Artwork with Artboard on, Properties, the Transform panel and the Control bar scale each object once, with the artboard it was inside before the change. The Transform panel, the Control bar and Artboard Options resize each one about its own reference point, a preset sizes each one in its own orientation, and X and Y still move the active artboard alone. Pointer and key tests in `panels/properties.rs`, dialog tests in `dialogs/tests.rs` and an engine test in `tests_xform.rs` cover them. Public references: [Artboard Enhancements In Illustrator CC](https://blog.adobe.com/en/publish/2017/11/10/artboard-enhancements-in-illustrator-cc) ("modify them all in bulk by defining the height and width values") and [Using multiple artboards](https://helpx.adobe.com/illustrator/using/using-multiple-artboards.html) (2019).
|
||||
- **Artboard Escape routing:** Escape leaves Artboard editing for Selection; during a drag it first cancels the gesture. Headless egui key events cover idle Escape, active-drag cancellation and a subsequent Escape in `tests_distortkeys.rs`.
|
||||
- **Artboard deletion keys:** Delete/Backspace delete selected boards in one Undo step, retaining the original first board in the current order when all are selected, as in the public [Delete artboards guide](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/delete-artboards.html). The last board and active drags stay protected; neither key clears selected artwork. Headless shortcut regressions in `tests_distortkeys.rs` cover reordered boards, artwork, saved state and Undo/Redo.
|
||||
- **Linux RPM packaging:** 64-bit runtime capabilities replace Fedora-specific required package names (#1275); glibc 2.35, libm and libgcc_s are declared alongside dynamically loaded xkbcommon and Vulkan/EGL. Generated x86_64/aarch64 metadata and isolated synthetic-provider resolution checked; real-distro install/start-up QA remains pending.
|
||||
|
||||
Reference in New Issue
Block a user