//! 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 { s.doc().unwrap().doc.artboards.iter().map(|a| a.rect).collect() } fn presets(s: &mut Session, category: &str) -> Vec { 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 = 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::>(), [(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::>(), [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::>(), [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::>(), [(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 = 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::>(), [(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); }