Merge pull request #7657 from dotCooCoo/feat/asset-export-panel-menu-options

Asset Export: Create Sub-folders, Open Location after Export and Format Settings in the panel menu
This commit is contained in:
BFlat
2026-11-15 04:23:01 +07:00
committed by GitHub
37 changed files with 350 additions and 32 deletions
+20 -4
View File
@@ -89,7 +89,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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<Value> {
fn settings_set(s: &mut Session, p: &Value) -> Result<Value> {
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);
+36
View File
@@ -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.
+1 -1
View File
@@ -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};
@@ -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<Format
widgets::dim_label(ui, tl!("Formats:"));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if widgets::icon_button(ui, "settings", tl!("Format Settings"), false, 24.0).clicked() {
// Preserve row spellings; format settings use canonical IDs.
let first = 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));
d.fields.insert("__settings".into(), json!(first.unwrap_or("png")));
d.fields.insert("__settings".into(), json!(settings_format(&rows)));
}
if let Some(menu) = menu.as_mut() {
menu(ui, d);
@@ -1072,6 +1066,17 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog, mut menu: Option<Format
}
}
/// The format Format Settings opens on for format rows `rows`: the first row's format when it has
/// settings of its own, else PNG. Row spellings (`jpeg`, `tif`) give the canonical id.
pub(crate) fn settings_format(rows: &[Value]) -> &'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());
+1
View File
@@ -1287,6 +1287,7 @@ font-technology Variable Променливи
Format Формат
Format Options Опции за формат
Format Settings Настройки на формата
Format Settings… Настройки на формата…
Format: Формат:
Formats: Формати:
Fractional Widths Дробни ширини
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2503,6 +2503,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1797,6 +1797,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2536,6 +2536,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
Can't render this file because it is too large.
+1
View File
@@ -2329,6 +2329,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1332,6 +1332,7 @@ file-filter Templates Vorlagen
Format Format
Format Options Formatoptionen
Format Settings Formateinstellungen
Format Settings… Formateinstellungen…
Format: Format:
Formats: Formate:
Fractional Widths Gebrochene Breiten
Can't render this file because it is too large.
+1
View File
@@ -1265,6 +1265,7 @@ font-technology Variable Μεταβλητές
Format Μορφή
Format Options Επιλογές μορφής
Format Settings Ρυθμίσεις μορφής
Format Settings… Ρυθμίσεις μορφής…
Format: Μορφή:
Formats: Μορφές:
Fractional Widths Κλασματικά πλάτη
Can't render this file because it is too large.
+1
View File
@@ -1341,6 +1341,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2491,6 +2491,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1335,6 +1335,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
Can't render this file because it is too large.
+1
View File
@@ -1457,6 +1457,7 @@
Format Options ફોર્મેટ વિકલ્પો
Format Presets ફોર્મેટ પ્રીસેટ્સ
Format Settings ફોર્મેટ સેટિંગ્સ
Format Settings… ફોર્મેટ સેટિંગ્સ…
Format: ફોર્મેટ:
Formats: ફોર્મેટ્સ:
Fractional Widths અપૂર્ણાંક પહોળાઈ
Can't render this file because it contains an unexpected character in line 72 and column 5.
+1
View File
@@ -1993,6 +1993,7 @@ antiAlias Sharp शार्प
Format Options फ़ॉर्मेट ऑप्शन्स
Format Presets फ़ॉर्मैट प्रीसेट
Format Settings फ़ॉर्मैट सेटिंग
Format Settings… फ़ॉर्मैट सेटिंग…
Format: फ़ॉर्मेट:
Formats: फ़ॉर्मैट:
Fractional Widths आंशिक चौड़ाई
Can't render this file because it is too large.
+1
View File
@@ -1666,6 +1666,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
Can't render this file because it is too large.
+1
View File
@@ -2329,6 +2329,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
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2329
2330
2331
2332
2333
2334
2335
+1
View File
@@ -1362,6 +1362,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
Can't render this file because it is too large.
+1
View File
@@ -1837,6 +1837,7 @@ antiAlias Sharp シャープ
Format 形式
Format Options 形式オプション
Format Settings 形式の設定
Format Settings… 形式の設定…
Format: 形式:
Formats: 形式:
Fractions 分数
Can't render this file because it is too large.
+1
View File
@@ -2255,6 +2255,7 @@ digits Malayalam მალაიალამური
Export Asset რესურსის ექსპორტირება
Export to: ექსპორტის ადგილი:
Format Settings ფორმატის პარამეტრები
Format Settings… ფორმატის პარამეტრები…
Formats: ფორმატები:
Full Document მთელი დოკუმენტი
Include Bleed ჭრისპირა არის ჩართვა
Can't render this file because it is too large.
+1
View File
@@ -1827,6 +1827,7 @@ antiAlias Sharp 날카롭게
Format 형식
Format Options 형식 옵션
Format Settings 형식 설정
Format Settings… 형식 설정…
Format: 형식:
Formats: 형식:
Fractions 분수
Can't render this file because it is too large.
+1
View File
@@ -2847,6 +2847,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
Can't render this file because it is too large.
+1
View File
@@ -2451,6 +2451,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
Can't render this file because it is too large.
+1
View File
@@ -1203,6 +1203,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
Can't render this file because it is too large.
+1
View File
@@ -1351,6 +1351,7 @@ file-filter Templates Шаблоны
Format Формат
Format Options Параметры формата
Format Settings Параметры формата
Format Settings… Параметры формата…
Format: Формат:
Formats: Форматы:
Fractional Widths Дробные ширины
Can't render this file because it is too large.
+1
View File
@@ -1300,6 +1300,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
Can't render this file because it is too large.
+1
View File
@@ -1293,6 +1293,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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1356,6 +1356,7 @@ file-filter Templates Шаблони
Format Формат
Format Options Параметри формату
Format Settings Параметри формату
Format Settings… Параметри формату…
Format: Формат:
Formats: Формати:
Fractional Widths Дробові ширини
Can't render this file because it is too large.
+1
View File
@@ -1576,6 +1576,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ố
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -1292,6 +1292,7 @@ file-filter Templates 模板
Format 格式
Format Options 格式选项
Format Settings 格式设置
Format Settings… 格式设置…
Format: 格式:
Formats: 格式:
Fractional Widths 比例宽度
Can't render this file because it is too large.
+1
View File
@@ -1327,6 +1327,7 @@ file-filter Templates 範本
Format 格式
Format Options 格式選項
Format Settings 格式設定
Format Settings… 格式設定…
Format: 格式:
Formats: 格式:
Fractional Widths 小數字寬
Can't render this file because it is too large.
+69 -10
View File
@@ -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<u64>, files_each: usize) {
let (three_d, ids): (Vec<u64>, Vec<u64>) = 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<u64>, 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<u64>, 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<String, Value>) -> 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<String, Value>, 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<String, Value>) -> 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<Settings>, 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<u64> = 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() };
+174 -6
View File
@@ -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();
@@ -526,8 +525,179 @@ 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()));
}
/// 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 = vectorcraft_testkit::TempDir::new("asset-panel-menu");
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::<String>::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}");
}
}
/// 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 = 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())));
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());
}
/// 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]
@@ -716,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
@@ -764,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
+1 -1
View File
@@ -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 |
+6 -1
View File
@@ -8082,7 +8082,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.
+1
View File
@@ -802,6 +802,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`).