From a19646b2f36442d2346e9834e6a4d558855a1b40 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 11:00:40 -0800 Subject: [PATCH 1/2] Asset Export: Create Sub-folders, Open Location after Export and Format Settings in the panel menu Create Sub-folders and Open Location after Export are check items that set the settings the panel shares with Export for Screens, and the panel's Export follows them. Create Sub-folders is checked as the dialog opens on it: on for a document without export settings, and on for remembered folders per row, format or item, which the panel's Export now uses too. Format Settings... opens Export for Screens on the Assets tab showing the first row's format settings. assets.settings.set takes openLocation (kept in document.exportSettings, not in presets), refuses a subfolders or openLocation value that is not true, false or null, and with subfolders false given alone also ends a remembered folder per format or item. Export for Screens reads remembered empty folders as none, as an export does. --- crates/engine/src/cmd/assets.rs | 24 ++- crates/engine/src/tests/assets.rs | 36 ++++ crates/ui-egui/src/dialogs.rs | 2 +- .../ui-egui/src/dialogs/export_for_screens.rs | 23 ++- crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/gu.tsv | 1 + crates/ui-egui/src/i18n/hi.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/panels/asset_export.rs | 79 +++++++- .../ui-egui/src/panels/tests_asset_export.rs | 178 +++++++++++++++++- docs/control-protocol.md | 2 +- docs/mcp.md | 7 +- docs/target-app-parity.md | 1 + 37 files changed, 353 insertions(+), 27 deletions(-) diff --git a/crates/engine/src/cmd/assets.rs b/crates/engine/src/cmd/assets.rs index e30e6efcdc..cb990e4857 100644 --- a/crates/engine/src/cmd/assets.rs +++ b/crates/engine/src/cmd/assets.rs @@ -89,7 +89,7 @@ pub fn specs() -> Vec { "Asset Export Settings", [], None, - "{exportPreset?: (a preset of document.exportPresets.list: its formats or preset, its format settings over the saved ones, its prefix (none when it has none), its folders (or subfolders: rows or none) with subfolders on for rows, and antiAlias; the other keys go over it), formats?, preset?: mobile|density|\"\" (none), settings?, prefix?, subfolders?, antiAlias?} change the export settings the Asset Export panel and Export for Screens share (document.exportSettings; the values of document.exportForScreens; null removes one): formats replace a preset, a preset replaces the formats → {settings}. Not an undo step", + "{exportPreset?: (a preset of document.exportPresets.list: its formats or preset, its format settings over the saved ones, its prefix (none when it has none), its folders (or subfolders: rows or none) with subfolders on for rows, and antiAlias; the other keys go over it), formats?, preset?: mobile|density|\"\" (none), settings?, prefix?, subfolders?: true|false (Create Sub-folders: true sorts the files into a folder per row; false puts assets.export's files in the folder itself and, given without exportPreset, sets a remembered folder per format or item to none as well), antiAlias?, openLocation?: true|false (Open Location after Export: remembered in document.exportSettings, not in presets or the reply; given alone to a document without settings, Create Sub-folders stays on)} change the export settings the Asset Export panel and Export for Screens share (document.exportSettings; the values of document.exportForScreens; null removes one): formats replace a preset, a preset replaces the formats → {settings}. Not an undo step", has_doc, settings_set ), @@ -435,19 +435,35 @@ fn export(s: &mut Session, p: &Value) -> Result { fn settings_set(s: &mut Session, p: &Value) -> Result { const C: &str = "assets.settings.set"; let o = p.as_object().ok_or_else(|| bad(C, "give the settings to change, e.g. {\"formats\": [{\"format\": \"png\", \"scale\": \"2x\"}]}"))?; - if let Some(k) = o.keys().find(|k| !SCREEN_SETTINGS_KEYS.contains(&k.as_str()) && k.as_str() != "exportPreset") { - return Err(bad(C, format!("`{k}` isn't an export setting: {}", SCREEN_SETTINGS_KEYS.join(", ")))); + if let Some(k) = o.keys().find(|k| !SCREEN_SETTINGS_KEYS.contains(&k.as_str()) && !matches!(k.as_str(), "exportPreset" | "openLocation")) { + return Err(bad(C, format!("`{k}` isn't an export setting: {}, openLocation", SCREEN_SETTINGS_KEYS.join(", ")))); + } + for k in ["subfolders", "openLocation"] { + if o.get(k).is_some_and(|v| !v.is_boolean() && !v.is_null()) { + return Err(bad(C, format!("`{k}` is true or false"))); + } } let mut settings = s.doc()?.doc.export_settings.clone(); let mut given = o.clone(); + // Create Sub-folders is on for a document without settings (Export for Screens opens so), and + // Open Location after Export set on its own leaves it on. + if settings.is_empty() && o.len() == 1 && o.get("openLocation").is_some_and(Value::is_boolean) { + given.insert("subfolders".into(), json!(true)); + } if let Some(name) = given.remove("exportPreset") { let name = name.as_str().ok_or_else(|| bad(C, "`exportPreset` is the name of a preset of document.exportPresets.list"))?; with_screen_export_preset(s, C, &mut settings, name)?; } // Create Sub-folders is Export for Screens' folder per row: the dialog opens on the choice. + // Turned off on its own (the panel menu), it also ends a remembered folder per format or item; + // with an export preset, that preset's folder per format or item stays. + let folders = settings.get("folders").and_then(Value::as_str).map(str::to_string); + let sorted = folders.as_deref().is_some_and(|f| f != "none"); match given.get("subfolders").and_then(Value::as_bool) { Some(true) => _ = settings.insert("folders".into(), json!("rows")), - Some(false) if settings.get("folders").and_then(Value::as_str) == Some("rows") => _ = settings.insert("folders".into(), json!("none")), + Some(false) if folders.as_deref() == Some("rows") || (sorted && !o.contains_key("exportPreset")) => { + _ = settings.insert("folders".into(), json!("none")) + } _ => {} } overlay(&mut settings, &given); diff --git a/crates/engine/src/tests/assets.rs b/crates/engine/src/tests/assets.rs index 8719873a94..64ef55d7ab 100644 --- a/crates/engine/src/tests/assets.rs +++ b/crates/engine/src/tests/assets.rs @@ -301,6 +301,42 @@ fn an_export_preset_sets_the_shared_settings() { assert_eq!(run(&mut s, "document.exportSettings", json!({}))["settings"]["prefix"], "p_"); } +/// `openLocation` (Open Location after Export) is remembered in the document's export settings, +/// outside presets; null removes it, and only true or false is taken. +#[test] +fn open_location_is_remembered_in_the_export_settings() { + let mut s = session(); + let open = |s: &mut Session| run(s, "document.exportSettings", json!({}))["settings"]["openLocation"].clone(); + let r = run(&mut s, "assets.settings.set", json!({"openLocation": false})); + assert_eq!(open(&mut s), false); + assert!(r["settings"].get("openLocation").is_none(), "{r}"); + // Create Sub-folders, on for a document without settings, stays on. + assert_eq!(r["settings"]["subfolders"], true, "{r}"); + run(&mut s, "document.exportPresets.save", json!({"name": "Kept", "settings": {"formats": [{"format": "svg"}]}})); + run(&mut s, "assets.settings.set", json!({"exportPreset": "Kept"})); + assert_eq!(open(&mut s), false, "a preset leaves it as it is"); + run(&mut s, "assets.settings.set", json!({"openLocation": null})); + assert_eq!(open(&mut s), Value::Null); + for bad in [json!({"openLocation": "yes"}), json!({"openLocation": 1}), json!({"subfolders": "no"})] { + assert!(s.execute("assets.settings.set", &bad).is_err(), "{bad}"); + } +} + +/// Create Sub-folders turned off on its own ends a remembered folder per format or item, so Export +/// for Screens opens with it off too; given with an export preset, the preset's folders stay. +#[test] +fn create_sub_folders_off_alone_ends_a_folder_per_format_or_item() { + let mut s = session(); + let folders = |s: &mut Session| run(s, "document.exportSettings", json!({}))["settings"]["folders"].clone(); + run(&mut s, "document.exportPresets.save", json!({"name": "Items", "settings": {"formats": [{"format": "svg"}], "folders": "item"}})); + for preset in ["Logo Package", "Items"] { + run(&mut s, "assets.settings.set", json!({"exportPreset": preset, "subfolders": false})); + assert_ne!(folders(&mut s), "none", "{preset}"); + run(&mut s, "assets.settings.set", json!({"subfolders": false})); + assert_eq!(folders(&mut s), "none", "{preset}"); + } +} + /// An export preset applied to the shared settings keeps its folders, with Create Sub-folders on /// for a folder per row; with Create Sub-folders off every file goes into the folder itself, /// mobile and density rows too. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5afad21b0d..13975cf1f9 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -159,7 +159,7 @@ pub use export_for_screens::open as open_export_for_screens; pub(crate) use export_for_screens::{ KIND as EXPORT_FOR_SCREENS, PresetPick as ScreenPresetPick, export_preset as screen_export_preset, formats as screen_formats, import_presets as screen_import_presets, open_assets as open_export_for_screens_assets, preset_list as screen_preset_list, - saved_options as screen_saved_options, saved_rows as screen_saved_rows, + saved_options as screen_saved_options, saved_rows as screen_saved_rows, settings_format as screen_settings_format, }; pub(crate) use form::{humanize, number_list_text, plugin_color_button, read_number_list, unit_suffix}; pub use new_document::{create_without_dialog as new_document_without_dialog, open as open_new_document, preset_card}; diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index 54c98052bf..ecd9ca6996 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -156,7 +156,8 @@ pub fn open(app: &mut VectorcraftApp) { // `folders: "format"`, any other sorting a folder per scale. let folders = get("folders").and_then(Value::as_str); let subfolders = match folders { - Some("none") => false, + // An export reads empty folders as none. + Some("none" | "") => false, Some(_) => true, None => flag("subfolders", saved.is_empty() || preset.is_some()), }; @@ -955,14 +956,7 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog, mut menu: Option &'static str { + rows.first() + .and_then(|r| r["format"].as_str()) + .and_then(fileio::format) + .map(|f| f.id) + .filter(|f| SETTINGS.iter().any(|(id, _)| id == f)) + .unwrap_or("png") +} + /// Format Settings: the options of format `id` that apply to every row of it, then Done. fn format_settings(ui: &mut egui::Ui, d: &mut Dialog, id: &str) { let t = Tokens::get(ui.ctx()); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 28944173dd..016dbdc0c6 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1284,6 +1284,7 @@ font-technology Variable Променливи Format Формат Format Options Опции за формат Format Settings Настройки на формата + Format Settings… Настройки на формата… Format: Формат: Formats: Формати: Fractional Widths Дробни ширини diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 01b90e5148..96f0fd64c9 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -2500,6 +2500,7 @@ digits Malayalam Malajalam Export Asset Izvezi resurs Export to: Izvezi u: Format Settings Postavke formata + Format Settings… Postavke formata… Formats: Formati: Full Document Cijeli dokument Include Bleed Uključi napust diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 4ddd1d5605..21104a526e 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1794,6 +1794,7 @@ digits Malayalam Malaiàlam Format Format Format Options Opcions de format Format Settings Configuració de format + Format Settings… Configuració de format… Format: Format: Formats: Formats: Fractional Widths Amplades fraccionàries diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 211982f086..befa2f17a9 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2533,6 +2533,7 @@ digits Malayalam Malajálamské Export Asset Exportovat prvek Export to: Exportovat do: Format Settings Nastavení formátu + Format Settings… Nastavení formátu… Formats: Formáty: Full Document Celý dokument Include Bleed Zahrnout spadávku diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b3de0ae89a..c1979782f6 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2326,6 +2326,7 @@ digits Malayalam Malayalam Export Asset Eksportér aktiv Export to: Eksportér til: Format Settings Formatindstillinger + Format Settings… Formatindstillinger… Formats: Formater: Full Document Hele dokumentet Include Bleed Medtag beskæring diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index d0610144ef..6821a71bb4 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1329,6 +1329,7 @@ file-filter Templates Vorlagen Format Format Format Options Formatoptionen Format Settings Formateinstellungen + Format Settings… Formateinstellungen… Format: Format: Formats: Formate: Fractional Widths Gebrochene Breiten diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index d03f8c6958..f896255a74 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1262,6 +1262,7 @@ font-technology Variable Μεταβλητές Format Μορφή Format Options Επιλογές μορφής Format Settings Ρυθμίσεις μορφής + Format Settings… Ρυθμίσεις μορφής… Format: Μορφή: Formats: Μορφές: Fractional Widths Κλασματικά πλάτη diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 3e8bd23752..7f329e8b24 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1338,6 +1338,7 @@ font-technology Variable Variables Format Formato Format Options Opciones de formato Format Settings Ajustes de formato + Format Settings… Ajustes de formato… Format: Formato: Formats: Formatos: Fractional Widths Anchuras fraccionarias diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index d408220f47..c1c63d9115 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -2488,6 +2488,7 @@ digits Malayalam Malajalam Export Asset Vie resurssi Export to: Vie kohteeseen: Format Settings Muotoasetukset + Format Settings… Muotoasetukset… Formats: Muodot: Full Document Koko asiakirja Include Bleed Sisällytä leikkuuvara diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 318bbbeb44..1e33a4880c 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1332,6 +1332,7 @@ font-technology Variable Variables Format Format Format Options Options de format Format Settings Paramètres de format + Format Settings… Paramètres de format… Format: Format : Formats: Formats : Fractional Widths Largeurs fractionnaires diff --git a/crates/ui-egui/src/i18n/gu.tsv b/crates/ui-egui/src/i18n/gu.tsv index 716d851655..0869d5ccda 100644 --- a/crates/ui-egui/src/i18n/gu.tsv +++ b/crates/ui-egui/src/i18n/gu.tsv @@ -1454,6 +1454,7 @@ Format Options ફોર્મેટ વિકલ્પો Format Presets ફોર્મેટ પ્રીસેટ્સ Format Settings ફોર્મેટ સેટિંગ્સ + Format Settings… ફોર્મેટ સેટિંગ્સ… Format: ફોર્મેટ: Formats: ફોર્મેટ્સ: Fractional Widths અપૂર્ણાંક પહોળાઈ diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 78c0a9a7eb..ef6b7c292c 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -1990,6 +1990,7 @@ antiAlias Sharp शार्प Format Options फ़ॉर्मेट ऑप्शन्स Format Presets फ़ॉर्मैट प्रीसेट Format Settings फ़ॉर्मैट सेटिंग + Format Settings… फ़ॉर्मैट सेटिंग… Format: फ़ॉर्मेट: Formats: फ़ॉर्मैट: Fractional Widths आंशिक चौड़ाई diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 7f2f03afb2..b494e8718b 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1663,6 +1663,7 @@ file-filter Templates Sablonok Format Formátum Format Options Formátumbeállítások Format Settings Formátumbeállítások + Format Settings… Formátumbeállítások… Format: Formátum: Formats: Formátumok: Fractional Widths Tört szélességek diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index c7c6b956db..0a497c0759 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -2326,6 +2326,7 @@ digits Malayalam Malayalam Export Asset Ekspor aset Export to: Ekspor ke: Format Settings Pengaturan format + Format Settings… Pengaturan format… Formats: Format: Full Document Dokumen penuh Include Bleed Sertakan bleed diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index f832a5baf3..d659800f9f 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1359,6 +1359,7 @@ file-filter Templates Modelli Format Formato Format Options Opzioni formato Format Settings Impostazioni formato + Format Settings… Impostazioni formato… Format: Formato: Formats: Formati: Fractional Widths Larghezze frazionarie diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 3abb388f49..ce93e3b316 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1834,6 +1834,7 @@ antiAlias Sharp シャープ Format 形式 Format Options 形式オプション Format Settings 形式の設定 + Format Settings… 形式の設定… Format: 形式: Formats: 形式: Fractions 分数 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 55f726dc38..2c47f65595 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -2252,6 +2252,7 @@ digits Malayalam მალაიალამური Export Asset რესურსის ექსპორტირება Export to: ექსპორტის ადგილი: Format Settings ფორმატის პარამეტრები + Format Settings… ფორმატის პარამეტრები… Formats: ფორმატები: Full Document მთელი დოკუმენტი Include Bleed ჭრისპირა არის ჩართვა diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index abe2b4bc28..a95d60d006 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1824,6 +1824,7 @@ antiAlias Sharp 날카롭게 Format 형식 Format Options 형식 옵션 Format Settings 형식 설정 + Format Settings… 형식 설정… Format: 형식: Formats: 형식: Fractions 분수 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index ed281382a4..bc2281a05e 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2844,6 +2844,7 @@ digits Malayalam Malayalam Export Asset Asset exporteren Export to: Exporteren naar: Format Settings Indelingsinstellingen + Format Settings… Indelingsinstellingen… Formats: Indelingen: Full Document Volledig document Include Bleed Afrulling opnemen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 08e96e6a35..b0208e3bc2 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2448,6 +2448,7 @@ digits Malayalam Malajalam Export Asset Eksportuj zasób Export to: Eksportuj do: Format Settings Ustawienia formatu + Format Settings… Ustawienia formatu… Formats: Formaty: Full Document Cały dokument Include Bleed Uwzględnij spad diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 79c4d9dc8e..c610792789 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1200,6 +1200,7 @@ antiAlias Sharp Nítido Format Formato Format Options Opções de Formato Format Settings Configurações de Formato + Format Settings… Configurações de Formato… Format: Formato: Formats: Formatos: Fractions Frações diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 9f608618e7..6b18a28844 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1348,6 +1348,7 @@ file-filter Templates Шаблоны Format Формат Format Options Параметры формата Format Settings Параметры формата + Format Settings… Параметры формата… Format: Формат: Formats: Форматы: Fractional Widths Дробные ширины diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 6156ca23d5..42199d0d29 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1297,6 +1297,7 @@ font-technology Variable Variabla Format Format Format Options Formatalternativ Format Settings Formatinställningar + Format Settings… Formatinställningar… Format: Format: Formats: Format: Fractional Widths Bråkdelsbredder diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 7120d44707..fa581ea21f 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1290,6 +1290,7 @@ font-technology Variable Değişken Format Biçim Format Options Biçim Seçenekleri Format Settings Biçim Ayarları + Format Settings… Biçim Ayarları… Format: Biçim: Formats: Formatlar: Fractional Widths Kesirli Genişlikler diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 84f296b0a6..372e0f33a2 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1353,6 +1353,7 @@ file-filter Templates Шаблони Format Формат Format Options Параметри формату Format Settings Параметри формату + Format Settings… Параметри формату… Format: Формат: Formats: Формати: Fractional Widths Дробові ширини diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index e8b9e5758f..7232e1f12a 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1573,6 +1573,7 @@ file-filter Templates Mẫu Format Định dạng Format Options Tùy chọn định dạng Format Settings Cài đặt định dạng + Format Settings… Cài đặt định dạng… Format: Định dạng: Formats: Các định dạng: Fractional Widths Chiều rộng phân số diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 719968086f..af4ff8fdda 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1289,6 +1289,7 @@ file-filter Templates 模板 Format 格式 Format Options 格式选项 Format Settings 格式设置 + Format Settings… 格式设置… Format: 格式: Formats: 格式: Fractional Widths 比例宽度 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 121a8b8c02..7336c40e4b 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1324,6 +1324,7 @@ file-filter Templates 範本 Format 格式 Format Options 格式選項 Format Settings 格式設定 + Format Settings… 格式設定… Format: 格式: Formats: 格式: Fractional Widths 小數字寬 diff --git a/crates/ui-egui/src/panels/asset_export.rs b/crates/ui-egui/src/panels/asset_export.rs index 021bc848c9..39b3a0ea85 100644 --- a/crates/ui-egui/src/panels/asset_export.rs +++ b/crates/ui-egui/src/panels/asset_export.rs @@ -11,6 +11,12 @@ //! marked 3D on their tiles. While the panel's assets include 3D assets, a Format row picks their //! mesh format (USDA, USDZ, glTF or OBJ), and Export writes them as mesh files beside the pictures. //! +//! The panel menu also holds Format Settings… (Export for Screens showing the first row's format +//! settings), and Create Sub-folders and Open Location after Export, the dialog's two checkboxes, +//! which Export follows. Create Sub-folders sorts the pictures into a folder per format row, or per +//! format or item when Export for Screens remembers that choice; 3D assets' mesh files go in the +//! folder itself. +//! //! Everything runs through the `assets.*` commands. use std::cell::RefCell; @@ -26,7 +32,7 @@ use vectorcraft_engine::cmd::fileio::{FORMATS_3D, SCREEN_SETTINGS_KEYS, format_3 use super::{pstate, set_pstate}; use crate::dialogs::{ EXPORT_FOR_SCREENS, ScreenPresetPick, drop_unread_screen_settings, open_export_for_screens_assets, screen_export_preset, screen_formats, - screen_import_presets, screen_preset_list, screen_saved_options, screen_saved_rows, + screen_import_presets, screen_preset_list, screen_saved_options, screen_saved_rows, screen_settings_format, }; use crate::state::Dialog; use crate::theme::Tokens; @@ -229,7 +235,14 @@ fn export_now(app: &mut VectorcraftApp, ids: Vec, files_each: usize) { let (three_d, ids): (Vec, Vec) = ids.into_iter().partition(|id| format(id).is_some()); // An OBJ is two files. let mesh_files: usize = three_d.iter().map(|id| if format(id).as_deref() == Some("obj") { 2 } else { 1 }).sum(); - let mut p = json!({ "assets": ids }); + // Create Sub-folders as the panel menu checks it: on by default, and remembered folders (per + // row, format or item) are used as Export for Screens uses them (a remembered `subfolders` + // false beside them would alone put every file in the folder itself). + let saved = &st.doc.export_settings; + let mut p = keep_subfolders(saved, json!({ "assets": ids })); + if let Some(f) = saved.get("folders").and_then(Value::as_str).filter(|f| !matches!(*f, "none" | "")) { + p["folders"] = json!(f); + } // Match the shared dialog's TIFF defaults, retaining saved choices and other formats. if let Some(st) = app.session.active() && screen_saved_rows(&st.doc.export_settings) @@ -267,7 +280,7 @@ fn export_now(app: &mut VectorcraftApp, ids: Vec, files_each: usize) { } }; p["folder"] = json!(folder); - p["openLocation"] = json!(saved.get("openLocation").and_then(Value::as_bool).unwrap_or(true)); + p["openLocation"] = json!(open_location(&saved)); } let mut meshes = json!({ "assets": three_d }); if io::is_web(app) { @@ -290,6 +303,43 @@ fn export_now(app: &mut VectorcraftApp, ids: Vec, files_each: usize) { } } +/// Create Sub-folders for the remembered settings `saved`, as Export for Screens opens on it: off +/// for remembered folders `none`, on for any other remembered folders (a folder per row, format or +/// item); without them the remembered `subfolders`, else on for a document without settings or +/// with a preset's rows. +fn subfolders_on(saved: &serde_json::Map) -> bool { + match saved.get("folders").and_then(Value::as_str) { + // An export reads empty folders as none. + Some("none" | "") => false, + Some(_) => true, + None => saved.get("subfolders").and_then(Value::as_bool).unwrap_or_else(|| saved.is_empty() || screen_saved_rows(saved).1.is_some()), + } +} + +/// `p` (params of `assets.settings.set` or `assets.export`) with `subfolders: true` when `saved` +/// makes no Create Sub-folders choice and it is on by default: the export would otherwise write +/// into the folder itself, and settings `p` adds would turn the default off. +fn keep_subfolders(saved: &serde_json::Map, mut p: Value) -> Value { + if !saved.contains_key("folders") && !saved.contains_key("subfolders") && subfolders_on(saved) { + p["subfolders"] = json!(true); + } + p +} + +/// Open Location after Export for the remembered settings `saved` (on until it is turned off). +fn open_location(saved: &serde_json::Map) -> bool { + saved.get("openLocation").and_then(Value::as_bool).unwrap_or(true) +} + +/// Open Export for Screens on the Assets tab with assets `ids` checked, showing the Format Settings +/// of format `format`. +fn format_settings(app: &mut VectorcraftApp, ids: &[u64], format: &str) { + open_export_for_screens_assets(app, Some(ids)); + if let Some(d) = app.ui.dialog.as_mut() { + d.fields.insert("__settings".into(), json!(format)); + } +} + /// What the export settings rows asked for. enum Settings { /// Change the shared settings (`assets.settings.set`). @@ -328,7 +378,7 @@ fn settings(ui: &mut Ui, st: &DocState) -> (Option, usize) { Some(Settings::Set(json!({ "preset": picked, "subfolders": true }))) } else if picked != preset || formats.as_array() != Some(&rows) { // Rows edited, or Custom picked: the rows as they are, without the preset. - Some(Settings::Set(json!({ "formats": formats, "preset": "" }))) + Some(Settings::Set(keep_subfolders(&st.doc.export_settings, json!({ "formats": formats, "preset": "" })))) } else { None }; @@ -432,12 +482,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { Some(Settings::Set(p)) => { run(app, "assets.settings.set", p); } - Some(Settings::Format(f)) => { - open_export_for_screens_assets(app, Some(&sel_or_all(&sel, &all))); - if let Some(d) = app.ui.dialog.as_mut() { - d.fields.insert("__settings".into(), json!(f)); - } - } + Some(Settings::Format(f)) => format_settings(app, &sel_or_all(&sel, &all), &f), None => {} } if let Some(pick) = save_pick { @@ -575,6 +620,9 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let doc = st.uid; let (has_art, sel) = (!st.selection.is_empty(), selected(&ctx, st)); let all: Vec = st.doc.assets.iter().map(|a| a.id).collect(); + let saved = &st.doc.export_settings; + let (subfolders, open_after) = (subfolders_on(saved), open_location(saved)); + let settings_format = screen_settings_format(&screen_saved_rows(saved).0); if menu_item(ui, tl!("Add Selected Artwork as One Asset"), has_art, false) { collect(app, &ctx, None, true); } @@ -599,6 +647,17 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, tl!("Export for Screens…"), true, false) { open_export_for_screens_assets(app, Some(&sel_or_all(&sel, &all))); } + if menu_item(ui, tl!("Format Settings…"), true, false) { + format_settings(app, &sel_or_all(&sel, &all), settings_format); + } + ui.separator(); + if menu_item(ui, tl!("Create Sub-folders"), true, subfolders) { + run(app, "assets.settings.set", json!({ "subfolders": !subfolders })); + } + // The web downloads the files: there is no location to open. + if !io::is_web(app) && menu_item(ui, tl!("Open Location after Export"), true, open_after) { + run(app, "assets.settings.set", json!({ "openLocation": !open_after })); + } ui.separator(); let (preset_doc, current): (u64, String) = pstate(&ctx, PRESET); let current = if preset_doc == doc { current } else { String::new() }; diff --git a/crates/ui-egui/src/panels/tests_asset_export.rs b/crates/ui-egui/src/panels/tests_asset_export.rs index 82a6f21245..6383febda4 100644 --- a/crates/ui-egui/src/panels/tests_asset_export.rs +++ b/crates/ui-egui/src/panels/tests_asset_export.rs @@ -526,10 +526,186 @@ fn export_writes_into_the_picked_folder() { frame(&ctx, &mut app, vec![], false); let texts = frame(&ctx, &mut app, vec![], false); frame(&ctx, &mut app, click(at(&texts, "Export")), false); - assert!(dir.join("Badge.png").is_file(), "{:?}", std::fs::read_dir(&dir).map(|d| d.count())); + // Create Sub-folders is on by default: the 1x row's folder. + assert!(dir.join("1x").join("Badge.png").is_file(), "{:?}", std::fs::read_dir(&dir).map(|d| d.count())); let _ = std::fs::remove_dir_all(&dir); } +/// The panel menu's Create Sub-folders and Open Location after Export are check items, on by +/// default, that change the settings the panel shares with Export for Screens, and the panel's +/// Export follows them. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn the_menu_sets_create_subfolders_and_open_location_and_export_follows_them() { + let dir = std::env::temp_dir().join(format!("vc-asset-panel-menu-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let folder = dir.to_string_lossy().replace('\\', "/"); + let (mut app, _, [a, _]) = app(false); + let picked = folder.clone(); + app.services.pick_folder = Some(Box::new(move || Some(picked.clone()))); + let shown = Rc::new(RefCell::new(Vec::::new())); + let s = shown.clone(); + app.services.reveal = Some(Box::new(move |path: &str| { + s.borrow_mut().push(path.to_string()); + Ok(()) + })); + app.run("assets.add", json!({"ids": [a], "name": "Badge"})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + menu_frame(&ctx, &mut app, vec![]); + let texts = menu_frame(&ctx, &mut app, vec![]); + for item in ["✓ Create Sub-folders", "✓ Open Location after Export", " Format Settings…"] { + assert!(has(&texts, item), "{item}"); + } + // Both on: the 1x row's folder, and the file manager shows the file. + let export = |app: &mut VectorcraftApp| { + frame(&ctx, app, vec![], false); + let texts = frame(&ctx, app, vec![], false); + frame(&ctx, app, click(at(&texts, "Export")), false); + }; + export(&mut app); + assert!(dir.join("1x").join("Badge.png").is_file()); + assert_eq!(shown.borrow().len(), 1); + // Both turned off (a click lands on the items of the frame before). + menu_frame(&ctx, &mut app, vec![]); + menu_frame(&ctx, &mut app, click(at(&texts, "✓ Create Sub-folders"))); + let texts = menu_frame(&ctx, &mut app, vec![]); + menu_frame(&ctx, &mut app, click(at(&texts, "✓ Open Location after Export"))); + let settings = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone(); + assert_eq!((&settings["subfolders"], &settings["openLocation"]), (&json!(false), &json!(false)), "{settings}"); + let texts = menu_frame(&ctx, &mut app, vec![]); + assert!(has(&texts, " Create Sub-folders") && has(&texts, " Open Location after Export")); + // Export for Screens opens on the same choices. + app.run("file.exportForScreens", json!({})).unwrap(); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((&d.fields["subfolders"], &d.fields["openLocation"]), (&json!(false), &json!(false))); + crate::dialogs::cancel(&mut app); + let _ = std::fs::remove_dir_all(&dir); + export(&mut app); + assert!(dir.join("Badge.png").is_file() && !dir.join("1x").exists()); + assert_eq!(shown.borrow().len(), 1, "no file manager after export"); + // Create Sub-folders on again: a folder per row. + menu_frame(&ctx, &mut app, vec![]); + menu_frame(&ctx, &mut app, click(at(&texts, " Create Sub-folders"))); + let settings = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone(); + assert_eq!((&settings["subfolders"], &settings["folders"]), (&json!(true), &json!("rows")), "{settings}"); + // Only true and false are taken. + for bad in [json!({"openLocation": "yes"}), json!({"subfolders": 1})] { + assert!(app.run("assets.settings.set", bad.clone()).is_err(), "{bad}"); + } + let _ = std::fs::remove_dir_all(&dir); +} + +/// A folder per format, remembered by Export for Screens or a preset, is used by the panel's Export +/// too. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn export_uses_a_remembered_folder_per_format() { + let dir = std::env::temp_dir().join(format!("vc-asset-panel-format-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let folder = dir.to_string_lossy().replace('\\', "/"); + let (mut app, _, [a, _]) = app(false); + app.services.pick_folder = Some(Box::new(move || Some(folder.clone()))); + let asset = app.run("assets.add", json!({"ids": [a], "name": "Badge"})).unwrap()["assets"][0].clone(); + // An export as Export for Screens sends it with Create Sub-folders by Format, which the + // document remembers. + let formats = json!([{"format": "png"}, {"format": "svg"}]); + app.run("document.exportForScreens", json!({"assets": [asset], "formats": formats, "folders": "format", "subfolders": false})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + menu_frame(&ctx, &mut app, vec![]); + assert!(has(&menu_frame(&ctx, &mut app, vec![]), "✓ Create Sub-folders")); + frame(&ctx, &mut app, vec![], false); + let texts = frame(&ctx, &mut app, vec![], false); + frame(&ctx, &mut app, click(at(&texts, "Export")), false); + assert!(dir.join("png").join("Badge.png").is_file() && dir.join("svg").join("Badge.svg").is_file()); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Remembered empty folders are none, as an export reads them: Create Sub-folders is off in the +/// panel menu and in Export for Screens. +#[test] +fn empty_remembered_folders_are_create_subfolders_off() { + let (mut app, _, [a, _]) = app(true); + let asset = app.run("assets.add", json!({"ids": [a]})).unwrap()["assets"][0].clone(); + app.run("document.exportForScreens", json!({"assets": [asset], "formats": [{"format": "svg"}], "folders": "", "subfolders": true})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + menu_frame(&ctx, &mut app, vec![]); + assert!(has(&menu_frame(&ctx, &mut app, vec![]), " Create Sub-folders")); + app.run("file.exportForScreens", json!({})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["subfolders"], false); +} + +/// Open Location after Export turned off in a document without settings leaves Create Sub-folders +/// on, in the panel menu and in Export for Screens. +#[test] +fn open_location_off_leaves_create_subfolders_on() { + let (mut app, _, _) = app(false); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + menu_frame(&ctx, &mut app, vec![]); + let texts = menu_frame(&ctx, &mut app, vec![]); + menu_frame(&ctx, &mut app, click(at(&texts, "✓ Open Location after Export"))); + let texts = menu_frame(&ctx, &mut app, vec![]); + assert!(has(&texts, "✓ Create Sub-folders") && has(&texts, " Open Location after Export")); + app.run("file.exportForScreens", json!({})).unwrap(); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((&d.fields["subfolders"], &d.fields["openLocation"]), (&json!(true), &json!(false))); +} + +/// Editing the rows of a document without a Create Sub-folders choice keeps it on, in Export for +/// Screens too; a preset that turns it off shows it off, and the menu turns it on again. +#[test] +fn row_edits_keep_create_subfolders_on_and_a_preset_can_turn_it_off() { + let (mut app, _, [a, _]) = app(true); + app.run("assets.add", json!({"ids": [a]})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + frame(&ctx, &mut app, vec![], false); + let texts = frame(&ctx, &mut app, vec![], false); + frame(&ctx, &mut app, click(at(&texts, "+ Add Scale")), false); + assert!(has(&menu_frame(&ctx, &mut app, vec![]), "✓ Create Sub-folders")); + app.run("file.exportForScreens", json!({})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["subfolders"], true); + crate::dialogs::cancel(&mut app); + // Logo Package sorts by format: checked, as in Export for Screens. Turned off, the sorting + // ends; turned on again, a folder per row. + app.run("assets.settings.set", json!({"exportPreset": "Logo Package"})).unwrap(); + let texts = menu_frame(&ctx, &mut app, vec![]); + menu_frame(&ctx, &mut app, click(at(&texts, "✓ Create Sub-folders"))); + let settings = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone(); + assert_eq!((&settings["subfolders"], &settings["folders"]), (&json!(false), &json!("none")), "{settings}"); + app.run("file.exportForScreens", json!({})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["subfolders"], false); + crate::dialogs::cancel(&mut app); + let texts = menu_frame(&ctx, &mut app, vec![]); + menu_frame(&ctx, &mut app, click(at(&texts, " Create Sub-folders"))); + let settings = app.run("document.exportSettings", json!({})).unwrap()["settings"].clone(); + assert_eq!((&settings["subfolders"], &settings["folders"]), (&json!(true), &json!("rows")), "{settings}"); + let texts = menu_frame(&ctx, &mut app, vec![]); + assert!(has(&texts, "✓ Create Sub-folders")); + // The web downloads the files: the menu has no Open Location after Export. + assert!(!texts.iter().any(|(t, _)| t.ends_with("Open Location after Export"))); +} + +/// Format Settings… in the panel menu opens Export for Screens on its Assets tab, showing the +/// first row's format settings. +#[test] +fn format_settings_opens_the_first_rows_format_settings() { + let (mut app, _, [a, b]) = app(true); + app.run("assets.add", json!({"ids": [a, b], "multiple": true})).unwrap(); + app.run("assets.settings.set", json!({"formats": [{"format": "jpeg"}, {"format": "svg"}]})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + menu_frame(&ctx, &mut app, vec![]); + let texts = menu_frame(&ctx, &mut app, vec![]); + menu_frame(&ctx, &mut app, click(at(&texts, " Format Settings…"))); + let d = app.ui.dialog.as_ref().expect("Export for Screens"); + assert_eq!((d.kind.as_str(), &d.fields["tab"], &d.fields["__settings"]), (crate::dialogs::EXPORT_FOR_SCREENS, &json!("assets"), &json!("jpg"))); + assert_eq!(d.fields["assets"].as_array().map(Vec::len), Some(2)); +} + #[test] fn tiff_dropdown_exports_only_the_selected_asset_and_shares_settings() { let (mut app, log, [a, b]) = app(true); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index cc52d58ed7..f08e3a010d 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -122,7 +122,7 @@ the two can still win that race. | 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`, `file.exportForScreens {folder, …}` | 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 (`file.exportForScreens`: `{background: true, status}`, and the files are written into the folder from a snapshot taken at the call; an `app.export` given an option the format doesn't read: also `warnings`, naming the option), `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. A job that fails reports the error in the status bar only (`ui.inspect` → `ui.status`), a path outside `--automation-write-root` included. With the preferences off (`prefs.set`), saves return once written. `app.save {path?, wait: true}` and `app.export {path, wait: true}` wait for the jobs already running, write the file before they reply whatever the preferences are set to, and return its result or the error. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the preferences. A `wait` that is not `true`, `false` or `null` is refused with "`wait` must be true or false", and nothing is 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 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`). The dialog has the format presets menu (☰). In the dialog, `exportPreset` names the preset applied last (`ui.dialog.set` of it applies the preset it names, as picking it does; a name that is not a preset's is an error and leaves the dialog as it was), `__exportPresets` lists the presets, and `__savePreset` holds the name being typed after Save Preset…. The panel menu applies presets through `assets.settings.set {exportPreset}` and holds Save As Preset…, Save Changes, Delete Preset, Import Preset… and Export Preset…. Import and Export use picked files. | +| 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`). The dialog has the format presets menu (☰). In the dialog, `exportPreset` names the preset applied last (`ui.dialog.set` of it applies the preset it names, as picking it does; a name that is not a preset's is an error and leaves the dialog as it was), `__exportPresets` lists the presets, and `__savePreset` holds the name being typed after Save Preset…. The panel menu applies presets through `assets.settings.set {exportPreset}` and holds Save As Preset…, Save Changes, Delete Preset, Import Preset… and Export Preset…. Import and Export use picked files. Its Create Sub-folders and Open Location after Export run `assets.settings.set {subfolders}` and `{openLocation}`; its Format Settings… opens the dialog on the Assets tab with `__settings` set to the first row's format. | | `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` with params still exports the selection in one go, without the dialog; with no params or `{}` the save panel picks the file and its format's options dialog follows, as after Export As, and the call returns `null` while the dialog is open (OK returns what `file.exportAs` returns, without `format` or `bounds`). `file.exportAs {path, exportSelection: true, …options}` writes the selection without a 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 `warnings` as Export As shows an export's notes | diff --git a/docs/mcp.md b/docs/mcp.md index 37ffd74300..3074e295ef 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -7995,7 +7995,12 @@ moving the art moves the crop, and deleting it removes it from its asset (an ass format row of the document's export settings, the ones Export for Screens remembers (`document.exportSettings`); any `formats`, `preset`, `settings`, `prefix` or `subfolders` given win for that export. `assets.settings.set` changes those shared settings (not an undo step); its `subfolders: true` sets Export for Screens' Folders to a folder per row, -and `subfolders: false` sets a folder per row back to All in One Folder. `document.exportForScreens {assets: [id…]}` does the same with its own params +and `subfolders: false` sets a folder per row (without `exportPreset`, also a folder per format or item) back to All +in One Folder. Its `openLocation: true|false` +is Open Location after Export, kept in `document.exportSettings` (not in presets). The panel menu's Create Sub-folders and +Open Location after Export run these two (sub-folders sort the pictures; `assets.export3D` writes mesh files in the +folder itself), and its Format Settings… opens Export for Screens on the first row's format +settings. `document.exportForScreens {assets: [id…]}` does the same with its own params (the dialog's Assets tab). An asset's crop, and the crop of `export` with `selection: true`, is the bounds the renderer measures for what the art draws, its effects included. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 9ba8866a8c..867baeff81 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -799,6 +799,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Batch… over data sets offers None, Save and Close and Folder as the destination, as for a folder source. Save and Close saves the open document over its own file after each data set, so the file keeps only the last data set, unless a recorded Save step that Override Save points into the destination folder first makes each data set's file the document's own. A recorded Save, Save a Copy, Save as Template or export step that writes a file and that the batch does not point into the destination folder (destination None, Override Save or Override Export off, or a copy, template, PDF, EPS, DXF, Save for Web or Save for Office Documents step) also writes the same file for every data set. In either case the dialog shows a warning and `action.batch` returns a `warning`. Unless a recorded Save or Save As step writes a file and makes it the document's own first, `action.batch` refuses Save and Close over the data sets of a document that was never saved, was converted from an older version or was restored by Data Recovery. - Export for Screens: the dialog's Example line shows the path of the file the export writes first, sub-folder included, named as the export names it, for the first chosen artboard, asset, layer or object (`document.exportForScreens`). - Export presets: the Asset Export panel menu lists the export presets of Export for Screens, with Import Preset… and Export Preset…, which read and write `.vcexportpresets` files (`document.exportPresets.import`, `document.exportPresets.export`). An imported preset whose name is taken gets a number. In the panel, a preset applies to the shared export settings (`assets.settings.set {exportPreset}`), and Save Preset… saves them. With Create Sub-folders off, the panel's Export writes every file into the folder itself, mobile and density rows included. + - Asset Export panel menu: Create Sub-folders (on by default) and Open Location after Export are check items that set the dialog's two checkboxes (`assets.settings.set {subfolders, openLocation}`), and the panel's Export follows them, including a folder per format or item that Export for Screens remembers (3D assets' mesh files go in the folder itself); Format Settings… opens Export for Screens on the first row's format settings ([Asset Export panel](https://helpx.adobe.com/illustrator/desktop/save-and-export/export-files-to-different-formats/collect-and-export-assets-from-asset-export-panel.html)). Tests: `panels::tests_asset_export`. - Export for Screens is laid out as the reference app's dialog: on the left the Artboards and Assets tabs (thumbnails with their checkboxes, number and name, in a grid or a list), Clear Selection and Prefix; on the right Select (All, Range, Include Bleed, Include Artboard Fill, Full Document), Export to, Open Location after Export, Create Sub-folders by Scale or by Format (on by default), Export PDFs As (Single File writes the chosen artboards as the pages of one PDF named after the document, `pdfs: "single"`), and the format rows with the iOS and Android buttons, the format presets (☰) and Format Settings; under them, how many items are selected and how many files the export writes. File name patterns, the Layers and Selection sources and per-item folders stay available through `document.exportForScreens` (scripts and MCP). Tests: `dialogs::tests_screens` (layout, iOS/Android, Clear Selection, sub-folders, Single File) and `fileio::tests_screens::single_file_pdfs_hold_every_chosen_artboard`. - Save for Web: Preview in Browser writes each preview's page and images to a folder of its own inside `VectorCraft Save for Web` in the system's temporary folder. A later preview, from the same app or another one running, does not overwrite an earlier one. Each preview first removes the preview folders there that are an hour old or older and, on Linux, macOS and FreeBSD, belong to the current user; it leaves other files and folders alone. The folders a preview makes there are open to their owner only on those systems. When `VectorCraft Save for Web` is a link or is not a folder, or on those systems belongs to another user or can be written by other users, the preview goes to a new folder of its own in the temporary folder, which a later preview removes once it is an hour old (`file.saveForWeb.browser`). - Export for Screens, Save for Web (slice images, and files named after the document's title) and the Libraries panel name files with the characters Windows does not allow written as `-` and with a `-` after a Windows device name such as `CON`, on every platform (`document.exportForScreens`, `document.exportForWeb`, `library.create`). From 74dc87f9224d1a855b75756f2af90ba4bb2ef640 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 12:02:10 -0800 Subject: [PATCH 2/2] Asset Export: panel export tests use temporary folders removed on drop The four panel tests that export into a folder made it under the system temporary folder and removed it at the end, so a failing assertion left the files behind. They now use vectorcraft_testkit::TempDir, which removes the folder when the test ends or panics. --- crates/ui-egui/src/panels/tests_asset_export.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/panels/tests_asset_export.rs b/crates/ui-egui/src/panels/tests_asset_export.rs index 6383febda4..8b9a9ed884 100644 --- a/crates/ui-egui/src/panels/tests_asset_export.rs +++ b/crates/ui-egui/src/panels/tests_asset_export.rs @@ -514,8 +514,7 @@ fn the_preset_menu_saves_changes_and_opens_import_and_export() { #[cfg(not(target_arch = "wasm32"))] #[test] fn export_writes_into_the_picked_folder() { - let dir = std::env::temp_dir().join(format!("vc-asset-panel-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); + let dir = vectorcraft_testkit::TempDir::new("asset-panel"); let folder = dir.to_string_lossy().replace('\\', "/"); let (mut app, _, [a, _]) = app(false); let picked = folder.clone(); @@ -528,7 +527,6 @@ fn export_writes_into_the_picked_folder() { frame(&ctx, &mut app, click(at(&texts, "Export")), false); // Create Sub-folders is on by default: the 1x row's folder. assert!(dir.join("1x").join("Badge.png").is_file(), "{:?}", std::fs::read_dir(&dir).map(|d| d.count())); - let _ = std::fs::remove_dir_all(&dir); } /// The panel menu's Create Sub-folders and Open Location after Export are check items, on by @@ -537,8 +535,7 @@ fn export_writes_into_the_picked_folder() { #[cfg(not(target_arch = "wasm32"))] #[test] fn the_menu_sets_create_subfolders_and_open_location_and_export_follows_them() { - let dir = std::env::temp_dir().join(format!("vc-asset-panel-menu-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); + let dir = vectorcraft_testkit::TempDir::new("asset-panel-menu"); let folder = dir.to_string_lossy().replace('\\', "/"); let (mut app, _, [a, _]) = app(false); let picked = folder.clone(); @@ -593,7 +590,6 @@ fn the_menu_sets_create_subfolders_and_open_location_and_export_follows_them() { for bad in [json!({"openLocation": "yes"}), json!({"subfolders": 1})] { assert!(app.run("assets.settings.set", bad.clone()).is_err(), "{bad}"); } - let _ = std::fs::remove_dir_all(&dir); } /// A folder per format, remembered by Export for Screens or a preset, is used by the panel's Export @@ -601,8 +597,7 @@ fn the_menu_sets_create_subfolders_and_open_location_and_export_follows_them() { #[cfg(not(target_arch = "wasm32"))] #[test] fn export_uses_a_remembered_folder_per_format() { - let dir = std::env::temp_dir().join(format!("vc-asset-panel-format-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); + let dir = vectorcraft_testkit::TempDir::new("asset-panel-format"); let folder = dir.to_string_lossy().replace('\\', "/"); let (mut app, _, [a, _]) = app(false); app.services.pick_folder = Some(Box::new(move || Some(folder.clone()))); @@ -619,7 +614,6 @@ fn export_uses_a_remembered_folder_per_format() { let texts = frame(&ctx, &mut app, vec![], false); frame(&ctx, &mut app, click(at(&texts, "Export")), false); assert!(dir.join("png").join("Badge.png").is_file() && dir.join("svg").join("Badge.svg").is_file()); - let _ = std::fs::remove_dir_all(&dir); } /// Remembered empty folders are none, as an export reads them: Create Sub-folders is off in the @@ -892,8 +886,7 @@ fn tiff_panel_preserves_shared_antialias_and_explicit_overrides() { #[cfg(not(target_arch = "wasm32"))] #[test] fn tiff_saved_aliases_use_document_defaults_and_explicit_rgb() { - let dir = std::env::temp_dir().join(format!("vc-asset-tiff-aliases-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); + let dir = vectorcraft_testkit::TempDir::new("asset-tiff-aliases"); // The first case is the historical control: saved tif without an explicit model must be CMYK. // Native picked folders let the mixed-row case inspect each file without introducing a ZIP decoder. for (case, (alias, rgb_override, mixed)) in @@ -940,7 +933,6 @@ fn tiff_saved_aliases_use_document_defaults_and_explicit_rgb() { "direct export preserves saved aliases and explicit options" ); } - let _ = std::fs::remove_dir_all(&dir); } /// Export 3D Object in the panel menu collects the selected 3D object; its tile is marked 3D, the