From 17ba27a0f611f23745179b965924863eada37e4d Mon Sep 17 00:00:00 2001 From: sungjea kim Date: Tue, 20 Oct 2026 08:23:52 +0900 Subject: [PATCH] Export Selection: the selection becomes one asset, not one per object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File › Export Selection… collected the selected objects one asset per object, so a few ungrouped shapes picked to export together came out as separate images unless they were grouped first. It now collects them as one asset of them all (`assets.add {multiple: false}`), the selection exported as one image; `file.exportSelection {multiple: true}` still makes one per object, and Object › Collect for Export › As Multiple Assets and the Asset Export panel's Multiple Assets item are unchanged. --- .../src/dialogs/tests_screen_assets.rs | 19 +++++++++++-------- crates/ui-egui/src/menus.rs | 18 ++++++++++-------- docs/control-protocol.md | 2 +- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/crates/ui-egui/src/dialogs/tests_screen_assets.rs b/crates/ui-egui/src/dialogs/tests_screen_assets.rs index 60d47b39a..c48362ea0 100644 --- a/crates/ui-egui/src/dialogs/tests_screen_assets.rs +++ b/crates/ui-egui/src/dialogs/tests_screen_assets.rs @@ -52,7 +52,7 @@ fn assets(app: &mut VectorcraftApp) -> Vec { 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 r = app.run("file.exportSelection", json!({"multiple": true})).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(); @@ -61,7 +61,7 @@ fn export_selection_collects_the_selection_and_opens_the_assets_tab() { 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(); + app.run("file.exportSelection", json!({"multiple": true})).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())); @@ -80,12 +80,15 @@ fn export_selection_collects_the_selection_and_opens_the_assets_tab() { #[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"]])); + // By default the selection, ungrouped, becomes one asset of it all; `multiple: false` says so too. + for p in [json!({}), json!({"multiple": false})] { + let (mut app, _, ids) = app(true); + app.run("file.exportSelection", p.clone()).unwrap(); + let list = assets(&mut app); + assert_eq!(list.len(), 1, "{p}"); + assert_eq!(list[0]["nodes"], json!(ids), "{p}"); + assert_eq!(field(&app, "assets"), json!([list[0]["id"]]), "{p}"); + } } #[test] diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index ef0138449..20d59709b 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -843,7 +843,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "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", + "{multiple?: false} collect the selected objects as assets (assets.add: one of them all, as one image; true: one per object) and open Export for Screens on its Assets tab with them checked → {assets: [asset id…]}; document.exportSelection exports the selection in one go instead", ), ( "css.copy", @@ -1666,13 +1666,15 @@ 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 }) - }), + // File → Export Selection…: the selection becomes one asset (`multiple: true`: one per + // object), 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(false) })).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 }) + }) + } "css.copy" => crate::panels::css_properties::copy(app, p), "css.exportFile" => crate::panels::css_properties::export_file(app, p), // File → Print…: its dialog; with params, print (or save the job as a PDF) without it. diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 332ac0323..ce996bbfe 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -121,7 +121,7 @@ the two can still win that race. | 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`). While the Artboard tool is in use, the artboards it has selected (its `selected` option, the rows the Artboards panel highlights) are checked instead, and `select` is `range`, or `all` when every artboard is selected. Fields: `tab` (`artboards` \| `assets` \| `layers` \| `selection`; OK reads Export Layer or Export Selection on those), `layers` ([layer id…] checked), `selectionMode` (`one` \| `each`), `name` (the file name pattern of `document.exportForScreens`), `counterStart`, `folders` (`none` \| `rows` \| `format` \| `item` \| `itemFormat`), `exportPreset` (the preset applied last), `__savePreset` (the name typed to save one), `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. When `warnings` is not empty, an export into a folder or a download shows the first in the status bar as Save As shows a save's notes | | 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 | +| `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as one asset (`assets.add`; one per object with `multiple: true`) and opens `exportForScreens` on its Assets tab with just them checked → `{assets}`. `document.exportSelection` still exports the selection in one go, without the dialog | | CSS Properties panel | `window.panel {panel: "cssProperties"}`; `engine.execute` `css.copy` / `css.exportFile` | the panel shows `css.selection` of the selection (after Generate CSS, while nothing is selected, `css.generate`); its menu holds the options the commands take (`units`, `position`, `dimensions`, `unnamed`, `rasterize`). `css.copy {scope?, …options}` copies the CSS to the clipboard → `{css, rules}`; `css.exportFile {path?, scope?, …options}` writes `css.export` to `path` (else a picked file; the web downloads it) with the pictures of rasterized art next to it → `{path, rules, images, warnings}`, and the status bar shows the first of `warnings` as Export As shows an export's notes | | `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 |