From 1f6ef9dcf3a66f19b759338b109851b5ef1ce15e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 14 Oct 2026 22:14:16 -0700 Subject: [PATCH] Export for Screens: the dialog opens with the Artboard tool's selected artboards checked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Export for Screens opened on the artboard choice the document last exported with (all artboards by default), whichever artboards were selected with the Artboard tool or in the Artboards panel. While the Artboard tool is in use, File › Export › Export for Screens… now opens with the artboards the tool has selected checked, and Export Artboard writes only those. They are the artboards the Artboards panel highlights, and they are checked even after a Full Document export. With another tool in use, the dialog still opens on the saved artboard choice. The other fields show the saved settings in both cases, and the params text of file.exportForScreens describes the rule. Docs: docs/control-protocol.md, docs/target-app-parity.md. --- .../ui-egui/src/dialogs/export_for_screens.rs | 15 ++-- crates/ui-egui/src/dialogs/tests_screens.rs | 80 ++++++++++++++++++- crates/ui-egui/src/menus.rs | 2 +- docs/control-protocol.md | 2 +- docs/target-app-parity.md | 1 + 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index 5665da8c6..8aa20552d 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -3,7 +3,8 @@ //! checkboxes), the destination (folder picker, Open Location after Export, Create Sub-folders; //! the web downloads instead), the format rows (scale as `2x`, `100w`, `100h` or `72ppi`, suffix, //! format), the presets, Format Settings (the gear) and the prefix. It opens on the settings the -//! document last exported with; the format rows are the Asset Export panel's too ([`formats`]). +//! document last exported with, except that while the Artboard tool is in use the artboards it has +//! selected are the ones chosen; the format rows are the Asset Export panel's too ([`formats`]). //! //! Fields (`ui.dialog.set`): `tab` (artboards|assets), `select` (all|range|full), `range`, //! `boards` ([bool] per artboard), `assets` ([asset id] checked on the Assets tab), `includeBleed`, @@ -86,7 +87,8 @@ pub(crate) fn open_assets(app: &mut VectorcraftApp, checked: Option<&[u64]>) { } /// Open the dialog on the settings the active document last exported with (else one PNG row at -/// 1x, into the Desktop), every asset checked. +/// 1x, into the Desktop), every asset checked. While the Artboard tool is in use, the artboards it +/// has selected (the rows the Artboards panel highlights) are the ones chosen instead. pub fn open(app: &mut VectorcraftApp) { let (n, saved, assets) = app .session @@ -95,9 +97,12 @@ pub fn open(app: &mut VectorcraftApp) { .unwrap_or_default(); let get = |k: &str| saved.get(k).filter(|v| !v.is_null()); let flag = |k: &str, default: bool| get(k).and_then(Value::as_bool).unwrap_or(default); - // The artboards: Full Document, a range (or a list of them), else all. - let chosen = ArtboardPick::deserialize(&Value::Object(saved.clone())).ok().and_then(|p| p.resolve(n).ok().flatten()); - let select = match (flag("fullDocument", false), &chosen) { + // The artboards are those the Artboard tool has selected while it is in use, else Full + // Document, a range (or a list of them), else all. + let picked = (app.session.tool_id() == "artboard").then(|| crate::panels::artboards::picked(app, n)); + let full = picked.is_none() && flag("fullDocument", false); + let chosen = picked.or_else(|| ArtboardPick::deserialize(&Value::Object(saved.clone())).ok().and_then(|p| p.resolve(n).ok().flatten())); + let select = match (full, &chosen) { (true, _) => "full", (false, Some(c)) if c.len() < n => "range", _ => "all", diff --git a/crates/ui-egui/src/dialogs/tests_screens.rs b/crates/ui-egui/src/dialogs/tests_screens.rs index 4341415cb..5c86c228d 100644 --- a/crates/ui-egui/src/dialogs/tests_screens.rs +++ b/crates/ui-egui/src/dialogs/tests_screens.rs @@ -1,5 +1,6 @@ //! Export for Screens, drawn headlessly: the fields it opens on (defaults, then the document's last -//! settings), the artboard choice, Format Settings, desktop folders and web downloads. +//! settings, and the artboards the Artboard tool has selected), the artboard choice, Format +//! Settings, desktop folders and web downloads. use std::cell::RefCell; use std::rc::Rc; @@ -7,6 +8,7 @@ use std::rc::Rc; use serde_json::{Value, json}; use vectorcraft_engine::Session; use vectorcraft_engine::cmd::fileio; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind}; use super::tests_export::{frame, kind, set}; use super::*; @@ -99,6 +101,82 @@ fn checked_artboards_read_as_a_range() { } } +#[test] +fn opens_with_the_artboards_the_artboard_tool_selected_and_exports_only_them() { + let (mut app, log) = app(4, true); + app.run("tool.select", json!({"tool": "artboard"})).unwrap(); + let boards: Vec<_> = app.session.active().unwrap().doc.artboards.iter().map(|a| a.rect).collect(); + let view = app.view_info(); + let click = |app: &mut VectorcraftApp, i: usize, shift: bool| { + let mods = Mods { shift, ..Default::default() }; + for kind in [PointerKind::Down, PointerKind::Up] { + crate::canvas::dispatch(app, &PointerEvent { kind, pos: boards[i].center(), mods, pressure: 1.0 }, view); + } + }; + // A click on artboard 2 and a Shift-click on artboard 3 select those two. The dialog opens with + // them checked, and Download writes only them. + click(&mut app, 1, false); + click(&mut app, 2, true); + open(&mut app); + assert_eq!( + (field(&app, "select"), field(&app, "range"), field(&app, "boards")), + (json!("range"), json!("2-3"), json!([false, true, true, false])) + ); + frame(&mut app); + assert_eq!(field(&app, "boards"), json!([false, true, true, false]), "drawing the dialog keeps them"); + let r = confirm(&mut app).unwrap(); + assert_eq!(r["files"], json!(["Artboard-2.png", "Artboard-3.png"])); + assert_eq!(log.borrow().len(), 1, "one zip"); + // With every artboard selected it opens on All. + click(&mut app, 0, true); + click(&mut app, 3, true); + open(&mut app); + assert_eq!((field(&app, "select"), field(&app, "range"), field(&app, "boards")), (json!("all"), json!("1-4"), json!([true, true, true, true]))); +} + +#[test] +fn opens_with_the_rows_selected_in_the_artboards_panel_else_on_the_saved_choice() { + let (mut app, _) = app(4, true); + // An export of the full document with a prefix and a JPEG row is the saved choice. + app.run("file.exportForScreens", json!({"fullDocument": true, "prefix": "proof-", "formats": [{"format": "jpg", "quality": 80, "scale": "2x"}]})) + .unwrap(); + app.run("tool.select", json!({"tool": "artboard"})).unwrap(); + let ctx = egui::Context::default(); + let screen = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(240.0, 400.0))); + let panel = |app: &mut VectorcraftApp, time: f64, events: Vec| { + let raw = egui::RawInput { events, time: Some(time), screen_rect: screen, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| crate::panels::artboards::show(app, ui)); + out.textures_delta.clear(); + out + }; + // With the Artboard tool in use, a click on Artboard 2's row and a Shift-click on Artboard 3's + // select those two. The dialog opens with them checked instead of the saved Full Document. + let rows = panel(&mut app, 0.0, vec![]); + for (time, label, modifiers) in [(1.0, "Artboard 2", egui::Modifiers::NONE), (2.0, "Artboard 3", egui::Modifiers::SHIFT)] { + let at = screen_text_at(&rows, label); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers }; + panel(&mut app, time, vec![egui::Event::ModifiersChanged(modifiers), egui::Event::PointerMoved(at), button(true), button(false)]); + } + assert_eq!(crate::panels::artboards::picked(&app, 4), [1, 2], "the panel highlights rows 2 and 3"); + open(&mut app); + let picked = app.ui.dialog.take().expect("a dialog is open").fields; + assert_eq!((&picked["select"], &picked["range"], &picked["boards"]), (&json!("range"), &json!("2-3"), &json!([false, true, true, false]))); + // With the Selection tool in use, the dialog opens on the saved choice. + app.run("tool.select", json!({"tool": "selection"})).unwrap(); + open(&mut app); + assert_eq!((field(&app, "select"), field(&app, "range"), field(&app, "boards")), (json!("full"), json!("1-4"), json!([true, true, true, true]))); + assert_eq!((field(&app, "prefix"), field(&app, "formats")[0]["format"].clone()), (json!("proof-"), json!("jpg"))); + // Only the artboard fields differ. + let others = |fields: &serde_json::Map| { + let mut f = fields.clone(); + for k in ["select", "range", "boards"] { + f.remove(k); + } + f + }; + assert_eq!(others(&picked), others(&app.ui.dialog.as_ref().expect("a dialog is open").fields)); +} + #[test] fn exports_into_the_folder_and_shows_it() { let (mut app, log) = app(2, false); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 046f43142..06732c572 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -162,7 +162,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "file.exportForScreens", "Export for Screens…", "Cmd+Alt+E", - "{} opens the dialog (on the document's last settings); with params = document.exportForScreens (no folder on the web: downloads the files or the zip; openLocation: shows the first file in the file manager)", + "{} opens the dialog (on the document's last settings; while the Artboard tool is in use, with the artboards it has selected chosen); with params = document.exportForScreens (no folder on the web: downloads the files or the zip; openLocation: shows the first file in the file manager)", ), ("file.documentSetup", "Document Setup…", "Cmd+Alt+P", "{}"), ("file.newDialog", "New…", "Cmd+N", "{} opens the New Document dialog"), diff --git a/docs/control-protocol.md b/docs/control-protocol.md index cbb9e3ab3..e14ab8989 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -115,7 +115,7 @@ the two can still win that race. | `engine.execute` `document.exportForOffice` | `{}` / `{path?, ppi?: 72 \| 150 \| 300, transparent?, artboard?}` | File → Save for Office Documents…: without params opens `saveForOffice` (fields `ppi`, `transparent`, `artboard`; OK picks the file); with params writes the artboard as a PNG (`path`, else a picked file) → `{path}`. Headless it returns `{dataBase64, width, height}` | | Native file dialogs | any command with no `path` that needs one (`file.open`, `file.saveAs`, `file.place`, exports…) | on macOS (a sheet on the window) and Linux the dialog doesn't hold the window: the call returns `"cancelled"` at once, `ui.inspect` reports `fileDialog`, and when the person at the computer picks a file the command runs again on the document it was asked for. While it is open another dialog is refused: that call returns `"cancelled"` too, and the status bar (`ui.inspect` → `ui.status`) says `"A file dialog is open: choose there first"`; on macOS `app.quit`, `file.close` and `file.closeAll` return that message and the menu bar does nothing (#592, #867). On Windows the dialog is shown in line: the call returns once it closes. Pass `path` to skip the dialog | | 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 | +| `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`), `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 | | 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}` | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 76517d720..41c8bbb39 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -377,6 +377,7 @@ since 2026-10-01. ## Evidence: what ships today, by area - **TIFF in screen and asset export:** both format menus expose the existing TIFF encoder and Format Settings. Headless UI regressions choose TIFF and check scaled dimensions, artboard selection, filenames, encoding options, saved settings and collected-asset output. Public reference: [Export for Screens](https://helpx.adobe.com/illustrator/desktop/save-and-export/export-files-to-different-formats/export-for-screens.html), updated 2026-09-28, lists TIFF for both artboards and assets. Adobe staff [confirmed support in 30.2.1](https://illustrator.uservoice.com/forums/333657-illustrator-desktop-feature-requests/suggestions/49081034-add-tiff-format-into-export-for-screens-assets-d) on 2026-02-19; the older 2025 Collect assets guide omits TIFF. +- **Export for Screens of the selected artboards:** while the Artboard tool is in use, File › Export › Export for Screens… (`file.exportForScreens`) opens with the artboards the tool has selected checked on its Artboards tab, and Export Artboard writes only those. Shift-clicks on the canvas, or Shift- or Ctrl/Cmd-clicks on the Artboards panel's rows, select several. The other fields show the settings the document last exported with. With another tool in use, the dialog takes its artboard choice from those settings too. Public reference: [Export selected artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/export-artboards.html). - **Effects in Export Selection and asset crops:** File › Export Selection, Asset Export, Export for Screens' Assets tab and CSS Properties' rasterized art crop each object to the bounds the renderer measures for what it draws: drop shadows, glows, geometry effects such as Offset Path, a group's own effects and the effects in a symbol's art. A brushed path is cropped to its visual bounds, and a Shaper group is cropped to the art it shows. `assets.list` reports the same crop. Engine tests export a square with each effect, a group with a shadow and an asset with a shadow, and check that the effect is inside the image. Public reference: a [Community Expert answer](https://community.adobe.com/t5/illustrator-discussions/exporting-assets-adds-transparent-borders/m-p/10708713/highlight/true) traces the extra area of exported assets to the raster effects' area (Document Raster Effects Settings › Add Around Object).