mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 02:37:01 -06:00
These commands edited the document even when nothing changed, so the call recorded an undo step, emptied Redo and marked the document modified: - All Artwork Above and Other Layers (Object › Lock and Object › Hide) with nothing to lock or hide: no object above the selection, no other layer, or every one already locked (Lock) or hidden (Hide). - Transform Each with no object to transform, and without Copy when both scales are 100% and there is no move, rotation or reflection. - Rearrange All Artboards when no artboard moves, or with no artboard. - stroke.widthPoint.set and stroke.widthPoint.copy when the profile comes out as it was, and stroke.widthProfile.set with the profile the strokes have already. - Liquify when the stroke reshapes no path, and Puppet Warp given the pins the art holds already (rest: true), or without rest when every pin stays where it is and turns the art by 0° or is free. - Define Perspective Grid with the grid the document has, Attach to Active Plane with no object, Release with Perspective on art that is not in perspective, Move Plane to where the plane is, without copies, and Move Plane to Match Object when the plane is there and active already. - Move, Nudge and Transform in Perspective without Copy when no object moves (1e-9 pt or less along the plane or its normal, Snap to Grid included, or the identity), and Transform Again after one. These commands now return without editing the document. The replies have the same form as before. Transform Each with Copy and no object to transform no longer empties the selection. Puppet Warp without rest, with every pin where it is and turning the art by 0° or free, no longer turns a live shape into a plain path. All Artwork Above still deselects selected objects that are hidden or locked. Width Point counts a change of 1e-9 or less in a point's place or width factor (its width over half the stroke weight) as none. OK in Width Point Edit with the widths it shows, with or without Adjust Adjoining Width Points, records no undo step. After a move in perspective that moved nothing, Transform Again repeats the identity. One line in the evidence list of docs/target-app-parity.md names the commands.
315 lines
16 KiB
Rust
315 lines
16 KiB
Rust
//! File → New (`file.new`, `file.newPresets*`): artboard layouts, orientation, bleed, colour mode,
|
||
//! raster effects resolution, background, presets, Recent and Saved.
|
||
|
||
use serde_json::{Value, json};
|
||
use vectorcraft_doc::{Background, ColorMode, Unit};
|
||
use vectorcraft_geom::Rect;
|
||
|
||
use super::*;
|
||
|
||
fn new_doc(p: Value) -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &p).unwrap();
|
||
s
|
||
}
|
||
|
||
fn boards(s: &Session) -> Vec<Rect> {
|
||
s.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect()
|
||
}
|
||
|
||
fn presets(s: &mut Session, category: &str) -> Vec<Value> {
|
||
let r = s.execute("file.newPresets", &json!({ "category": category })).unwrap();
|
||
r["categories"][0]["presets"].as_array().unwrap().clone()
|
||
}
|
||
|
||
#[test]
|
||
fn four_artboards_in_two_columns_make_a_two_by_two_grid() {
|
||
let s = new_doc(json!({"width": 100, "height": 50, "artboards": 4, "artboardLayout": {"columns": 2, "spacing": 10}}));
|
||
let origins: Vec<(f64, f64)> = boards(&s).iter().map(|r| (r.x0, r.y0)).collect();
|
||
assert_eq!(origins, [(0.0, 0.0), (110.0, 0.0), (0.0, 60.0), (110.0, 60.0)]);
|
||
assert!(boards(&s).iter().all(|r| r.width() == 100.0 && r.height() == 50.0));
|
||
let names: Vec<String> = s.doc().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect();
|
||
assert_eq!(names, ["Artboard 1", "Artboard 2", "Artboard 3", "Artboard 4"]);
|
||
// Grid by Column fills down first; Right-to-Left starts at the right.
|
||
let s = new_doc(json!({"width": 100, "height": 50, "artboards": 4, "artboardLayout": {"layout": "gridByColumn", "columns": 2, "spacing": 10}}));
|
||
assert_eq!(boards(&s).iter().map(|r| (r.x0, r.y0)).collect::<Vec<_>>(), [(0.0, 0.0), (0.0, 60.0), (110.0, 0.0), (110.0, 60.0)]);
|
||
let s = new_doc(json!({"width": 100, "height": 50, "artboards": 3, "artboardLayout": {"layout": "row", "rightToLeft": true}}));
|
||
assert_eq!(boards(&s).iter().map(|r| r.x0).collect::<Vec<_>>(), [240.0, 120.0, 0.0]);
|
||
let s = new_doc(json!({"width": 100, "height": 50, "artboards": 3, "artboardLayout": "column"}));
|
||
assert_eq!(boards(&s).iter().map(|r| r.y0).collect::<Vec<_>>(), [0.0, 70.0, 140.0]);
|
||
// Without a layout: one row, 20 pt apart (as before).
|
||
let s = new_doc(json!({"width": 100, "height": 50, "artboards": 2}));
|
||
assert_eq!(boards(&s)[1].x0, 120.0);
|
||
}
|
||
|
||
#[test]
|
||
fn orientation_swaps_width_and_height() {
|
||
let s = new_doc(json!({"width": 612, "height": 792, "orientation": "landscape"}));
|
||
assert_eq!((boards(&s)[0].width(), boards(&s)[0].height()), (792.0, 612.0));
|
||
let s = new_doc(json!({"width": 792, "height": 612, "orientation": "Portrait"}));
|
||
assert_eq!((boards(&s)[0].width(), boards(&s)[0].height()), (612.0, 792.0));
|
||
let s = new_doc(json!({"preset": "HDTV 1080p", "orientation": "portrait"}));
|
||
assert_eq!((boards(&s)[0].width(), boards(&s)[0].height()), (1080.0, 1920.0));
|
||
}
|
||
|
||
#[test]
|
||
fn every_setting_reaches_the_document() {
|
||
let mut s = Session::new();
|
||
let r = s
|
||
.execute(
|
||
"file.new",
|
||
&json!({"name": "Poster", "width": "210 mm", "height": "297mm", "units": "Millimeters", "bleed": [9, 9, 4.5, 4.5],
|
||
"backgroundContents": "white", "colorMode": "cmyk", "rasterEffectsPpi": 150}),
|
||
)
|
||
.unwrap();
|
||
assert_eq!(r["previewMode"], "default");
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!(d.title, "Poster");
|
||
assert_eq!(d.units, Unit::Millimeters);
|
||
assert!((d.artboards[0].rect.width() - 595.2756).abs() < 1e-3);
|
||
assert_eq!(d.setup.bleed, [9.0, 9.0, 4.5, 4.5]);
|
||
assert_eq!(d.setup.background, Background::White);
|
||
assert_eq!(d.color_mode, ColorMode::Cmyk);
|
||
assert_eq!(d.raster_effects_ppi, 150.0);
|
||
// Saved and reopened, the settings stay.
|
||
let back = vectorcraft_format::load(&vectorcraft_format::save_file(d)).unwrap();
|
||
assert_eq!((back.raster_effects_ppi, back.setup.bleed, back.setup.background), (150.0, d.setup.bleed, Background::White));
|
||
}
|
||
|
||
#[test]
|
||
fn a_white_background_exports_opaque() {
|
||
let alpha = |bg: &str| {
|
||
let mut s = new_doc(json!({"width": 20, "height": 20, "backgroundContents": bg}));
|
||
let r = s.execute("document.export", &json!({"format": "png"})).unwrap();
|
||
let png = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
|
||
image::load_from_memory(&png).unwrap().to_rgba8().get_pixel(10, 10).0
|
||
};
|
||
assert_eq!(alpha("white"), [255, 255, 255, 255]);
|
||
assert_eq!(alpha("transparent")[3], 0);
|
||
}
|
||
|
||
#[test]
|
||
fn a_preset_starts_the_document_and_params_change_it() {
|
||
let s = new_doc(json!({"preset": "a4"}));
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.units, d.color_mode, d.raster_effects_ppi), (Unit::Points, ColorMode::Cmyk, 300.0), "print sizes in prefs unitsGeneral");
|
||
assert!((d.artboards[0].rect.height() - 841.89).abs() < 0.01);
|
||
let s = new_doc(json!({"preset": "Web 1920×1080", "colorMode": "cmyk", "artboards": 2}));
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.units, d.color_mode, d.artboards.len()), (Unit::Pixels, ColorMode::Cmyk, 2));
|
||
assert!(Session::new().execute("file.new", &json!({"preset": "Napkin"})).is_err());
|
||
// Print presets and plain sizes start in Preferences ▸ Units ▸ General; screen presets in pixels.
|
||
let mut s = Session::new();
|
||
s.execute("prefs.set", &json!({"key": "unitsGeneral", "value": "millimeters"})).unwrap();
|
||
s.execute("file.new", &json!({"preset": "A4"})).unwrap();
|
||
assert_eq!(s.doc().unwrap().doc.units, Unit::Millimeters);
|
||
assert_eq!(presets(&mut s, "Print")[4]["size"], "210 × 297 mm");
|
||
s.execute("file.new", &json!({"width": 100})).unwrap();
|
||
assert_eq!(s.doc().unwrap().doc.units, Unit::Millimeters);
|
||
s.execute("file.new", &json!({"preset": "HDTV 720p"})).unwrap();
|
||
assert_eq!(s.doc().unwrap().doc.units, Unit::Pixels);
|
||
}
|
||
|
||
#[test]
|
||
fn ppi_overprint_preview_and_bad_params() {
|
||
// Overprint Preview is process-wide.
|
||
let _view = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
|
||
let mut s = new_doc(json!({"rasterEffectsPpi": 300, "previewMode": "overprint"}));
|
||
assert_eq!(s.doc().unwrap().doc.raster_effects_ppi, 300.0);
|
||
let overprint = |s: &mut Session| s.execute("view.proofSetup", &json!({})).unwrap()["overprintPreview"].clone();
|
||
assert_eq!(overprint(&mut s), true);
|
||
s.execute("view.overprintPreview", &json!({"on": false})).unwrap();
|
||
let r = s.execute("file.new", &json!({"previewMode": "pixel"})).unwrap();
|
||
assert_eq!(r["previewMode"], "pixel", "for the app's Pixel Preview");
|
||
assert_eq!(overprint(&mut s), false);
|
||
for p in [
|
||
json!({"orientation": "sideways"}),
|
||
json!({"width": -5}),
|
||
json!({"width": "wide"}),
|
||
json!({"bleed": 100}),
|
||
json!({"backgroundContents": "plaid"}),
|
||
json!({"colorMode": "lab"}),
|
||
json!({"rasterEffectsPpi": 0}),
|
||
json!({"previewMode": "xray"}),
|
||
json!({"artboardLayout": {"layout": "spiral"}}),
|
||
json!({"artboardLayout": {"columns": 0}}),
|
||
json!({"artboardLayout": {"rows": 2}}),
|
||
json!({"units": "furlongs"}),
|
||
json!({"width": 160000, "artboards": 1000}),
|
||
] {
|
||
let mut s = Session::new();
|
||
assert!(s.execute("file.new", &p).is_err(), "{p}");
|
||
assert!(s.active().is_none(), "{p}: no document on an error");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn categories_list_generated_presets_with_neutral_names() {
|
||
let mut s = Session::new();
|
||
let r = s.execute("file.newPresets", &json!({})).unwrap();
|
||
let names: Vec<&str> = r["categories"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect();
|
||
assert_eq!(names, ["Recent", "Saved", "Mobile", "Web", "Print", "Film & Video", "Art & Illustration", "Branding", "Social"]);
|
||
let print = presets(&mut s, "print");
|
||
let letter = print.iter().find(|p| p["name"] == "Letter").unwrap();
|
||
assert_eq!(
|
||
(letter["size"].as_str(), letter["colorMode"].as_str(), letter["orientation"].as_str()),
|
||
(Some("612 × 792 pt"), Some("cmyk"), Some("portrait"))
|
||
);
|
||
assert_eq!(presets(&mut s, "Film & Video").iter().find(|p| p["name"] == "4K UHD").unwrap()["size"], "3840 × 2160 px");
|
||
for c in ["Mobile", "Web", "Print", "Film & Video", "Art & Illustration", "Branding", "Social"] {
|
||
assert!(!presets(&mut s, c).is_empty(), "{c}");
|
||
}
|
||
// A listed preset can be passed straight back to file.new.
|
||
let story = presets(&mut s, "Social").into_iter().find(|p| p["name"] == "Social Story 1080×1920").unwrap();
|
||
s.execute("file.new", &story).unwrap();
|
||
assert_eq!(boards(&s)[0], Rect::new(0.0, 0.0, 1080.0, 1920.0));
|
||
assert!(s.execute("file.newPresets", &json!({"category": "Furniture"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn recent_remembers_the_last_sizes() {
|
||
let mut s = Session::new();
|
||
assert!(presets(&mut s, "Recent").is_empty());
|
||
s.execute("file.new", &json!({"preset": "A4"})).unwrap();
|
||
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
|
||
s.execute("file.new", &json!({"preset": "A4", "orientation": "landscape"})).unwrap();
|
||
let recent = presets(&mut s, "Recent");
|
||
let names: Vec<&str> = recent.iter().map(|p| p["name"].as_str().unwrap()).collect();
|
||
assert_eq!(names, ["A4", "Custom", "A4"], "newest first; a turned preset keeps its name");
|
||
// The same size again moves to the front instead of repeating.
|
||
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
|
||
assert_eq!(presets(&mut s, "Recent").len(), 3);
|
||
assert_eq!(presets(&mut s, "Recent")[0]["size"], "300 × 200 pt");
|
||
for i in 0..20 {
|
||
s.execute("file.new", &json!({"width": 100 + i, "height": 100})).unwrap();
|
||
}
|
||
assert_eq!(presets(&mut s, "Recent").len(), 12);
|
||
}
|
||
|
||
#[test]
|
||
fn saved_presets_live_in_the_preferences() {
|
||
let mut s = Session::new();
|
||
assert!(s.execute("file.newPresets.delete", &json!({"name": "Mine"})).is_err());
|
||
let r = s
|
||
.execute(
|
||
"file.newPresets.save",
|
||
&json!({"name": "Mine", "width": 400, "height": 300, "units": "Pixels", "artboards": 4, "artboardLayout": {"columns": 2}, "bleed": 9}),
|
||
)
|
||
.unwrap();
|
||
assert_eq!(r, json!({"name": "Mine", "count": 1}));
|
||
let saved = presets(&mut s, "Saved");
|
||
assert_eq!(
|
||
(saved[0]["name"].as_str(), saved[0]["size"].as_str(), saved[0]["artboardLayout"]["columns"].as_u64()),
|
||
(Some("Mine"), Some("400 × 300 px"), Some(2))
|
||
);
|
||
// The same name replaces it.
|
||
s.execute("file.newPresets.save", &json!({"name": "mine", "width": 500, "height": 300})).unwrap();
|
||
assert_eq!(presets(&mut s, "Saved").len(), 1);
|
||
s.execute("file.new", &json!({"preset": "Mine"})).unwrap();
|
||
assert_eq!(boards(&s)[0].width(), 500.0);
|
||
// Kept with the preferences, and by a reset.
|
||
let prefs: crate::Prefs = serde_json::from_value(s.prefs.to_json()).unwrap();
|
||
assert_eq!(prefs.new_doc_presets, s.prefs.new_doc_presets);
|
||
s.execute("prefs.reset", &json!({})).unwrap();
|
||
assert_eq!(s.prefs.new_doc_presets.len(), 1);
|
||
assert_eq!(s.execute("file.newPresets.delete", &json!({"name": "MINE"})).unwrap(), json!({"deleted": "mine"}));
|
||
assert!(presets(&mut s, "Saved").is_empty());
|
||
assert!(s.execute("file.newPresets.save", &json!({"name": " "})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn rearrange_keeps_its_grid() {
|
||
let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 3}));
|
||
s.execute("artboard.rearrange", &json!({"columns": 2, "spacing": 5})).unwrap();
|
||
assert_eq!(boards(&s).iter().map(|r| (r.x0, r.y0)).collect::<Vec<_>>(), [(0.0, 0.0), (105.0, 0.0), (0.0, 55.0)]);
|
||
}
|
||
|
||
/// #681: every layout and order of Rearrange All Artboards, five 100 × 50 artboards 10 pt apart
|
||
/// with Columns (the rows of a grid by column) 2: each artboard lands in its (row, column) cell, the
|
||
/// art on it rides along, and it all undoes in one step.
|
||
#[test]
|
||
fn rearrange_lays_out_by_layout_and_order() {
|
||
type Cells = [(u32, u32); 5];
|
||
let cells: [(&str, &str, Cells); 8] = [
|
||
("gridByRow", "leftToRight", [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0)]),
|
||
("gridByRow", "rightToLeft", [(0, 1), (0, 0), (1, 1), (1, 0), (2, 1)]),
|
||
("gridByColumn", "leftToRight", [(0, 0), (1, 0), (0, 1), (1, 1), (0, 2)]),
|
||
("gridByColumn", "rightToLeft", [(0, 2), (1, 2), (0, 1), (1, 1), (0, 0)]),
|
||
("row", "leftToRight", [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)]),
|
||
("row", "rightToLeft", [(0, 4), (0, 3), (0, 2), (0, 1), (0, 0)]),
|
||
("column", "leftToRight", [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]),
|
||
("column", "rightToLeft", [(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]),
|
||
];
|
||
for (layout, order, want) in cells {
|
||
// One row, 20 pt apart; a 10 pt square 10 pt into each artboard.
|
||
let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 5}));
|
||
let art: Vec<u64> = boards(&s)
|
||
.iter()
|
||
.map(|r| {
|
||
s.execute("shape.rectangle", &json!({"x": r.x0 + 10.0, "y": r.y0 + 10.0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap()
|
||
})
|
||
.collect();
|
||
let before = boards(&s);
|
||
let undo = s.doc().unwrap().history.undo.len();
|
||
let p = json!({"layout": layout, "order": order, "columns": 2, "spacing": 10});
|
||
let r = s.execute("artboard.rearrange", &p).unwrap();
|
||
let (rows, cols) = want.iter().fold((0, 0), |(r, c), (wr, wc)| (r.max(wr + 1), c.max(wc + 1)));
|
||
assert_eq!(r, json!({"artboards": 5, "rows": rows, "columns": cols}), "{layout} {order}");
|
||
let got: Vec<(f64, f64)> = boards(&s).iter().map(|r| (r.x0, r.y0)).collect();
|
||
let cells: Vec<(f64, f64)> = want.iter().map(|&(r, c)| (f64::from(c) * 110.0, f64::from(r) * 60.0)).collect();
|
||
assert_eq!(got, cells, "{layout} {order}");
|
||
assert!(boards(&s).iter().all(|r| r.width() == 100.0 && r.height() == 50.0));
|
||
for (id, (x, y)) in art.iter().zip(&got) {
|
||
let b = s.doc().unwrap().doc.node(NodeId(*id)).unwrap().geometric_bounds().unwrap();
|
||
assert_eq!((b.x0, b.y0), (x + 10.0, y + 10.0), "{layout} {order}: the art moves with its artboard");
|
||
}
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
assert_eq!(boards(&s), before);
|
||
}
|
||
}
|
||
|
||
/// `byColumn: true` is Grid by Column; without `moveArtwork` the art stays; bad values are errors.
|
||
#[test]
|
||
fn rearrange_aliases_and_rejects_bad_values() {
|
||
let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 3}));
|
||
let id = s.execute("shape.rectangle", &json!({"x": 130, "y": 10, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||
s.execute("artboard.rearrange", &json!({"byColumn": true, "columns": 2, "spacing": 0, "moveArtwork": false})).unwrap();
|
||
assert_eq!(boards(&s).iter().map(|r| (r.x0, r.y0)).collect::<Vec<_>>(), [(0.0, 0.0), (0.0, 50.0), (100.0, 0.0)]);
|
||
assert_eq!(s.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap().x0, 130.0, "the art stays");
|
||
let before = boards(&s);
|
||
for p in [json!({"layout": "diagonal"}), json!({"order": "upward"}), json!({"layout": 3}), json!({"order": true})] {
|
||
assert!(s.execute("artboard.rearrange", &p).is_err(), "{p}");
|
||
}
|
||
assert_eq!(boards(&s), before, "nothing moved");
|
||
}
|
||
|
||
/// Rearrange All Artboards with the artboards already where it puts them, or with no artboard,
|
||
/// records no undo step: the step to redo stays, the document stays unmodified, and the reply is the
|
||
/// same.
|
||
#[test]
|
||
fn rearrange_that_moves_no_artboard_records_no_undo_step() {
|
||
let mut s = new_doc(json!({"width": 100, "height": 50, "artboards": 3}));
|
||
let p = json!({"columns": 2, "spacing": 5});
|
||
let reply = s.execute("artboard.rearrange", &p).unwrap();
|
||
let placed = boards(&s);
|
||
// A rectangle drawn and undone leaves a step to redo, and the document is marked saved.
|
||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 10, "height": 10})).unwrap();
|
||
s.execute("edit.undo", &json!({})).unwrap();
|
||
s.doc_mut().unwrap().mark_saved();
|
||
let history = |s: &Session| {
|
||
let st = s.doc().unwrap();
|
||
(st.history.undo.len(), st.history.redo.len(), st.is_dirty())
|
||
};
|
||
let before = history(&s);
|
||
assert_eq!((before.1, before.2), (1, false));
|
||
assert_eq!(s.execute("artboard.rearrange", &p).unwrap(), reply);
|
||
assert_eq!((history(&s), boards(&s)), (before, placed));
|
||
// A file can hold no artboard.
|
||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).artboards.clear();
|
||
s.doc_mut().unwrap().mark_saved();
|
||
let before = history(&s);
|
||
assert_eq!(s.execute("artboard.rearrange", &p).unwrap(), json!({"artboards": 0, "rows": 1, "columns": 1}));
|
||
assert_eq!(history(&s), before);
|
||
}
|