diff --git a/crates/ui-egui/src/dialogs/tests_screen_assets.rs b/crates/ui-egui/src/dialogs/tests_screen_assets.rs index feedb177..b052d165 100644 --- a/crates/ui-egui/src/dialogs/tests_screen_assets.rs +++ b/crates/ui-egui/src/dialogs/tests_screen_assets.rs @@ -1,5 +1,5 @@ -//! Export for Screens' Assets tab, drawn headlessly: the checked assets, the OK label, desktop -//! folders and web downloads. +//! Export for Screens' Assets tab and File → Export Selection…, drawn headlessly: collecting the +//! selection, the checked assets, the OK label, desktop folders and web downloads. use std::cell::RefCell; use std::rc::Rc; @@ -45,6 +45,46 @@ fn assets(app: &mut VectorcraftApp) -> Vec { app.run("assets.list", json!({})).unwrap()["assets"].as_array().unwrap().clone() } +#[test] +fn export_selection_collects_the_selection_and_opens_the_assets_tab() { + let (mut app, log, _) = app(true); + assert!(crate::menus::enabled(&app, "file.exportSelection")); + let r = app.run("file.exportSelection", json!({})).unwrap(); + let list = assets(&mut app); + assert_eq!(list.len(), 2, "one asset per object"); + let ids: Vec = list.iter().map(|a| a["id"].clone()).collect(); + assert_eq!(r["assets"], json!(ids)); + assert_eq!(kind(&app), Some(export_for_screens::KIND)); + assert_eq!((field(&app, "tab"), field(&app, "assets")), (json!("assets"), json!(ids))); + // Collecting the same art again opens on the same assets. + super::cancel(&mut app); + app.run("file.exportSelection", json!({})).unwrap(); + assert_eq!(assets(&mut app).len(), 2); + assert_eq!(field(&app, "assets"), json!(ids)); + let text = painted_text(&mut app, |app, ui| super::show(app, ui.ctx())); + assert!(text.contains("Asset 1") && text.contains("Asset 2") && text.contains("Select All"), "{text}"); + // The web downloads both as one zip. + let r = confirm(&mut app).unwrap(); + assert_eq!(r["files"], json!(["Asset-1.png", "Asset-2.png"])); + assert_eq!(log.borrow().len(), 1); + assert!(app.ui.dialog.is_none()); + // Nothing selected: Export Selection is off; document.exportSelection still exports in one go. + app.run("select.set", json!({"ids": []})).unwrap(); + assert!(!crate::menus::enabled(&app, "file.exportSelection")); + app.run("select.set", json!({"ids": [list[0]["nodes"][0]]})).unwrap(); + assert!(app.session.execute("document.exportSelection", &json!({"format": "png"})).unwrap()["dataBase64"].is_string()); +} + +#[test] +fn export_selection_as_one_asset() { + let (mut app, _, ids) = app(true); + app.run("file.exportSelection", json!({"multiple": false})).unwrap(); + let list = assets(&mut app); + assert_eq!(list.len(), 1); + assert_eq!(list[0]["nodes"], json!(ids)); + assert_eq!(field(&app, "assets"), json!([list[0]["id"]])); +} + #[test] fn the_assets_tab_exports_the_checked_assets_into_the_folder() { let (mut app, log, _) = app(false); @@ -89,6 +129,7 @@ fn the_menus_reach_asset_export() { let (mut app, _, _) = app(false); let entries = crate::menus::menu_entries(&app); let find = |cmd: &str, label: &str| entries.iter().find(|e| e.command.as_deref() == Some(cmd) && e.label == label).map(|e| e.path.clone()); + assert_eq!(find("file.exportSelection", "Export Selection…"), Some(vec!["File".to_string()])); assert_eq!(find("assets.add", "As Single Asset"), Some(vec!["Object".to_string(), "Collect for Export".to_string()])); assert_eq!(find("assets.add", "As Multiple Assets"), Some(vec!["Object".to_string(), "Collect for Export".to_string()])); let window = entries.iter().find(|e| e.label == "Asset Export").unwrap(); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index fd33d93a..0691d3e0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -485,6 +485,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{…document.exportForWeb settings} write the HTML page and its images to a temporary folder and open the page in the default browser (desktop) → {path}", ), + ( + "file.exportSelection", + "Export Selection…", + "", + "{multiple?: true} collect the selected objects as assets (assets.add: one per object; false: one of them all) and open Export for Screens on its Assets tab with them checked → {assets: [asset id…]}; document.exportSelection exports the selection in one go instead", + ), ]; /// Handle a UI command. `None` = not a UI command (the engine handles it). @@ -871,6 +877,13 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::dialogs::save_for_web::open(app), "file.saveForWeb" => crate::dialogs::save_for_web::save(app, p.clone()), "file.saveForWeb.browser" => crate::dialogs::save_for_web::browser_preview(app, p), + // File → Export Selection…: the selection becomes assets, shown checked on Export for + // Screens' Assets tab. + "file.exportSelection" => app.run("assets.add", json!({ "multiple": p.get("multiple").and_then(Value::as_bool).unwrap_or(true) })).map(|r| { + let ids: Vec = r["assets"].as_array().into_iter().flatten().filter_map(Value::as_u64).collect(); + crate::dialogs::open_export_for_screens_assets(app, Some(&ids)); + json!({ "assets": ids }) + }), _ => return None, }; Some(r) @@ -1106,6 +1119,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool { "ui.packageDialog" | "docInfo.save" => app.session.active().is_some(), "ui.epsOptionsDialog" => app.session.active().is_some(), "file.saveForWeb" | "file.saveForWeb.browser" => app.session.active().is_some(), + "file.exportSelection" => app.session.active().is_some_and(|d| !d.selection.is_empty()), _ => true, } } @@ -1169,7 +1183,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { c("Save for Web (Legacy)…", "file.saveForWeb"), ], ), - c("Export Selection…", "document.exportSelection"), + c("Export Selection…", "file.exportSelection"), Sep, c("Package…", "ui.packageDialog"), sub("Scripts", vec![todos("Other Script…", "Cmd+F12")]), diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 9eab6465..3e0494e4 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -33,6 +33,7 @@ | Background Save / Export | `app.save`, `app.export {path}`, `file.save`, `file.saveAs`, `file.saveCopy` | with the `backgroundSave` / `backgroundExport` preferences on (the default) the desktop app writes the file from a snapshot on a worker thread: the call returns `{path, background: true, status}` at once, `ui.inspect` lists the jobs still running in `background` (labels such as `"Saving a.vectorcraft"`), and the document counts as saved (as of the snapshot) once its file is written. Quit and Close wait for them. With the preferences off (`prefs.set`), saves return once written | | `engine.execute` `file.exportForScreens` | `{}` / `{…document.exportForScreens params}` | without params opens `exportForScreens` on the settings the document last exported with (`document.exportSettings`). Fields: `tab` (`artboards` \| `assets`), `select` (`all` \| `range` \| `full`), `range` ("1-3, 5"), `boards` (a bool per artboard; checking them sets `select` and `range`), `includeBleed`, `folder` (desktop; default the Desktop, else home), `openLocation`, `subfolders`, `prefix`, `preset` (`""` \| `mobile` \| `density`), `formats` (rows `{format, scale: "2x" \| "100w" \| "100h" \| "72ppi", suffix, quality?, folder?}`), `settings` (`{png \| png8 \| jpg \| webp \| gif \| svg \| pdf: {…options}}`, the gear's Format Settings; `__settings` names the format shown). `ui.dialog.confirm` exports (a bad range or no folder keeps it open); on the web OK reads "Download" and the files download (several as one `.zip`). With params it runs `document.exportForScreens`: no `folder` returns the files (or with `zip: true` one store-only zip) as `dataBase64`; with `openLocation` the first file is shown in the file manager | | Export for Screens' Assets tab, Asset Export panel | `ui.dialog.set` `tab: "assets"`, `assets: [asset id…]`; `window.panel {panel: "assetExport"}` | on the Assets tab of `exportForScreens` the `assets` field lists the checked asset ids (all of them when it opens); `ui.dialog.confirm` runs `document.exportForScreens {assets}` (nothing checked keeps it open) and OK reads "Export Asset". The Asset Export panel acts through `assets.*` and shares the format rows with the dialog (`assets.settings.set`) | +| `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as assets (`assets.add`; one per object unless `multiple` is false) and opens `exportForScreens` on its Assets tab with just them checked → `{assets}`. `document.exportSelection` still exports the selection in one go, without the dialog | | `ui.dialog.*` on `importPdf` | `{field: "page" \| "allPages" \| "range" \| "cropTo" \| "password", value}` | opening (`app.open`, `file.open`) a PDF or `.ai` with several pages or a password, or placing one (`file.place` without `page`), opens the Import PDF dialog (`mode`: `open` or `place`; `pageCount`). While `__locked` is true, set `password` and `ui.dialog.confirm` to unlock it (a one-page file then opens or is placed). Then set `allPages: false` and `range` ("2-3, 5") to open some pages, or `page` to place one, and `cropTo` (`bounding`, `art`, `crop`, `trim`, `bleed`, `media`); `ui.dialog.confirm` opens or places them (placing keeps the other `file.place` params, such as `link`, `template` and `at`, in `__place`). `document.open {pages, cropTo, password}` and `file.place {page, crop, password}` do the same without the dialog | diff --git a/docs/mcp.md b/docs/mcp.md index 3b4376be..3b797f00 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1415,7 +1415,8 @@ defaults); presets and remembered settings are preferences. ## Asset Export -Assets are pieces of art collected for export on their own (Window › Asset Export, Object › Collect for Export). +Assets are pieces of art collected for export on their own (Window › Asset Export, Object › Collect for Export, File › +Export Selection…). `assets.add {ids?, multiple?}` collects the selected objects (or `ids`): one asset per object, or with `multiple: false` one asset of them all, named after the object or `Asset 1`, `Asset 2`…; art already collected keeps its asset. Assets name their objects, so they follow edits: moving the art moves the crop, and deleting it