mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 02:07:09 -06:00
Add shared presets and artwork fitting to Artboard Options
Squashed from #3277 onto main.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
//! 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, its position
|
||||
//! from a reference point, and its background color (`color`: a color as the paint commands take
|
||||
//! it, or null for none). A new size also goes to the other selected artboards.
|
||||
//! it, or null for none). Sizing choices also go to the other selected artboards; fitting
|
||||
//! changes only the active artboard around stationary artwork.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Color;
|
||||
@@ -12,6 +13,7 @@ use vectorcraft_geom::{Point, Rect};
|
||||
use super::{DialogSpec, form, run_and_close};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::panels::artboards::{FILLS, fill_colors, fill_name};
|
||||
use crate::panels::properties::ArtboardSizeChoice;
|
||||
use crate::panels::transform::{artboard_rect, reference_index, reference_locator};
|
||||
use crate::state::Dialog;
|
||||
|
||||
@@ -34,8 +36,24 @@ fn placed(d: &Dialog) -> Rect {
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let (unit, origin, refi) = (app.session.general_unit(), app.ruler_origin(), reference_index(ui));
|
||||
let (was, w0, h0) = (placed(d), d.f64("width", 0.0), d.f64("height", 0.0));
|
||||
let bounds = [fit_bounds(app, ui.ctx(), false), fit_bounds(app, ui.ctx(), true)];
|
||||
let (choice, fit) = crate::panels::properties::artboard_size_controls_with_fit(app, ui, w0, h0, bounds.map(|b| b.is_some()));
|
||||
let picked = choice.is_some();
|
||||
if let Some(choice) = choice {
|
||||
if let Some((w, h)) = choice.size_for(w0, h0) {
|
||||
set_rect(d, artboard_rect(was, refi, None, None, Some(w), Some(h), false));
|
||||
}
|
||||
remember_size_choice(d, choice);
|
||||
d.fields.insert("__refx".into(), json!(refi % 3));
|
||||
d.fields.insert("__refy".into(), json!(refi / 3));
|
||||
d.fields.remove("__fit");
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
form::grid_then(ui, d, unit, |ui, d| {
|
||||
let (w, h) = (d.f64("width", 0.0), d.f64("height", 0.0));
|
||||
if !picked && (w != w0 || h != h0) {
|
||||
d.fields.remove("__sizeChoice");
|
||||
}
|
||||
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. The
|
||||
// reference point's column or row is kept for the other selected artboards (`confirm`).
|
||||
@@ -47,6 +65,15 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
position_rows(ui, d, refi, unit, origin);
|
||||
});
|
||||
// Apply a Fit after the ordinary size/reference-point grid: that grid must not replace
|
||||
// the chosen artwork's position with one derived from the previous artboard rectangle.
|
||||
if let Some(selection) = fit
|
||||
&& let Some(bounds) = bounds[usize::from(selection)]
|
||||
{
|
||||
set_rect(d, bounds);
|
||||
d.fields.insert("__fit".into(), json!(true));
|
||||
d.fields.remove("__sizeChoice");
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
let color = d.fields.get("color").and_then(color_value);
|
||||
let fills = fill_colors(app);
|
||||
@@ -68,6 +95,59 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Store document-coordinate draft bounds, retaining an editable minimum for zero-area art.
|
||||
fn set_rect(d: &mut Dialog, r: Rect) {
|
||||
for (key, value) in [("x", r.x0), ("y", r.y0), ("width", r.width().max(1.0)), ("height", r.height().max(1.0))] {
|
||||
d.fields.insert(key.into(), json!(value));
|
||||
}
|
||||
}
|
||||
|
||||
/// Preserve preset/orientation intent separately from typed dimensions for the selected set.
|
||||
fn remember_size_choice(d: &mut Dialog, choice: ArtboardSizeChoice) {
|
||||
let value = match choice {
|
||||
ArtboardSizeChoice::Preset(w, h) => json!({"preset": [w, h]}),
|
||||
ArtboardSizeChoice::Orientation(landscape) => json!({"landscape": landscape}),
|
||||
};
|
||||
d.fields.insert("__sizeChoice".into(), value);
|
||||
}
|
||||
|
||||
/// Validate stored sizing intent before deriving the other selected artboards' dimensions.
|
||||
fn size_choice(d: &Dialog) -> Option<ArtboardSizeChoice> {
|
||||
let value = d.fields.get("__sizeChoice")?;
|
||||
if let Some(landscape) = value.get("landscape").and_then(Value::as_bool) {
|
||||
return Some(ArtboardSizeChoice::Orientation(landscape));
|
||||
}
|
||||
let preset = value.get("preset")?.as_array()?;
|
||||
let (w, h) = (preset.first()?.as_f64()?, preset.get(1)?.as_f64()?);
|
||||
(w.is_finite() && h.is_finite() && w > 0.0 && h > 0.0).then_some(ArtboardSizeChoice::Preset(w, h))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ArtworkFitBounds {
|
||||
key: (u64, u64),
|
||||
bounds: Option<Rect>,
|
||||
}
|
||||
|
||||
/// Artwork fitting includes all drawn artwork, even outside every artboard; selection uses
|
||||
/// the shared selection cache. Draft/target geometry never restricts membership. Global artwork
|
||||
/// bounds are cached only for the active document and its revision.
|
||||
fn fit_bounds(app: &mut VectorcraftApp, ctx: &egui::Context, selection: bool) -> Option<Rect> {
|
||||
if selection {
|
||||
return app.selection_bounds();
|
||||
}
|
||||
let st = app.session.active()?;
|
||||
let key = (st.uid, st.revision);
|
||||
let id = egui::Id::new("artboard-options-artwork-fit-bounds");
|
||||
if let Some(cached) = ctx.data_mut(|data| data.get_temp::<ArtworkFitBounds>(id))
|
||||
&& cached.key == key
|
||||
{
|
||||
return cached.bounds;
|
||||
}
|
||||
let bounds = vectorcraft_render::encode::art_bounds(&st.doc);
|
||||
ctx.data_mut(|data| data.insert_temp(id, ArtworkFitBounds { key, bounds }));
|
||||
bounds
|
||||
}
|
||||
|
||||
/// The X and Y rows: where reference point `refi` lies, counted from the ruler origin `origin`,
|
||||
/// with the locator before X's label. A typed X or Y moves the artboard so that its reference point
|
||||
/// lands there.
|
||||
@@ -119,10 +199,12 @@ 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
|
||||
/// OK sets the artboard's fields. A preset or orientation choice applies separately to each
|
||||
/// selected artboard; a typed Width or Height that differs also goes to the others (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.
|
||||
/// row; the top-left corner when the form set none), all in one undo step. Fitting applies only
|
||||
/// to this artboard and never moves its artwork.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
if d.bool("new") {
|
||||
return confirm_new(app, d);
|
||||
@@ -142,17 +224,25 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
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 choice = size_choice(d);
|
||||
let mut entries = vec![];
|
||||
if w.is_some() || h.is_some() {
|
||||
if !d.bool("__fit") && (choice.is_some() || 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 (w, h) = if let Some(choice) = choice {
|
||||
let Some((w, h)) = choice.size_for(r.width(), r.height()) else { continue };
|
||||
(Some(w), Some(h))
|
||||
} else {
|
||||
(w, h)
|
||||
};
|
||||
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)});
|
||||
// Fitting changes the artboard around existing art; it must never move that art.
|
||||
let common = json!({"moveArt": !d.bool("__fit") && 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)
|
||||
}
|
||||
|
||||
@@ -1792,3 +1792,400 @@ fn polar_grid_shared_cancel_reopen_zero_counts_and_failed_creation() {
|
||||
assert_eq!(app.session.tool_options_of("polarGrid"), options);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
}
|
||||
|
||||
/// Open the real Preset menu before choosing a Fit; these actions are no longer body buttons.
|
||||
fn artboard_fit_preset_at(frames: &mut Frames, app: &mut VectorcraftApp, label: &str) -> egui::Pos2 {
|
||||
let texts = frames.texts(app);
|
||||
assert!(texts.iter().all(|(t, _)| !matches!(t.as_str(), "Fit to Artwork Bounds" | "Fit to Selected Art")), "Fit lives only in Preset");
|
||||
let row = texts.iter().find(|(t, _)| t == "Preset:").unwrap().1;
|
||||
let combo = frames.ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.layers()
|
||||
.flat_map(|(_, widgets)| widgets.iter())
|
||||
.filter(|w| w.sense.senses_click() && w.rect.y_range().contains(row.center().y) && w.rect.left() > row.right())
|
||||
.map(|w| w.rect)
|
||||
.min_by(|a, b| a.left().total_cmp(&b.left()))
|
||||
.unwrap()
|
||||
.center()
|
||||
});
|
||||
frames.press(app, combo, true);
|
||||
frames.press(app, combo, false);
|
||||
frames.texts(app).into_iter().find(|(text, _)| text.trim() == label).unwrap().1.center()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_fit_drafts_cancel_and_apply_without_moving_art() {
|
||||
for selected in [false, true] {
|
||||
let mut app = app();
|
||||
app.run("shape.rectangle", json!({"x":30,"y":40,"width":80,"height":60})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
for apply in [false, true] {
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let mut frames = Frames::new();
|
||||
let label = if selected { "Fit to Selected Art" } else { "Fit to Artwork Bounds" };
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, label);
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "draft only");
|
||||
if !apply {
|
||||
cancel(&mut app);
|
||||
continue;
|
||||
}
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, before.layers, "art stays in place");
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 110.0, 100.0));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_shared_preset_drafts_and_resizes_selected_boards_in_their_orientations() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":200,"height":100,"artboards":3})).unwrap();
|
||||
app.run("artboard.setProps", json!({"index":2,"width":150,"height":300})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
crate::panels::artboards::select(&mut app, 0);
|
||||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
for apply in [false, true] {
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let mut frames = Frames::new();
|
||||
let at = |texts: &[(String, egui::Rect)], label: &str| {
|
||||
texts.iter().find(|(t, _)| t.trim() == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"))
|
||||
};
|
||||
let custom = at(&frames.texts(&mut app), "Custom");
|
||||
frames.press(&mut app, custom, true);
|
||||
frames.press(&mut app, custom, false);
|
||||
let print = at(&frames.texts(&mut app), "Print");
|
||||
frames.run(&mut app, vec![egui::Event::PointerMoved(print)]);
|
||||
let letter = at(&frames.texts(&mut app), "Letter");
|
||||
frames.press(&mut app, letter, true);
|
||||
frames.press(&mut app, letter, false);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "preset is a draft until OK");
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.f64("width", 0.0), d.f64("height", 0.0)), (792.0, 612.0));
|
||||
if !apply {
|
||||
cancel(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
continue;
|
||||
}
|
||||
let steps = app.session.doc().unwrap().history.undo.len();
|
||||
confirm(&mut app).unwrap();
|
||||
let doc = &app.session.doc().unwrap().doc;
|
||||
assert_eq!((doc.artboards[0].rect.width(), doc.artboards[0].rect.height()), (792.0, 612.0));
|
||||
assert_eq!((doc.artboards[2].rect.width(), doc.artboards[2].rect.height()), (612.0, 792.0));
|
||||
assert_eq!(doc.artboards[1], before.artboards[1], "unselected board stays unchanged");
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps + 1);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_orientation_applies_to_other_selected_boards_when_active_is_already_portrait() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":100,"height":200,"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 before = app.session.doc().unwrap().doc.clone();
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let mut frames = Frames::new();
|
||||
let texts = frames.texts(&mut app);
|
||||
let row = texts.iter().find(|(t, _)| t == "Preset:").unwrap().1;
|
||||
let mut buttons: Vec<egui::Rect> = frames.ctx.viewport(|vp| {
|
||||
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()
|
||||
});
|
||||
buttons.sort_by(|a, b| a.left().total_cmp(&b.left()));
|
||||
let portrait = buttons[buttons.len() - 2].center();
|
||||
frames.press(&mut app, portrait, true);
|
||||
frames.press(&mut app, portrait, false);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "orientation remains a draft");
|
||||
confirm(&mut app).unwrap();
|
||||
let doc = &app.session.doc().unwrap().doc;
|
||||
assert_eq!(doc.artboards[0], before.artboards[0], "active board was already portrait");
|
||||
assert_eq!((doc.artboards[2].rect.width(), doc.artboards[2].rect.height()), (150.0, 300.0));
|
||||
assert_eq!(doc.artboards[1], before.artboards[1]);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_fit_changes_only_active_board_even_with_an_artboard_multiselection() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":200,"height":200,"artboards":3})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x":30,"y":40,"width":80,"height":60})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
app.select_tool("artboard");
|
||||
crate::panels::artboards::select(&mut app, 0);
|
||||
app.session.set_tool_option("selected", &json!([0, 2]));
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let mut frames = Frames::new();
|
||||
let fit = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||||
frames.press(&mut app, fit, true);
|
||||
frames.press(&mut app, fit, false);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "fit remains a draft");
|
||||
confirm(&mut app).unwrap();
|
||||
let doc = &app.session.doc().unwrap().doc;
|
||||
assert_eq!(doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 110.0, 100.0));
|
||||
assert_eq!(&doc.artboards[1..], &before.artboards[1..]);
|
||||
assert_eq!(doc.layers, before.layers, "fitting never moves artwork");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_fit_artwork_ignores_draft_membership_and_invalidates_on_edits() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":400,"height":320})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x":30,"y":40,"width":80,"height":60})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
let boards = app.session.doc().unwrap().doc.artboards.clone();
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let dialog = app.ui.dialog.as_mut().unwrap();
|
||||
dialog.fields.insert("width".into(), json!(1));
|
||||
dialog.fields.insert("height".into(), json!(1));
|
||||
let mut frames = Frames::new();
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
(dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)),
|
||||
(30.0, 40.0, 80.0, 60.0),
|
||||
"Fit queries all artwork, not the shrunken draft"
|
||||
);
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||||
// A remote command can edit the document while the modal remains open.
|
||||
app.run("shape.rectangle", json!({"x":600,"y":120,"width":40,"height":30})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
let layers = app.session.doc().unwrap().doc.layers.clone();
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
(dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)),
|
||||
(30.0, 40.0, 610.0, 110.0),
|
||||
"A document revision must invalidate cached fit bounds"
|
||||
);
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 640.0, 150.0));
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 640.0, 150.0));
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_fit_artwork_includes_all_boards_and_is_independent_of_target() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":400,"height":320,"artboards":2})).unwrap();
|
||||
let boards = app.session.doc().unwrap().doc.artboards.clone();
|
||||
for (index, width, height) in [(0, 80.0, 60.0), (1, 40.0, 30.0)] {
|
||||
let rect = boards[index].rect;
|
||||
app.run("shape.rectangle", json!({"x":rect.x0+30.0,"y":rect.y0+40.0,"width":width,"height":height})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
}
|
||||
let layers = app.session.doc().unwrap().doc.layers.clone();
|
||||
let expected =
|
||||
vectorcraft_geom::Rect::new(boards[0].rect.x0 + 30.0, boards[0].rect.y0 + 40.0, boards[0].rect.x0 + 110.0, boards[0].rect.y0 + 100.0).union(
|
||||
vectorcraft_geom::Rect::new(boards[1].rect.x0 + 30.0, boards[1].rect.y0 + 40.0, boards[1].rect.x0 + 70.0, boards[1].rect.y0 + 70.0),
|
||||
);
|
||||
app.select_tool("artboard");
|
||||
let mut frames = Frames::new();
|
||||
for index in [0, 1, 0] {
|
||||
crate::panels::artboards::select(&mut app, index);
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
(dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)),
|
||||
(expected.x0, expected.y0, expected.width(), expected.height())
|
||||
);
|
||||
cancel(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_fit_artwork_new_board_includes_art_outside_its_draft() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":400,"height":320})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x":450,"y":40,"width":80,"height":60})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x":30,"y":170,"width":20,"height":20})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
let boards = app.session.doc().unwrap().doc.artboards.clone();
|
||||
let layers = app.session.doc().unwrap().doc.layers.clone();
|
||||
app.run("ui.newArtboard", json!({})).unwrap();
|
||||
assert!(app.ui.dialog.as_ref().unwrap().bool("new"));
|
||||
let mut frames = Frames::new();
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
let dialog = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((dialog.f64("x", 0.0), dialog.f64("y", 0.0), dialog.f64("width", 0.0), dialog.f64("height", 0.0)), (30.0, 40.0, 500.0, 150.0));
|
||||
confirm(&mut app).unwrap();
|
||||
let after = app.session.doc().unwrap().doc.artboards.clone();
|
||||
assert_eq!(after.len(), 2);
|
||||
assert_eq!(after[0], boards[0]);
|
||||
assert_eq!(after[1].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 530.0, 190.0));
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards, boards);
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc.artboards, after);
|
||||
assert_eq!(app.session.doc().unwrap().doc.layers, layers);
|
||||
}
|
||||
|
||||
/// Fit and background color share one draft and one undo step for existing and new boards.
|
||||
#[test]
|
||||
fn artboard_options_fit_and_background_color_coexist_through_cancel_undo_and_redo() {
|
||||
for new in [false, true] {
|
||||
for apply in [false, true] {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":400,"height":320})).unwrap();
|
||||
app.run("artboard.setProps", json!({"index":0,"color":"#336699"})).unwrap();
|
||||
let x = if new { 450.0 } else { 30.0 };
|
||||
app.run("shape.rectangle", json!({"x":x,"y":40,"width":80,"height":60})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
let steps = app.session.doc().unwrap().history.undo.len();
|
||||
if new {
|
||||
app.run("ui.newArtboard", json!({})).unwrap();
|
||||
} else {
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
}
|
||||
let labels = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui.ctx()));
|
||||
assert!(labels.contains("Background Color:") && labels.contains("Preset:") && !labels.contains("Fit to Artwork Bounds"), "{labels}");
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#c05030"));
|
||||
let mut frames = Frames::new();
|
||||
let fit = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||||
frames.press(&mut app, fit, true);
|
||||
frames.press(&mut app, fit, false);
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["color"], json!("#c05030"));
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "both changes remain a draft");
|
||||
if !apply {
|
||||
cancel(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps);
|
||||
continue;
|
||||
}
|
||||
confirm(&mut app).unwrap();
|
||||
let after = app.session.doc().unwrap().doc.clone();
|
||||
let board = &after.artboards[usize::from(new)];
|
||||
assert_eq!(board.rect, vectorcraft_geom::Rect::new(x, 40.0, x + 80.0, 100.0));
|
||||
assert_eq!(board.color.map(|c| c.to_hex()).as_deref(), Some("#c05030"));
|
||||
assert_eq!(after.layers, before.layers, "fitting never moves the art");
|
||||
if new {
|
||||
assert_eq!(after.artboards[0], before.artboards[0]);
|
||||
}
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps + 1);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moving only the board, with no resize, must not translate its art even when Move Artwork is on.
|
||||
#[test]
|
||||
fn artboard_options_position_only_fit_keeps_art_with_move_art_enabled() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":80,"height":60})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x":10,"y":10,"width":20,"height":20})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x":70,"y":50,"width":20,"height":20})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
app.run("tool.setOption", json!({"tool":"artboard", "values":{"moveArt":true}})).unwrap();
|
||||
assert!(crate::panels::artboards::move_art(&app));
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let mut frames = Frames::new();
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Artwork Bounds");
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
confirm(&mut app).unwrap();
|
||||
let after = app.session.doc().unwrap().doc.clone();
|
||||
assert_eq!(after.artboards[0].rect, vectorcraft_geom::Rect::new(10.0, 10.0, 90.0, 70.0));
|
||||
assert_eq!(after.layers, before.layers, "position-only fitting never moves art");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_fit_preset_disabled_without_art_or_selection() {
|
||||
for has_art in [false, true] {
|
||||
for label in ["Fit to Artwork Bounds", "Fit to Selected Art"] {
|
||||
if has_art && label == "Fit to Artwork Bounds" {
|
||||
continue;
|
||||
}
|
||||
let mut app = app();
|
||||
if has_art {
|
||||
app.run("shape.rectangle", json!({"x":600,"y":40,"width":80,"height":60})).unwrap();
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
}
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let mut frames = Frames::new();
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, label);
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
let draft = app.ui.dialog.as_ref().unwrap();
|
||||
assert_ne!(draft.fields.get("__fit"), Some(&json!(true)), "disabled {label} must not become a fit draft");
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
cancel(&mut app);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_options_selected_fit_spans_other_boards_and_off_board_art() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width":200,"height":200,"artboards":3})).unwrap();
|
||||
for (x, y, w, h) in [(30, 40, 80, 60), (1000, 500, 50, 40)] {
|
||||
app.run("shape.rectangle", json!({"x":x,"y":y,"width":w,"height":h})).unwrap();
|
||||
app.run("paint.setStroke", json!({"none":true})).unwrap();
|
||||
}
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
let before = app.session.doc().unwrap().doc.clone();
|
||||
artboard_options::open(&mut app).unwrap();
|
||||
let mut frames = Frames::new();
|
||||
let at = artboard_fit_preset_at(&mut frames, &mut app, "Fit to Selected Art");
|
||||
frames.press(&mut app, at, true);
|
||||
frames.press(&mut app, at, false);
|
||||
assert_eq!(app.session.doc().unwrap().doc, before, "selection fit is a draft");
|
||||
confirm(&mut app).unwrap();
|
||||
let after = app.session.doc().unwrap().doc.clone();
|
||||
assert_eq!(after.artboards[0].rect, vectorcraft_geom::Rect::new(30.0, 40.0, 1050.0, 540.0));
|
||||
assert_eq!(after.artboards[1..], before.artboards[1..]);
|
||||
assert_eq!(after.layers, before.layers);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, before);
|
||||
app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||||
}
|
||||
|
||||
@@ -323,6 +323,58 @@ fn turned(landscape: bool, pw: f64, ph: f64) -> (f64, f64) {
|
||||
/// 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 Some(choice) = artboard_size_controls(app, ui, w, h) else { 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 (width, height) = choice.size_for(r.width(), r.height())?;
|
||||
Some(json!({"index": j, "width": width, "height": height}))
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
/// A shared picker choice, applied separately to each artboard's orientation.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum ArtboardSizeChoice {
|
||||
Preset(f64, f64),
|
||||
Orientation(bool),
|
||||
}
|
||||
|
||||
impl ArtboardSizeChoice {
|
||||
pub(crate) fn size_for(self, w: f64, h: f64) -> Option<(f64, f64)> {
|
||||
match self {
|
||||
Self::Preset(pw, ph) => Some(turned(w > h, pw, ph)),
|
||||
Self::Orientation(landscape) if (w > h) != landscape && w != h => Some((h, w)),
|
||||
Self::Orientation(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared existing presets/orientation UI. Dialogs keep the chosen size in their draft until OK.
|
||||
pub(crate) fn artboard_size_controls(app: &VectorcraftApp, ui: &mut Ui, w: f64, h: f64) -> Option<ArtboardSizeChoice> {
|
||||
artboard_size_controls_inner(app, ui, w, h, None).0
|
||||
}
|
||||
|
||||
/// The dialog's shared preset/orientation picker with optional artwork and selection fits.
|
||||
/// The second return value is a chosen Fit: false for all artwork, true for selected artwork.
|
||||
pub(crate) fn artboard_size_controls_with_fit(
|
||||
app: &VectorcraftApp,
|
||||
ui: &mut Ui,
|
||||
w: f64,
|
||||
h: f64,
|
||||
enabled: [bool; 2],
|
||||
) -> (Option<ArtboardSizeChoice>, Option<bool>) {
|
||||
artboard_size_controls_inner(app, ui, w, h, Some(enabled))
|
||||
}
|
||||
|
||||
/// Properties and Control keep their ordinary picker; Artboard Options adds two Fit entries.
|
||||
fn artboard_size_controls_inner(
|
||||
app: &VectorcraftApp,
|
||||
ui: &mut Ui,
|
||||
w: f64,
|
||||
h: f64,
|
||||
fits: Option<[bool; 2]>,
|
||||
) -> (Option<ArtboardSizeChoice>, Option<bool>) {
|
||||
let landscape = w > h;
|
||||
let same = |s: &newdoc::DocSettings| {
|
||||
let (pw, ph) = turned(landscape, s.width, s.height);
|
||||
@@ -338,10 +390,19 @@ pub(crate) fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize,
|
||||
// The preset's width and height as listed, or the orientation clicked.
|
||||
let mut preset = None;
|
||||
let mut orientation = None;
|
||||
let mut fit = None;
|
||||
ui.horizontal(|ui| {
|
||||
dim_label(ui, tl!("Preset:"));
|
||||
preset = widgets::combo(ui, "ab-preset", ¤t, 140.0, false, |ui| {
|
||||
let mut chosen = None;
|
||||
if let Some(enabled) = fits {
|
||||
for (selection, label) in [(false, tl!("Fit to Artwork Bounds")), (true, tl!("Fit to Selected Art"))] {
|
||||
if widgets::menu_item_name(ui, label, enabled[usize::from(selection)], false) {
|
||||
fit = Some(selection);
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
for (cat, list) in &cats {
|
||||
ui.menu_button(tl!(cat), |ui| {
|
||||
widgets::menu_scroll(ui, |ui| {
|
||||
@@ -362,21 +423,7 @@ pub(crate) fn artboard_size_row(app: &mut VectorcraftApp, ui: &mut Ui, i: usize,
|
||||
}
|
||||
}
|
||||
});
|
||||
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();
|
||||
(preset.map(|(w, h)| ArtboardSizeChoice::Preset(w, h)).or_else(|| orientation.map(ArtboardSizeChoice::Orientation)), fit)
|
||||
}
|
||||
|
||||
/// Artboard `i`'s Fill, its background `color`: the chip shows it (the None paint when the artboard
|
||||
|
||||
File diff suppressed because one or more lines are too long
+23
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user