mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Merge pull request #4895 from storytold/feat/export-for-screens-parity
Export for Screens: the dialog works as Illustrator's
This commit is contained in:
@@ -54,7 +54,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export for Screens",
|
||||
["File", "Export"],
|
||||
None,
|
||||
"{folder?, zip?: false (one .zip of every file, each deflated or stored, whichever is smaller: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with <filename> for the document, <name> for the artboard, asset, layer or object, <index> from 1, #### for the counter zero-padded to the run of #, <format>, <scale>, <suffix> (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, <date>, <time>, <YYYY>, <MM>, <DD>, <hh>, <mm> and <ss>; a trailing .ext is dropped, as in `COMPANY-Logo-<filename>-####-YYYYMMDD`; different files it names alike get -2, -3…), counterStart?: 1, counterPer?: item (every file of an item has its number)|file, date?: YYYY-MM-DD (the date the pattern uses; default today, UTC), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N) → {files: [path…]}; no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png); every reply has warnings: [the encoders' warnings for the files, each once: an image that couldn't be decoded, type written in the fallback font…]. eps, ai and vectorcraft rows write a document of the item alone (its one artboard). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step)",
|
||||
"{folder?, zip?: false (one .zip of every file, each deflated or stored, whichever is smaller: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), pdfs?: multiple|single (Export PDFs As: with several artboards, single writes them as the pages of one PDF per PDF row, named after the document), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with <filename> for the document, <name> for the artboard, asset, layer or object, <index> from 1, #### for the counter zero-padded to the run of #, <format>, <scale>, <suffix> (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, <date>, <time>, <YYYY>, <MM>, <DD>, <hh>, <mm> and <ss>; a trailing .ext is dropped, as in `COMPANY-Logo-<filename>-####-YYYYMMDD`; different files it names alike get -2, -3…), counterStart?: 1, counterPer?: item (every file of an item has its number)|file, date?: YYYY-MM-DD (the date the pattern uses; default today, UTC), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N) → {files: [path…]}; no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png); every reply has warnings: [the encoders' warnings for the files, each once: an image that couldn't be decoded, type written in the fallback font…]. eps, ai and vectorcraft rows write a document of the item alone (its one artboard). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step)",
|
||||
has_doc,
|
||||
screens::export_for_screens
|
||||
),
|
||||
|
||||
@@ -309,6 +309,9 @@ struct ScreenParams {
|
||||
/// Where files go: `none` (all in the folder), `rows` (the rows' sub-folders, as
|
||||
/// `subfolders`), `format` (a folder per extension), `item`, or `itemFormat`.
|
||||
folders: Option<String>,
|
||||
/// Export PDFs As: `single` (the artboards a PDF row writes are the pages of one file) or
|
||||
/// `multiple` (a file per artboard, the default).
|
||||
pdfs: Option<String>,
|
||||
}
|
||||
|
||||
/// How exported files are sorted into folders.
|
||||
@@ -389,6 +392,8 @@ struct Naming {
|
||||
/// The counter counts files rather than items.
|
||||
per_file: bool,
|
||||
folders: Folders,
|
||||
/// Export PDFs As › Single file.
|
||||
single_pdf: bool,
|
||||
}
|
||||
|
||||
impl Naming {
|
||||
@@ -406,9 +411,14 @@ impl Naming {
|
||||
Some("file") => true,
|
||||
Some(c) => return Err(bad(C, format!("counterPer `{c}`: item or file"))),
|
||||
};
|
||||
let single_pdf = match o.pdfs.as_deref() {
|
||||
None | Some("multiple") => false,
|
||||
Some("single") => true,
|
||||
Some(m) => return Err(bad(C, format!("pdfs `{m}`: single or multiple"))),
|
||||
};
|
||||
let filename = safe_name(&super::file_stem(title)).unwrap_or_else(|| "Untitled".into());
|
||||
let pattern = o.name.clone().filter(|n| !n.trim().is_empty());
|
||||
Ok(Self { pattern, prefix, filename, when, counter_start: o.counter_start.unwrap_or(1), per_file, folders })
|
||||
Ok(Self { pattern, prefix, filename, when, counter_start: o.counter_start.unwrap_or(1), per_file, folders, single_pdf })
|
||||
}
|
||||
|
||||
/// The sub-folder (`None` for the export folder itself) and the file name of the file row `r`
|
||||
@@ -781,15 +791,34 @@ fn encode_and_write(export: ScreenExport, writer: &mut dyn FnMut(&str, &[u8]) ->
|
||||
// One step per item and row, the files a name collision skips included.
|
||||
let steps = (items.len() * rows.len()) as u64;
|
||||
let mut step = 0u64;
|
||||
// Export PDFs As › Single file: with several artboards, a PDF row writes them all as the pages
|
||||
// of one file, named as the document, with the first of them.
|
||||
let artboards: Option<Vec<usize>> = items.iter().map(|i| i.artboard).collect::<Option<Vec<_>>>().filter(|a| naming.single_pdf && a.len() > 1);
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
for r in &rows {
|
||||
vectorcraft_doc::cancel::check()?;
|
||||
vectorcraft_doc::cancel::progress(step, steps, &format!("Exporting file {} of {steps}", step + 1));
|
||||
step += 1;
|
||||
let (sub, file) = naming.file(item, index, files, r);
|
||||
let single = artboards.as_ref().filter(|_| r.format.id == "pdf");
|
||||
if single.is_some() && index > 0 {
|
||||
continue;
|
||||
}
|
||||
let document = single.map(|_| Item {
|
||||
name: naming.filename.clone(),
|
||||
stem: naming.filename.clone(),
|
||||
doc: item.doc.clone(),
|
||||
artboard: None,
|
||||
region: item.region,
|
||||
every_artboard: None,
|
||||
});
|
||||
let (sub, file) = naming.file(document.as_ref().unwrap_or(item), index, files, r);
|
||||
let name = sub.map_or_else(|| file.clone(), |d| format!("{d}/{file}"));
|
||||
let Some(name) = out.claim(&name, &format!("{}|{}", item.stem, r.key), o.name.is_some()) else { continue };
|
||||
let bytes = if let (Some(all), "pdf") = (&item.every_artboard, r.format.id) {
|
||||
let bytes = if let Some(boards) = single {
|
||||
let mut options = r.options.clone();
|
||||
options["range"] = json!(boards.iter().map(|b| (b + 1).to_string()).collect::<Vec<_>>().join(", "));
|
||||
write(&item.doc, "pdf", &options)?
|
||||
} else if let (Some(all), "pdf") = (&item.every_artboard, r.format.id) {
|
||||
// One page per artboard.
|
||||
let mut options = r.options.clone();
|
||||
options["range"] = json!("all");
|
||||
|
||||
@@ -811,3 +811,25 @@ fn the_document_remembers_its_export_settings() {
|
||||
assert!(!plain.contains("export_settings") && !plain.contains("exportSettings"), "not written when empty");
|
||||
assert!(vectorcraft_format::load(plain.as_bytes()).unwrap().export_settings.is_empty());
|
||||
}
|
||||
|
||||
/// Export PDFs As › Single file: with several artboards, a PDF row writes them as the pages of one
|
||||
/// file named after the document (raster rows still write a file per artboard); with one artboard
|
||||
/// the PDF is that artboard's, as Multiple files writes it.
|
||||
#[test]
|
||||
fn single_file_pdfs_hold_every_chosen_artboard() {
|
||||
let mut s = session(3);
|
||||
let title = s.doc().unwrap().doc.title.clone();
|
||||
let rows = json!([{"format": "png", "scale": "1x"}, {"format": "pdf"}]);
|
||||
let r = export(&mut s, json!({"range": "1-2", "pdfs": "single", "formats": rows}));
|
||||
let got = files(&r);
|
||||
let pdf: Vec<&(String, Vec<u8>)> = got.iter().filter(|(n, _)| n.ends_with(".pdf")).collect();
|
||||
assert_eq!(pdf.len(), 1, "{:?}", names(&r));
|
||||
assert_eq!(pdf[0].0, format!("{title}.pdf"));
|
||||
assert_eq!(vectorcraft_pdf::import(&pdf[0].1).unwrap().artboards.len(), 2, "one page per artboard");
|
||||
assert_eq!(got.iter().filter(|(n, _)| n.ends_with(".png")).count(), 2);
|
||||
let r = export(&mut s, json!({"range": "2", "pdfs": "single", "formats": [{"format": "pdf"}]}));
|
||||
assert_eq!(names(&r), ["Artboard-2.pdf"]);
|
||||
let r = export(&mut s, json!({"range": "1-2", "pdfs": "multiple", "formats": [{"format": "pdf"}]}));
|
||||
assert_eq!(names(&r), ["Artboard-1.pdf", "Artboard-2.pdf"]);
|
||||
assert!(s.execute("document.exportForScreens", &json!({"pdfs": "both", "formats": [{"format": "pdf"}]})).is_err());
|
||||
}
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
//! File → Export → Export for Screens: the Artboards tab (All, a range or Full Document, Include
|
||||
//! Bleed, thumbnails with checkboxes) and the Assets tab (the Asset Export panel's assets, with
|
||||
//! checkboxes), the destination (folder picker, Open Location after Export, Create Sub-folders;
|
||||
//! the web downloads instead), the format rows (scale as `2x`, `100w`, `100h` or `72ppi`, suffix,
|
||||
//! format), the presets, Format Settings (the gear) and the prefix. It opens on the settings the
|
||||
//! document last exported with, except that while the Artboard tool is in use the artboards it has
|
||||
//! selected are the ones chosen; the format rows are the Asset Export panel's too ([`formats`]).
|
||||
//! File → Export → Export for Screens, laid out as Illustrator's: on the left the Artboards and
|
||||
//! Assets tabs (thumbnails with checkboxes, in a grid or a list), Clear Selection and the prefix;
|
||||
//! on the right Select (All, a range or Full Document, Include Bleed), the destination (folder
|
||||
//! picker, Open Location after Export, Create Sub-folders by scale or by format; the web downloads
|
||||
//! instead), Export PDFs As (one file of every artboard or a file each), and the format rows (scale
|
||||
//! as `2x`, `100w`, `100h` or `72ppi`, suffix, format) with the iOS and Android rows, the format
|
||||
//! presets (the ☰ menu) and Format Settings (the gear); below, how many items are selected and
|
||||
//! how many files the export writes. It opens on the settings the document last exported with,
|
||||
//! except that while the Artboard tool is in use the artboards it has selected are the ones chosen;
|
||||
//! the format rows are the Asset Export panel's too ([`formats`]). On the desktop with Background
|
||||
//! Export on, the files are encoded and written on the background thread
|
||||
//! ([`crate::background`]) after the dialog closes.
|
||||
//!
|
||||
//! The Layers tab exports each checked layer's art alone, and the Selection tab the selected art as
|
||||
//! one file or a file per object. A file name pattern (`name`: `<filename>`, `<name>`, `####`,
|
||||
//! `YYYYMMDD`…, see `document.exportForScreens`) names every file alike, with a live example, and
|
||||
//! the files go into one folder, or a folder per row, format, item, or item and format
|
||||
//! (`folders`). Export presets (built-in and saved) set the formats, naming and folders at once.
|
||||
//! On the desktop with Background Export on, the files are encoded and written on the background
|
||||
//! thread ([`crate::background`]) after the dialog closes.
|
||||
//! `document.exportForScreens` does more than the dialog shows (layers, the selection, file name
|
||||
//! patterns, a folder per item): those are for scripts and MCP.
|
||||
//!
|
||||
//! Fields (`ui.dialog.set`): `tab` (artboards|assets|layers|selection), `select` (all|range|full), `range`,
|
||||
//! Fields (`ui.dialog.set`): `tab` (artboards|assets), `select` (all|range|full), `range`,
|
||||
//! `boards` ([bool] per artboard), `assets` ([asset id] checked on the Assets tab), `includeBleed`,
|
||||
//! `folder`, `openLocation`, `subfolders`, `prefix`, `preset` (""|mobile|density: where the rows
|
||||
//! came from), `formats` (rows of `document.exportForScreens`, `scale` as text), `settings`
|
||||
//! ({png|png8|jpg|webp|gif|tiff|svg|pdf: {…}}), `layers` ([layer id] checked on the Layers tab),
|
||||
//! `selectionMode` (one|each), `name` (the pattern), `counterStart`, `folders`
|
||||
//! (none|rows|format|item|itemFormat), `exportPreset` (the preset applied last, "" for none);
|
||||
//! `folder`, `openLocation`, `subfolders`, `subfolderBy` (scale|format), `pdfs` (single|multiple),
|
||||
//! `prefix`, `preset` (""|mobile|density: where the rows came from), `formats` (rows of
|
||||
//! `document.exportForScreens`, `scale` as text), `settings` ({png|png8|jpg|webp|gif|tiff|svg|pdf:
|
||||
//! {…}}), `view` (grid|list), `exportPreset` (the format preset applied last, "" for none);
|
||||
//! `__settings` names the format whose Format Settings show ("" when closed), `__savePreset` is the
|
||||
//! name being typed to save a preset (absent when not saving).
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_engine::cmd::fileio::{self, ArtboardPick, SCREEN_PRESETS, ScreenSize, pdf};
|
||||
|
||||
use vectorcraft_doc::{ExportAsset, NodeId};
|
||||
use vectorcraft_engine::cmd::fileio::{self, ArtboardPick, ScreenSize, pdf};
|
||||
|
||||
use super::{DialogSpec, form, png_options, svg_options};
|
||||
use crate::state::Dialog;
|
||||
@@ -43,11 +40,9 @@ pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
ok_label: Some(|app| match (io::is_web(app), app.ui.dialog.as_ref().map(|d| d.str("tab")).as_deref()) {
|
||||
(true, _) => "Download",
|
||||
(false, Some("assets")) => "Export Asset",
|
||||
(false, Some("layers")) => "Export Layer",
|
||||
(false, Some("selection")) => "Export Selection",
|
||||
(false, _) => "Export Artboard",
|
||||
}),
|
||||
min_width: 660.0,
|
||||
min_width: 720.0,
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
@@ -80,32 +75,13 @@ fn print_marks() -> Value {
|
||||
json!({"trim": true, "registration": true, "colorBars": true, "pageInfo": true})
|
||||
}
|
||||
|
||||
/// Where the files go: (`folders` value, label).
|
||||
const FOLDERS: [(&str, &str); 5] = [
|
||||
("none", "All in One Folder"),
|
||||
("rows", "By Size or Row"),
|
||||
("format", "By Format"),
|
||||
("item", "By Artboard or Item"),
|
||||
("itemFormat", "By Item, Then Format"),
|
||||
];
|
||||
const FOLDER_LABELS: [&str; 5] = ["All in One Folder", "By Size or Row", "By Format", "By Artboard or Item", "By Item, Then Format"];
|
||||
|
||||
/// The tokens Insert adds to the file name pattern: (label, token).
|
||||
const TOKENS: [(&str, &str); 8] = [
|
||||
("Document Name", "<filename>"),
|
||||
("Artboard or Item Name", "<name>"),
|
||||
("Number (####)", "####"),
|
||||
("Date (YYYYMMDD)", "YYYYMMDD"),
|
||||
("Date (YYYY-MM-DD)", "YYYY-MM-DD"),
|
||||
("Time (hhmmss)", "<time>"),
|
||||
("Format", "<format>"),
|
||||
("Scale", "<scale>"),
|
||||
];
|
||||
const TOKEN_LABELS: [&str; 8] =
|
||||
["Document Name", "Artboard or Item Name", "Number (####)", "Date (YYYYMMDD)", "Date (YYYY-MM-DD)", "Time (hhmmss)", "Format", "Scale"];
|
||||
|
||||
/// The tabs: (`tab` value, label).
|
||||
const TABS: [&str; 4] = ["artboards", "assets", "layers", "selection"];
|
||||
const TABS: [&str; 2] = ["artboards", "assets"];
|
||||
|
||||
/// The left pane's width and the height of its thumbnails' area; the right pane's width.
|
||||
const LEFT_W: f32 = 300.0;
|
||||
const PANE_H: f32 = 360.0;
|
||||
const RIGHT_W: f32 = 370.0;
|
||||
|
||||
/// The formats Format Settings has options for: (format, label).
|
||||
const SETTINGS: [(&str, &str); 8] =
|
||||
@@ -144,8 +120,9 @@ pub(crate) fn open_assets(app: &mut VectorcraftApp, checked: Option<&[u64]>) {
|
||||
}
|
||||
|
||||
/// Open the dialog on the settings the active document last exported with (else one PNG row at
|
||||
/// 1x, into the Desktop), every asset checked. While the Artboard tool is in use, the artboards it
|
||||
/// has selected (the rows the Artboards panel highlights) are the ones chosen instead.
|
||||
/// 1x, into the Desktop, with Create Sub-folders on), every asset checked. While the Artboard tool
|
||||
/// is in use, the artboards it has selected (the rows the Artboards panel highlights) are the ones
|
||||
/// chosen instead.
|
||||
pub fn open(app: &mut VectorcraftApp) {
|
||||
let (n, saved, assets) = app
|
||||
.session
|
||||
@@ -172,12 +149,14 @@ pub fn open(app: &mut VectorcraftApp) {
|
||||
settings.insert(id.into(), Value::Object(saved_options(app, &saved, id)));
|
||||
}
|
||||
let folder = get("folder").and_then(Value::as_str).filter(|f| !f.is_empty()).map(str::to_string).or_else(fileio::export_folder);
|
||||
// Where files go: the saved choice, else a folder per row as Create Sub-folders did.
|
||||
let folders = get("folders")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|f| FOLDERS.iter().any(|(id, _)| id == f))
|
||||
.map_or_else(|| if flag("subfolders", preset.is_some()) { "rows" } else { "none" }.to_string(), str::to_string);
|
||||
let layers: Vec<u64> = app.session.active().map(|st| st.doc.layers.iter().map(|l| l.id.0).collect()).unwrap_or_default();
|
||||
// Create Sub-folders is on until an export turns it off; a folder per format is the saved
|
||||
// `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,
|
||||
Some(_) => true,
|
||||
None => flag("subfolders", saved.is_empty() || preset.is_some()),
|
||||
};
|
||||
let fields = json!({
|
||||
"tab": "artboards",
|
||||
"select": select,
|
||||
@@ -187,18 +166,16 @@ pub fn open(app: &mut VectorcraftApp) {
|
||||
"includeBleed": flag("includeBleed", false),
|
||||
"folder": folder.unwrap_or_default(),
|
||||
"openLocation": flag("openLocation", true),
|
||||
"subfolders": flag("subfolders", preset.is_some()),
|
||||
"subfolders": subfolders,
|
||||
"subfolderBy": if folders == Some("format") { "format" } else { "scale" },
|
||||
"pdfs": if get("pdfs").and_then(Value::as_str) == Some("single") { "single" } else { "multiple" },
|
||||
"prefix": get("prefix").and_then(Value::as_str).unwrap_or_default(),
|
||||
"preset": preset.unwrap_or_default(),
|
||||
"formats": rows,
|
||||
"settings": settings,
|
||||
"view": "grid",
|
||||
"__settings": "",
|
||||
"__presets": pdf::presets(&app.session),
|
||||
"layers": layers,
|
||||
"selectionMode": get("selection").and_then(Value::as_str).filter(|m| *m == "each").unwrap_or("one"),
|
||||
"name": get("name").and_then(Value::as_str).unwrap_or_default(),
|
||||
"counterStart": get("counterStart").and_then(Value::as_u64).unwrap_or(1),
|
||||
"folders": folders,
|
||||
"exportPreset": "",
|
||||
"__exportPresets": export_presets(app),
|
||||
});
|
||||
@@ -281,24 +258,6 @@ pub(super) fn export_presets(app: &mut VectorcraftApp) -> Value {
|
||||
app.run("document.exportPresets.list", json!({})).ok().and_then(|r| r.get("presets").cloned()).unwrap_or_else(|| json!([]))
|
||||
}
|
||||
|
||||
/// The art of layer `id` (sublayers unfolded), back to front: what its file holds.
|
||||
fn layer_art(doc: &vectorcraft_doc::Document, id: NodeId, out: &mut Vec<NodeId>) {
|
||||
let Some(n) = doc.node(id) else { return };
|
||||
if n.is_layer() {
|
||||
for c in n.children().into_iter().flatten() {
|
||||
layer_art(doc, c.id, out);
|
||||
}
|
||||
} else {
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// The checked layers that are still in the document, in stacking order.
|
||||
fn checked_layers(app: &VectorcraftApp, d: &Dialog) -> Vec<u64> {
|
||||
let on: Vec<u64> = d.fields.get("layers").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default();
|
||||
app.session.active().map(|st| st.doc.layers.iter().map(|l| l.id.0).filter(|id| on.contains(id)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The `document.exportForScreens` params the dialog describes (the web: no folder, one ZIP for
|
||||
/// several files).
|
||||
fn params(app: &VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
@@ -316,24 +275,24 @@ fn params(app: &VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
Ok(Value::Object(p))
|
||||
}
|
||||
|
||||
/// The counter's first value: the Start at field (a number, or text typed through `ui.dialog.set`).
|
||||
fn counter_start(d: &Dialog) -> u64 {
|
||||
d.fields.get("counterStart").and_then(|v| v.as_u64().or_else(|| v.as_str().and_then(|t| t.trim().parse().ok()))).unwrap_or(1)
|
||||
}
|
||||
|
||||
/// The params of [`params`] without the destination (what is exported, at which sizes, and how the
|
||||
/// files are named and sorted), and how many files that is.
|
||||
fn settings_params(app: &VectorcraftApp, d: &Dialog) -> Result<(Map<String, Value>, usize), String> {
|
||||
let (p, files, _) = selection_params(app, d)?;
|
||||
Ok((p, files))
|
||||
}
|
||||
|
||||
/// [`settings_params`] and how many artboards or assets are chosen.
|
||||
fn selection_params(app: &VectorcraftApp, d: &Dialog) -> Result<(Map<String, Value>, usize, usize), String> {
|
||||
let n = app.session.active().map_or(0, |st| st.doc.artboards.len());
|
||||
let rows = d.fields.get("formats").and_then(Value::as_array).cloned().unwrap_or_default();
|
||||
if rows.is_empty() {
|
||||
return Err("add a format to export".into());
|
||||
}
|
||||
let mut p = Map::new();
|
||||
// What each row writes a file of: the checked assets, layers, the selection, else the
|
||||
// artboards.
|
||||
let tab = d.str("tab");
|
||||
let pieces = if tab == "assets" {
|
||||
// What each row writes a file of: the checked assets, else the artboards.
|
||||
let artboards = d.str("tab") != "assets";
|
||||
let pieces = if !artboards {
|
||||
let checked = checked_assets(app, d);
|
||||
if checked.is_empty() {
|
||||
return Err("check an asset to export".into());
|
||||
@@ -341,22 +300,6 @@ fn settings_params(app: &VectorcraftApp, d: &Dialog) -> Result<(Map<String, Valu
|
||||
let count = checked.len();
|
||||
p.insert("assets".into(), json!(checked));
|
||||
count
|
||||
} else if tab == "layers" {
|
||||
let checked = checked_layers(app, d);
|
||||
if checked.is_empty() {
|
||||
return Err("check a layer to export".into());
|
||||
}
|
||||
let count = checked.len();
|
||||
p.insert("layers".into(), json!(checked));
|
||||
count
|
||||
} else if tab == "selection" {
|
||||
let selected = app.session.active().map_or(0, |st| st.selection.objects.len());
|
||||
if selected == 0 {
|
||||
return Err("select the art to export first".into());
|
||||
}
|
||||
let each = d.str("selectionMode") == "each";
|
||||
p.insert("selection".into(), json!(if each { "each" } else { "one" }));
|
||||
if each { selected } else { 1 }
|
||||
} else {
|
||||
match d.str("select").as_str() {
|
||||
"full" => {
|
||||
@@ -367,34 +310,33 @@ fn settings_params(app: &VectorcraftApp, d: &Dialog) -> Result<(Map<String, Valu
|
||||
let range = d.str("range");
|
||||
// A bad range keeps the dialog open.
|
||||
let chosen = ArtboardPick { range: Some(range.clone()), ..Default::default() }.resolve(n)?.unwrap_or_default();
|
||||
if chosen.is_empty() {
|
||||
return Err("check an artboard to export".into());
|
||||
}
|
||||
p.insert("range".into(), json!(range));
|
||||
chosen.len()
|
||||
}
|
||||
_ => n,
|
||||
}
|
||||
};
|
||||
let files = pieces * rows.len();
|
||||
// Export PDFs As › Single file: every chosen artboard in one PDF per PDF row.
|
||||
let single = artboards && pieces > 1 && d.str("select") != "full" && d.str("pdfs") == "single";
|
||||
let pdf_rows = rows.iter().filter(|r| r["format"].as_str().and_then(fileio::format).is_some_and(|f| f.id == "pdf")).count();
|
||||
let files = if single { (rows.len() - pdf_rows) * pieces + pdf_rows } else { rows.len() * pieces };
|
||||
p.insert("includeBleed".into(), json!(d.bool("includeBleed")));
|
||||
let folders = d.str("folders");
|
||||
let folders = match (FOLDERS.iter().any(|(id, _)| *id == folders), d.bool("subfolders")) {
|
||||
// Create Sub-folders (as the Asset Export panel and older scripts set it) is a folder per
|
||||
// row while Folders is still All in One Folder.
|
||||
(true, true) if folders == "none" => "rows".to_string(),
|
||||
(true, _) => folders,
|
||||
(false, sub) => if sub { "rows" } else { "none" }.to_string(),
|
||||
let folders = match (d.bool("subfolders"), d.str("subfolderBy").as_str()) {
|
||||
(false, _) => "none",
|
||||
(true, "format") => "format",
|
||||
(true, _) => "rows",
|
||||
};
|
||||
// Create Sub-folders, as the Asset Export panel (which shares these settings) still names it.
|
||||
// Create Sub-folders, as the Asset Export panel (which shares these settings) names it.
|
||||
p.insert("subfolders".into(), json!(folders == "rows"));
|
||||
p.insert("folders".into(), json!(folders));
|
||||
p.insert("pdfs".into(), json!(if single { "single" } else { "multiple" }));
|
||||
p.insert("prefix".into(), json!(d.str("prefix")));
|
||||
let name = d.str("name");
|
||||
if !name.trim().is_empty() {
|
||||
p.insert("name".into(), json!(name.trim()));
|
||||
}
|
||||
p.insert("counterStart".into(), json!(counter_start(d)));
|
||||
p.insert("formats".into(), Value::Array(rows));
|
||||
p.insert("settings".into(), d.fields.get("settings").cloned().unwrap_or(json!({})));
|
||||
Ok((p, files))
|
||||
Ok((p, files, pieces))
|
||||
}
|
||||
|
||||
/// Export; the dialog stays open when the export fails.
|
||||
@@ -410,130 +352,218 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let tab = TABS.iter().position(|k| *k == d.str("tab")).unwrap_or(0);
|
||||
if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets"), tl!("Layers"), tl!("Selection")], tab) {
|
||||
d.fields.insert("tab".into(), json!(TABS.get(i).copied().unwrap_or("artboards")));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(270.0);
|
||||
match tab {
|
||||
1 => assets(app, ui, d),
|
||||
2 => layers(app, ui, d),
|
||||
3 => selection(app, ui, d),
|
||||
_ => artboards(app, ui, d),
|
||||
ui.set_width(LEFT_W);
|
||||
if let Some(i) = widgets::tab_bar(ui, &[tl!("Artboards"), tl!("Assets")], tab) {
|
||||
d.fields.insert("tab".into(), json!(TABS.get(i).copied().unwrap_or("artboards")));
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
let list = d.str("view") == "list";
|
||||
let (pane, _) = ui.allocate_exact_size(egui::vec2(LEFT_W, PANE_H), egui::Sense::hover());
|
||||
ui.painter().rect_filled(pane, 2.0, t.panel_darker);
|
||||
ui.painter().rect_stroke(pane, 2.0, egui::Stroke::new(1.0, t.divider), egui::StrokeKind::Inside);
|
||||
let mut inner = ui.new_child(egui::UiBuilder::new().max_rect(pane.shrink(8.0)).layout(egui::Layout::top_down(egui::Align::Min)));
|
||||
match tab {
|
||||
1 => assets(app, &mut inner, d, list),
|
||||
_ => artboard_tiles(app, &mut inner, d, list),
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
for (view, icon, tip) in [("grid", "layout-grid", tl!("Grid View")), ("list", "dc-list-view", tl!("List View"))] {
|
||||
if widgets::icon_button(ui, icon, tip, d.str("view") == view || (view == "grid" && !list && d.str("view") != "list"), 22.0)
|
||||
.clicked()
|
||||
{
|
||||
d.fields.insert("view".into(), json!(view));
|
||||
}
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
if ui.small_button(tl!("Clear Selection")).clicked() {
|
||||
clear_selection(app, d, tab);
|
||||
}
|
||||
widgets::dim_label(ui, tl!("Prefix:"));
|
||||
form::text(ui, d, "prefix", (ui.available_width() - 4.0).max(60.0));
|
||||
});
|
||||
});
|
||||
let (sep, _) = ui.allocate_exact_size(egui::vec2(17.0, 380.0), egui::Sense::hover());
|
||||
let (sep, _) = ui.allocate_exact_size(egui::vec2(17.0, PANE_H + 60.0), egui::Sense::hover());
|
||||
ui.painter().line_segment([sep.center_top(), sep.center_bottom()], egui::Stroke::new(1.0, t.divider));
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(350.0);
|
||||
ui.set_width(RIGHT_W);
|
||||
let shown = d.str("__settings");
|
||||
if SETTINGS.iter().any(|(id, _)| *id == shown) {
|
||||
format_settings(ui, d, &shown);
|
||||
} else {
|
||||
presets(app, ui, d);
|
||||
ui.add_space(8.0);
|
||||
if tab == 0 {
|
||||
select_options(app, ui, d);
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
destination(app, ui, d);
|
||||
ui.add_space(6.0);
|
||||
pdf_mode(ui, d, tab == 0);
|
||||
ui.add_space(10.0);
|
||||
formats(ui, d);
|
||||
ui.add_space(8.0);
|
||||
naming(app, ui, d);
|
||||
formats(ui, d, Some(&mut |ui: &mut egui::Ui, d: &mut Dialog| preset_menu(app, ui, d)));
|
||||
save_preset_row(app, ui, d);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
let (picked, files) = counts(app, d);
|
||||
let status =
|
||||
crate::i18n::fmt(tl!("Selected: {n}, Total number of files to export: {m}"), &[("n", &picked.to_string()), ("m", &files.to_string())]);
|
||||
widgets::dim_label(ui, &status);
|
||||
false
|
||||
}
|
||||
|
||||
/// How many artboards or assets are chosen and how many files the export writes (0 while the
|
||||
/// settings choose nothing to export).
|
||||
fn counts(app: &VectorcraftApp, d: &Dialog) -> (usize, usize) {
|
||||
match selection_params(app, d) {
|
||||
Ok((_, files, pieces)) => (pieces, files),
|
||||
Err(_) => (if d.str("tab") == "assets" { checked_assets(app, d).len() } else { 0 }, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear Selection: no artboard (or, on the Assets tab, no asset) is checked.
|
||||
fn clear_selection(app: &VectorcraftApp, d: &mut Dialog, tab: usize) {
|
||||
if tab == 1 {
|
||||
d.fields.insert("assets".into(), json!([]));
|
||||
} else {
|
||||
let n = app.session.active().map_or(0, |st| st.doc.artboards.len());
|
||||
d.fields.insert("boards".into(), json!(vec![false; n]));
|
||||
set_select(d, "range");
|
||||
d.fields.insert("range".into(), json!(""));
|
||||
}
|
||||
}
|
||||
|
||||
fn set_select(d: &mut Dialog, select: &str) {
|
||||
d.fields.insert("select".into(), json!(select));
|
||||
}
|
||||
|
||||
/// Select (All, Range, Full Document), Include Bleed, and the artboards' thumbnails and checkboxes.
|
||||
fn artboards(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
/// The checked artboards, one per artboard (all checked when the field is short).
|
||||
fn boards(app: &VectorcraftApp, d: &Dialog) -> Vec<bool> {
|
||||
let n = app.session.active().map_or(0, |st| st.doc.artboards.len());
|
||||
let mut boards: Vec<bool> =
|
||||
d.fields.get("boards").and_then(Value::as_array).map(|a| a.iter().map(|b| b.as_bool().unwrap_or(false)).collect()).unwrap_or_default();
|
||||
boards.resize(n, true);
|
||||
let before = boards.clone();
|
||||
boards
|
||||
}
|
||||
|
||||
/// Select: All or a Range (which checks the artboards it names), Include Bleed and Full Document.
|
||||
fn select_options(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let n = app.session.active().map_or(0, |st| st.doc.artboards.len());
|
||||
let select = d.str("select");
|
||||
widgets::dim_label(ui, tl!("Select:"));
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::radio(ui, tl!("All"), select == "all", true) {
|
||||
set_select(d, "all");
|
||||
boards.iter_mut().for_each(|b| *b = true);
|
||||
d.fields.insert("boards".into(), json!(vec![true; n]));
|
||||
d.fields.insert("range".into(), json!(range_text(&vec![true; n])));
|
||||
}
|
||||
if widgets::radio(ui, tl!("Range:"), select == "range", true) {
|
||||
set_select(d, "range");
|
||||
}
|
||||
ui.add_enabled_ui(select == "range", |ui| {
|
||||
if form::text_named(ui, d, "range", tl!("Range:"), 90.0)
|
||||
if form::text_named(ui, d, "range", tl!("Range:"), 150.0)
|
||||
&& let Ok(Some(chosen)) = (ArtboardPick { range: Some(d.str("range")), ..Default::default() }).resolve(n)
|
||||
{
|
||||
boards = (0..n).map(|i| chosen.contains(&i)).collect();
|
||||
d.fields.insert("boards".into(), json!((0..n).map(|i| chosen.contains(&i)).collect::<Vec<_>>()));
|
||||
}
|
||||
});
|
||||
});
|
||||
if widgets::radio(ui, tl!("Full Document"), select == "full", true) {
|
||||
form::check(ui, d, "includeBleed", tl!("Include Bleed"));
|
||||
if widgets::radio(ui, tl!("Full Document"), d.str("select") == "full", true) {
|
||||
set_select(d, "full");
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
form::check(ui, d, "includeBleed", tl!("Include Bleed"));
|
||||
ui.add_space(6.0);
|
||||
let typed = boards != before;
|
||||
let checked = boards.clone();
|
||||
}
|
||||
|
||||
/// The artboards' thumbnails, each with its checkbox, number and name, in a grid or a list;
|
||||
/// checking them picks them as the range (all of them: All). Dimmed with Full Document.
|
||||
fn artboard_tiles(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, list: bool) {
|
||||
let mut boards = boards(app, d);
|
||||
let before = boards.clone();
|
||||
let names: Vec<String> = app.session.active().map(|s| s.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default();
|
||||
ui.add_enabled_ui(d.str("select") != "full", |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button(tl!("Select All")).clicked() {
|
||||
boards.iter_mut().for_each(|b| *b = true);
|
||||
tiles(ui, "efs-artboards", names.len(), list, |ui, i, r| {
|
||||
match artboard_thumb(app, ui.ctx(), i) {
|
||||
Some(tex) => {
|
||||
let sz = tex.size_vec2();
|
||||
let s = (r.width() / sz.x).min(r.height() / sz.y).min(1.0);
|
||||
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
|
||||
ui.painter().image(tex.id(), egui::Rect::from_center_size(r.center(), sz * s), uv, egui::Color32::WHITE);
|
||||
}
|
||||
None => {
|
||||
ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE);
|
||||
}
|
||||
}
|
||||
if ui.small_button(tl!("Clear")).clicked() {
|
||||
boards.iter_mut().for_each(|b| *b = false);
|
||||
}
|
||||
});
|
||||
let names: Vec<String> = app.session.active().map(|s| s.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default();
|
||||
egui::ScrollArea::vertical().max_height(250.0).show(ui, |ui| {
|
||||
for (i, name) in names.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(egui::vec2(46.0, 46.0), egui::Sense::hover());
|
||||
match artboard_thumb(app, ui.ctx(), i) {
|
||||
Some(tex) => {
|
||||
let sz = tex.size_vec2();
|
||||
let s = (46.0 / sz.x.max(sz.y)).min(1.0);
|
||||
let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
|
||||
ui.painter().image(tex.id(), egui::Rect::from_center_size(r.center(), sz * s), uv, egui::Color32::WHITE);
|
||||
}
|
||||
None => {
|
||||
ui.painter().rect_filled(r, 2.0, egui::Color32::WHITE);
|
||||
}
|
||||
}
|
||||
if let Some(b) = boards.get_mut(i)
|
||||
&& crate::widgets::check_name(ui, name, *b)
|
||||
{
|
||||
*b ^= true;
|
||||
}
|
||||
});
|
||||
let name = names.get(i).cloned().unwrap_or_default();
|
||||
(format!("{} {name}", i + 1), name, boards.get(i).copied().unwrap_or(false))
|
||||
})
|
||||
.into_iter()
|
||||
.for_each(|i| {
|
||||
if let Some(b) = boards.get_mut(i) {
|
||||
*b ^= true;
|
||||
}
|
||||
});
|
||||
});
|
||||
// Checking artboards picks them as the range (all of them: All); a typed range keeps its text.
|
||||
if boards != checked {
|
||||
if boards != before {
|
||||
set_select(d, if boards.iter().all(|b| *b) { "all" } else { "range" });
|
||||
d.fields.insert("range".into(), json!(range_text(&boards)));
|
||||
}
|
||||
if typed || boards != checked {
|
||||
d.fields.insert("boards".into(), json!(boards));
|
||||
}
|
||||
}
|
||||
|
||||
/// `count` thumbnails in a grid (or, with `list`, a list) that scrolls: `paint` draws thumbnail `i`
|
||||
/// into its rect and gives its caption, its checkbox's name and whether it is checked → the
|
||||
/// thumbnails whose checkbox (or picture) was clicked.
|
||||
fn tiles(
|
||||
ui: &mut egui::Ui,
|
||||
id: &str,
|
||||
count: usize,
|
||||
list: bool,
|
||||
mut paint: impl FnMut(&mut egui::Ui, usize, egui::Rect) -> (String, String, bool),
|
||||
) -> Vec<usize> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut toggled = vec![];
|
||||
let side = if list { 40.0 } else { 84.0 };
|
||||
let width = ui.available_width();
|
||||
let columns = if list { 1 } else { ((width + 10.0) / (side + 10.0)).floor().max(1.0) as usize };
|
||||
egui::ScrollArea::vertical().id_salt(id).auto_shrink([false, false]).show(ui, |ui| {
|
||||
for start in (0..count).step_by(columns) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 10.0;
|
||||
for i in start..(start + columns).min(count) {
|
||||
let cell = if list { egui::vec2(width - 16.0, side) } else { egui::vec2(side, side + 18.0) };
|
||||
let (r, resp) = ui.allocate_exact_size(cell, egui::Sense::click());
|
||||
let pic = egui::Rect::from_min_size(r.min, egui::vec2(side, side));
|
||||
let (caption, name, checked) = paint(ui, i, pic.shrink(3.0));
|
||||
let stroke = if checked { egui::Stroke::new(2.0, t.accent) } else { egui::Stroke::new(1.0, t.divider) };
|
||||
ui.painter().rect_stroke(pic, 2.0, stroke, egui::StrokeKind::Inside);
|
||||
let label_at = if list { egui::pos2(pic.right() + 8.0, pic.center().y) } else { egui::pos2(pic.left(), pic.bottom() + 9.0) };
|
||||
let galley =
|
||||
ui.painter().layout(caption, egui::FontId::proportional(11.0), t.text, if list { cell.x - side - 12.0 } else { side });
|
||||
ui.painter().galley(egui::pos2(label_at.x, label_at.y - galley.size().y / 2.0), galley, t.text);
|
||||
// The checkbox sits on the thumbnail's lower left corner, as Illustrator's.
|
||||
let bx = egui::Rect::from_min_size(egui::pos2(pic.left() + 4.0, pic.bottom() - 18.0), egui::vec2(14.0, 14.0));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(bx));
|
||||
if widgets::check_box(&mut child, &name, checked, true) || resp.clicked() {
|
||||
toggled.push(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
});
|
||||
toggled
|
||||
}
|
||||
|
||||
/// The checked assets that are still in the document, in panel order.
|
||||
fn checked_assets(app: &VectorcraftApp, d: &Dialog) -> Vec<u64> {
|
||||
let on: Vec<u64> = d.fields.get("assets").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default();
|
||||
app.session.active().map(|st| st.doc.assets.iter().map(|a| a.id).filter(|id| on.contains(id)).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The Assets tab: each asset's thumbnail and checkbox, Select All and Clear.
|
||||
fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
/// The Assets tab: each asset's thumbnail with its checkbox and name, in a grid or a list.
|
||||
fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, list: bool) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
if st.doc.assets.is_empty() {
|
||||
ui.add_space(24.0);
|
||||
@@ -542,39 +572,30 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
let mut on = checked_assets(app, d);
|
||||
let before = on.clone();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button(tl!("Select All")).clicked() {
|
||||
on = st.doc.assets.iter().map(|a| a.id).collect();
|
||||
}
|
||||
if ui.small_button(tl!("Clear")).clicked() {
|
||||
on.clear();
|
||||
}
|
||||
});
|
||||
// Asked for until the row is in view: the dialog's first frame only measures it.
|
||||
// Asked for until its row is in view: the dialog's first frame only measures it.
|
||||
let scroll_to = d.fields.get("__scrollTo").and_then(Value::as_u64).filter(|id| st.doc.assets.iter().any(|a| a.id == *id));
|
||||
let mut in_view = scroll_to.is_none();
|
||||
egui::ScrollArea::vertical().max_height(330.0).show(ui, |ui| {
|
||||
for a in &st.doc.assets {
|
||||
let row = ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(egui::vec2(46.0, 46.0), egui::Sense::hover());
|
||||
crate::panels::asset_export::paint_thumb(ui, st, a, r);
|
||||
let checked = on.contains(&a.id);
|
||||
if crate::widgets::check_name(ui, &a.name, checked) {
|
||||
on.retain(|id| *id != a.id);
|
||||
if !checked {
|
||||
on.push(a.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
if scroll_to == Some(a.id) {
|
||||
if ui.clip_rect().contains_rect(row.response.rect) {
|
||||
in_view = true;
|
||||
} else {
|
||||
row.response.scroll_to_me(Some(egui::Align::Center));
|
||||
}
|
||||
let clicked = tiles(ui, "efs-assets", st.doc.assets.len(), list, |ui, i, r| {
|
||||
let Some(a) = st.doc.assets.get(i) else { return (String::new(), String::new(), false) };
|
||||
crate::panels::asset_export::paint_thumb(ui, st, a, r);
|
||||
if scroll_to == Some(a.id) {
|
||||
if ui.clip_rect().contains_rect(r) {
|
||||
in_view = true;
|
||||
} else {
|
||||
ui.scroll_to_rect(r, Some(egui::Align::Center));
|
||||
}
|
||||
}
|
||||
(a.name.clone(), a.name.clone(), on.contains(&a.id))
|
||||
});
|
||||
for i in clicked {
|
||||
if let Some(a) = st.doc.assets.get(i) {
|
||||
if on.contains(&a.id) {
|
||||
on.retain(|id| *id != a.id);
|
||||
} else {
|
||||
on.push(a.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if in_view {
|
||||
d.fields.remove("__scrollTo");
|
||||
}
|
||||
@@ -583,8 +604,8 @@ fn assets(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Export to: the folder (typed or picked), Open Location after Export and Create Sub-folders; the
|
||||
/// web downloads instead.
|
||||
/// Export to: the folder (typed or picked), Open Location after Export, and Create Sub-folders,
|
||||
/// a folder per scale or per format; the web downloads instead.
|
||||
fn destination(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
widgets::dim_label(ui, tl!("Export to:"));
|
||||
if io::is_web(app) {
|
||||
@@ -592,78 +613,111 @@ fn destination(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
} else {
|
||||
let pick = app.services.pick_folder.is_some();
|
||||
ui.horizontal(|ui| {
|
||||
form::text(ui, d, "folder", if pick { 300.0 } else { 334.0 });
|
||||
form::text(ui, d, "folder", if pick { RIGHT_W - 40.0 } else { RIGHT_W - 6.0 });
|
||||
if pick && widgets::icon_button(ui, "folder-open", tl!("Choose a folder"), false, 26.0).clicked() {
|
||||
crate::picks::folder_field(app, d, "folder");
|
||||
}
|
||||
});
|
||||
form::check(ui, d, "openLocation", tl!("Open Location after Export"));
|
||||
}
|
||||
form::check(ui, d, "subfolders", tl!("Create Sub-folders"));
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Folders:"));
|
||||
let current = d.str("folders");
|
||||
let at = FOLDERS.iter().position(|(id, _)| *id == current).unwrap_or(0);
|
||||
if let Some((id, _)) = widgets::dropdown(ui, "efs-folders", FOLDER_LABELS[at], &FOLDER_LABELS, 190.0).and_then(|i| FOLDERS.get(i)) {
|
||||
d.fields.insert("folders".into(), json!(id));
|
||||
d.fields.insert("subfolders".into(), json!(*id == "rows"));
|
||||
ui.add_space(22.0);
|
||||
let on = d.bool("subfolders");
|
||||
let by = d.str("subfolderBy");
|
||||
if widgets::radio(ui, tl!("Scale"), by != "format", on) {
|
||||
d.fields.insert("subfolderBy".into(), json!("scale"));
|
||||
}
|
||||
if widgets::radio(ui, tl!("Format"), by == "format", on) {
|
||||
d.fields.insert("subfolderBy".into(), json!("format"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Export Preset: apply a built-in or saved preset (its formats, settings, naming and folders),
|
||||
/// save the dialog's settings as one (Save…, then a name), or delete a saved one.
|
||||
fn presets(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
/// Export PDFs As: one file of every chosen artboard, or a file each; only for artboards (not
|
||||
/// Full Document) with a PDF row.
|
||||
fn pdf_mode(ui: &mut egui::Ui, d: &mut Dialog, artboards: bool) {
|
||||
let pdf = d
|
||||
.fields
|
||||
.get("formats")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|rows| rows.iter().any(|r| r["format"].as_str().and_then(fileio::format).is_some_and(|f| f.id == "pdf")));
|
||||
let on = artboards && pdf && d.str("select") != "full";
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled_ui(on, |ui| widgets::dim_label(ui, tl!("Export PDFs As:")));
|
||||
let single = d.str("pdfs") == "single";
|
||||
if widgets::radio(ui, tl!("Single File"), single, on) {
|
||||
d.fields.insert("pdfs".into(), json!("single"));
|
||||
}
|
||||
if widgets::radio(ui, tl!("Multiple Files"), !single, on) {
|
||||
d.fields.insert("pdfs".into(), json!("multiple"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The format presets (the ☰ by iOS and Android): apply a built-in or saved one (its rows, their
|
||||
/// settings, the prefix and the sub-folders), save the dialog's rows as one, or delete the saved
|
||||
/// one applied last.
|
||||
fn preset_menu(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let list: Vec<Value> = d.fields.get("__exportPresets").and_then(Value::as_array).cloned().unwrap_or_default();
|
||||
let names: Vec<&str> = list.iter().filter_map(|p| p["name"].as_str()).collect();
|
||||
let current = d.str("exportPreset");
|
||||
let saved = list.iter().any(|p| p["name"] == current && p["builtIn"] == false);
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Export Preset:"));
|
||||
let labels: Vec<&str> = std::iter::once("Custom").chain(names.iter().copied()).collect();
|
||||
let at = names.iter().position(|n| *n == current).map_or(0, |i| i + 1);
|
||||
if let Some(i) = widgets::dropdown(ui, "efs-export-preset", labels[at], &labels, 150.0)
|
||||
&& let Some(preset) = i.checked_sub(1).and_then(|i| list.get(i))
|
||||
{
|
||||
apply_preset(d, preset);
|
||||
}
|
||||
if widgets::icon_button(ui, "save", tl!("Save Preset…"), false, 24.0).clicked() {
|
||||
let name = if saved { current.clone() } else { String::new() };
|
||||
d.fields.insert("__savePreset".into(), json!(name));
|
||||
}
|
||||
ui.add_enabled_ui(saved, |ui| {
|
||||
if widgets::icon_button(ui, "trash-2", tl!("Delete Preset"), false, 24.0).clicked()
|
||||
&& app.run("document.exportPresets.delete", json!({"name": current})).is_ok()
|
||||
{
|
||||
d.fields.insert("exportPreset".into(), json!(""));
|
||||
d.fields.insert("__exportPresets".into(), export_presets(app));
|
||||
let resp = widgets::icon_button(ui, "menu", tl!("Format Presets"), false, 24.0);
|
||||
let clicked = resp.clicked();
|
||||
let _ = widgets::popover(&resp, clicked, |ui| {
|
||||
for p in &list {
|
||||
let name = p["name"].as_str().unwrap_or_default();
|
||||
if widgets::menu_item_name(ui, name, true, name == current) {
|
||||
apply_preset(d, p);
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.separator();
|
||||
if widgets::menu_item(ui, tl!("Save Preset…"), true, false) {
|
||||
d.fields.insert("__savePreset".into(), json!(if saved { current.clone() } else { String::new() }));
|
||||
ui.close();
|
||||
}
|
||||
if widgets::menu_item(ui, tl!("Delete Preset"), saved, false) && app.run("document.exportPresets.delete", json!({"name": current})).is_ok() {
|
||||
d.fields.insert("exportPreset".into(), json!(""));
|
||||
d.fields.insert("__exportPresets".into(), export_presets(app));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
if d.fields.contains_key("__savePreset") {
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Name:"));
|
||||
form::text(ui, d, "__savePreset", 150.0);
|
||||
let name = d.str("__savePreset").trim().to_string();
|
||||
if ui.add_enabled(!name.is_empty(), egui::Button::new(tl!("Save"))).clicked() {
|
||||
let settings = params(app, d).unwrap_or_else(|_| json!({"formats": d.fields.get("formats").cloned().unwrap_or(json!([]))}));
|
||||
match app.run("document.exportPresets.save", json!({"name": name, "settings": settings})) {
|
||||
Ok(_) => {
|
||||
d.fields.remove("__savePreset");
|
||||
d.fields.insert("exportPreset".into(), json!(name));
|
||||
d.fields.insert("__exportPresets".into(), export_presets(app));
|
||||
}
|
||||
Err(e) => app.ui.status = e,
|
||||
}
|
||||
}
|
||||
if ui.small_button(tl!("Cancel")).clicked() {
|
||||
d.fields.remove("__savePreset");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the dialog to preset `preset` (`{name, settings}`): its formats, settings, naming and
|
||||
/// folders; what it doesn't name keeps the dialog's value.
|
||||
/// The row that names a format preset being saved (Save Preset… in the ☰ menu).
|
||||
fn save_preset_row(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
if !d.fields.contains_key("__savePreset") {
|
||||
return;
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Name:"));
|
||||
form::text(ui, d, "__savePreset", 150.0);
|
||||
let name = d.str("__savePreset").trim().to_string();
|
||||
if ui.add_enabled(!name.is_empty(), egui::Button::new(tl!("Save"))).clicked() {
|
||||
let settings = json!({
|
||||
"formats": d.fields.get("formats").cloned().unwrap_or(json!([])),
|
||||
"settings": d.fields.get("settings").cloned().unwrap_or(json!({})),
|
||||
"prefix": d.str("prefix"),
|
||||
"folders": if !d.bool("subfolders") { "none" } else if d.str("subfolderBy") == "format" { "format" } else { "rows" },
|
||||
});
|
||||
match app.run("document.exportPresets.save", json!({"name": name, "settings": settings})) {
|
||||
Ok(_) => {
|
||||
d.fields.remove("__savePreset");
|
||||
d.fields.insert("exportPreset".into(), json!(name));
|
||||
d.fields.insert("__exportPresets".into(), export_presets(app));
|
||||
}
|
||||
Err(e) => app.ui.status = e,
|
||||
}
|
||||
}
|
||||
if ui.small_button(tl!("Cancel")).clicked() {
|
||||
d.fields.remove("__savePreset");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Set the dialog to format preset `preset` (`{name, settings}`): its rows, their settings, the
|
||||
/// prefix and the sub-folders; what it doesn't name keeps the dialog's value.
|
||||
pub(super) fn apply_preset(d: &mut Dialog, preset: &Value) {
|
||||
let st = &preset["settings"];
|
||||
if let Some(rows) = st["formats"].as_array() {
|
||||
@@ -680,127 +734,29 @@ pub(super) fn apply_preset(d: &mut Dialog, preset: &Value) {
|
||||
}
|
||||
let folders = st["folders"].as_str().or_else(|| st["subfolders"].as_bool().map(|b| if b { "rows" } else { "none" }));
|
||||
if let Some(f) = folders {
|
||||
d.fields.insert("folders".into(), json!(f));
|
||||
d.fields.insert("subfolders".into(), json!(f == "rows"));
|
||||
d.fields.insert("subfolders".into(), json!(f != "none"));
|
||||
d.fields.insert("subfolderBy".into(), json!(if f == "format" { "format" } else { "scale" }));
|
||||
}
|
||||
for k in ["name", "prefix"] {
|
||||
d.fields.insert(k.into(), json!(st[k].as_str().unwrap_or_default()));
|
||||
if let Some(prefix) = st["prefix"].as_str() {
|
||||
d.fields.insert("prefix".into(), json!(prefix));
|
||||
}
|
||||
d.fields.insert("counterStart".into(), json!(st["counterStart"].as_u64().unwrap_or(1)));
|
||||
if let Some(b) = st["includeBleed"].as_bool() {
|
||||
d.fields.insert("includeBleed".into(), json!(b));
|
||||
}
|
||||
if let Some(f) = st["folder"].as_str().filter(|f| !f.is_empty()) {
|
||||
d.fields.insert("folder".into(), json!(f));
|
||||
}
|
||||
d.fields.insert("exportPreset".into(), json!(preset["name"]));
|
||||
}
|
||||
|
||||
/// File Name: the pattern (with Insert for its tokens), the counter's start, the prefix and an
|
||||
/// example of the first file's name.
|
||||
fn naming(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("File Name:"));
|
||||
// A pattern typed by hand is no longer the preset's.
|
||||
if form::text(ui, d, "name", 170.0) {
|
||||
d.fields.insert("exportPreset".into(), json!(""));
|
||||
}
|
||||
if let Some((_, token)) =
|
||||
widgets::dropdown_named(ui, "efs-name-token", Some(tl!("Insert")), tl!("Insert"), &TOKEN_LABELS, 90.0).and_then(|i| TOKENS.get(i))
|
||||
{
|
||||
let name = d.str("name");
|
||||
d.fields.insert("name".into(), json!(format!("{name}{token}")));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Prefix:"));
|
||||
form::text(ui, d, "prefix", 110.0);
|
||||
widgets::dim_label(ui, tl!("Start at:"));
|
||||
if let Some(v) = widgets::range_field(ui, "efs-counter-start", counter_start(d) as f64, 0.0..=999_999.0, "", 0, 64.0) {
|
||||
d.fields.insert("counterStart".into(), json!(v.round().max(0.0) as u64));
|
||||
}
|
||||
});
|
||||
let example = example_name(app, d);
|
||||
if !example.is_empty() {
|
||||
widgets::dim_label(ui, &crate::i18n::fmt(tl!("Example: {name}"), &[("name", &example)]));
|
||||
}
|
||||
}
|
||||
/// What the dialog adds by the iOS and Android buttons: its format presets.
|
||||
pub(crate) type FormatMenu<'a> = &'a mut dyn FnMut(&mut egui::Ui, &mut Dialog);
|
||||
|
||||
/// The first file the export writes, relative to the export folder, as `document.exportForScreens`
|
||||
/// names it ([`fileio::screen_first_file`]); empty while the dialog's settings choose nothing to
|
||||
/// export (no format, nothing checked or selected, a bad range).
|
||||
pub(super) fn example_name(app: &VectorcraftApp, d: &Dialog) -> String {
|
||||
settings_params(app, d).ok().and_then(|(p, _)| fileio::screen_first_file(&app.session, &Value::Object(p)).ok()).unwrap_or_default()
|
||||
}
|
||||
/// The iOS and Android rows (the engine's `mobile` and `density` presets) and their labels.
|
||||
const PLATFORMS: [(&str, &str); 2] = [("mobile", "iOS"), ("density", "Android")];
|
||||
|
||||
/// The Layers tab: each top-level layer's thumbnail and checkbox (a file of its art alone),
|
||||
/// Select All and Clear.
|
||||
fn layers(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let mut on = checked_layers(app, d);
|
||||
let before = on.clone();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button(tl!("Select All")).clicked() {
|
||||
on = st.doc.layers.iter().map(|l| l.id.0).collect();
|
||||
}
|
||||
if ui.small_button(tl!("Clear")).clicked() {
|
||||
on.clear();
|
||||
}
|
||||
});
|
||||
egui::ScrollArea::vertical().max_height(330.0).show(ui, |ui| {
|
||||
for l in &st.doc.layers {
|
||||
ui.horizontal(|ui| {
|
||||
let (r, _) = ui.allocate_exact_size(egui::vec2(46.0, 46.0), egui::Sense::hover());
|
||||
let mut art = vec![];
|
||||
layer_art(&st.doc, l.id, &mut art);
|
||||
// An id of its own for the thumbnail cache, away from the assets' ids.
|
||||
let a = ExportAsset { id: u64::MAX - l.id.0, name: l.display_name(), nodes: art };
|
||||
crate::panels::asset_export::paint_thumb(ui, st, &a, r);
|
||||
let checked = on.contains(&l.id.0);
|
||||
if crate::widgets::check_name(ui, &l.display_name(), checked) {
|
||||
on.retain(|id| *id != l.id.0);
|
||||
if !checked {
|
||||
on.push(l.id.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if on != before {
|
||||
d.fields.insert("layers".into(), json!(on));
|
||||
}
|
||||
}
|
||||
|
||||
/// The Selection tab: what is selected (a thumbnail and the count), as one file or a file per
|
||||
/// object.
|
||||
fn selection(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let ids = st.selection.objects.clone();
|
||||
if ids.is_empty() {
|
||||
ui.add_space(24.0);
|
||||
widgets::dim_label(ui, tl!("Nothing is selected. Select the art to export, then open this tab again."));
|
||||
return;
|
||||
}
|
||||
let (r, _) = ui.allocate_exact_size(egui::vec2(120.0, 120.0), egui::Sense::hover());
|
||||
// The selection changes without a new revision: its ids name its thumbnail.
|
||||
let key = ids.iter().fold(0x005e_1ec7_u64, |h, id| h.rotate_left(7) ^ id.0);
|
||||
let a = ExportAsset { id: u64::MAX / 2 + (key % (u64::MAX / 4)), name: "Selection".into(), nodes: ids.clone() };
|
||||
crate::panels::asset_export::paint_thumb(ui, st, &a, r);
|
||||
let count = ids.len().to_string();
|
||||
widgets::dim_label(ui, &crate::i18n::fmt(tl!("{n} objects selected"), &[("n", &count)]));
|
||||
ui.add_space(6.0);
|
||||
let mode = d.str("selectionMode");
|
||||
if widgets::radio(ui, tl!("One File of the Selection"), mode != "each", true) {
|
||||
d.fields.insert("selectionMode".into(), json!("one"));
|
||||
}
|
||||
if widgets::radio(ui, tl!("A File per Object"), mode == "each", true) {
|
||||
d.fields.insert("selectionMode".into(), json!("each"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The preset, the Format Settings gear (which sets `__settings`), the format rows (scale, suffix,
|
||||
/// format) and Add Scale, as wide as `ui` allows: the dialog's and the Asset Export panel's rows.
|
||||
pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
/// Formats: the iOS and Android buttons (each sets the rows that platform uses), `menu` (the
|
||||
/// dialog's format presets), the Format Settings gear (which sets `__settings`), the format rows
|
||||
/// (scale, suffix, format) and Add Scale, as wide as `ui` allows: the dialog's and the Asset
|
||||
/// Export panel's rows.
|
||||
pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog, mut menu: Option<FormatMenu<'_>>) {
|
||||
let mut rows: Vec<Value> = d.fields.get("formats").and_then(Value::as_array).cloned().unwrap_or_default();
|
||||
let mut edited = false;
|
||||
ui.horizontal(|ui| {
|
||||
@@ -816,19 +772,21 @@ pub(crate) fn formats(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
.filter(|f| SETTINGS.iter().any(|(id, _)| id == f));
|
||||
d.fields.insert("__settings".into(), json!(first.unwrap_or("png")));
|
||||
}
|
||||
let preset = d.str("preset");
|
||||
let labels: Vec<&str> = std::iter::once("Custom").chain(SCREEN_PRESETS.iter().map(|(_, l)| *l)).collect();
|
||||
let at = SCREEN_PRESETS.iter().position(|(id, _)| *id == preset).map_or(0, |i| i + 1);
|
||||
if let Some(i) =
|
||||
widgets::dropdown_named(ui, "efs-preset", Some(tl!("Preset:")), labels[at], &labels, (ui.available_width() - 8.0).clamp(90.0, 180.0))
|
||||
{
|
||||
let id = i.checked_sub(1).and_then(|i| SCREEN_PRESETS.get(i)).map_or("", |(id, _)| *id);
|
||||
if let Some(preset_rows) = fileio::screen_preset_rows(id) {
|
||||
if let Some(menu) = menu.as_mut() {
|
||||
menu(ui, d);
|
||||
}
|
||||
// Right to left: Android, then iOS before it.
|
||||
for (id, label) in PLATFORMS.iter().rev() {
|
||||
let on = d.str("preset") == *id;
|
||||
if ui.add(egui::Button::new(egui::RichText::new(tl!(label)).size(12.0)).selected(on).frame(on)).clicked()
|
||||
&& let Some(preset_rows) = fileio::screen_preset_rows(id)
|
||||
{
|
||||
rows = preset_rows;
|
||||
d.fields.insert("subfolders".into(), json!(true));
|
||||
d.fields.insert("subfolderBy".into(), json!("scale"));
|
||||
d.fields.insert("formats".into(), json!(rows));
|
||||
d.fields.insert("preset".into(), json!(id));
|
||||
}
|
||||
d.fields.insert("preset".into(), json!(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,16 +39,16 @@ fn strict(app: VectorcraftApp) -> Harness {
|
||||
h
|
||||
}
|
||||
|
||||
/// Export for Screens names its three text fields and exposes no raw widget ids.
|
||||
/// Export for Screens names its range and prefix fields and exposes no raw widget ids.
|
||||
#[test]
|
||||
fn export_for_screens_names_the_form_text_fields() {
|
||||
let mut app = app();
|
||||
app.run("file.exportForScreens", json!({})).unwrap();
|
||||
let h = strict(app);
|
||||
for name in ["File Name:", "Prefix:", "Start at:"] {
|
||||
for name in ["Range:", "Prefix:"] {
|
||||
let _ = h.get_by_role_and_label(Role::TextInput, name);
|
||||
}
|
||||
assert!(h.query_by_role_and_label(Role::TextInput, "name").is_none(), "no internal widget id");
|
||||
assert!(h.query_by_role_and_label(Role::TextInput, "range").is_none(), "no internal widget id");
|
||||
assert!(h.query_by_role_and_label(Role::TextInput, "prefix").is_none(), "no internal widget id");
|
||||
}
|
||||
|
||||
|
||||
@@ -65,10 +65,10 @@ fn export_selection_collects_the_selection_and_opens_the_assets_tab() {
|
||||
assert_eq!(assets(&mut app).len(), 2);
|
||||
assert_eq!(field(&app, "assets"), json!(ids));
|
||||
let text = painted_text(&mut app, |app, ui| super::show(app, ui.ctx()));
|
||||
assert!(text.contains("Asset 1") && text.contains("Asset 2") && text.contains("Select All"), "{text}");
|
||||
// The web downloads both as one zip.
|
||||
assert!(text.contains("Asset 1") && text.contains("Asset 2") && text.contains("Clear Selection"), "{text}");
|
||||
// The web downloads both as one zip, in the 1x sub-folder Create Sub-folders makes by default.
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["files"], json!(["Asset-1.png", "Asset-2.png"]));
|
||||
assert_eq!(r["files"], json!(["1x/Asset-1.png", "1x/Asset-2.png"]));
|
||||
assert_eq!(log.borrow().len(), 1);
|
||||
assert!(app.ui.dialog.is_none());
|
||||
// Nothing selected: Export Selection is off; document.exportSelection still exports in one go.
|
||||
@@ -112,8 +112,9 @@ fn the_assets_tab_exports_the_checked_assets_into_the_folder() {
|
||||
set(&mut app, "assets", json!([ids[1], 999]));
|
||||
set(&mut app, "formats", json!([{"format": "png", "scale": "2x", "suffix": "@2x"}]));
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["files"], json!([format!("{folder}/Asset-2@2x.png")]));
|
||||
let png = image::load_from_memory(&std::fs::read(dir.join("Asset-2@2x.png")).unwrap()).unwrap();
|
||||
// Create Sub-folders is on by default: a folder per scale.
|
||||
assert_eq!(r["files"], json!([format!("{folder}/2x/Asset-2@2x.png")]));
|
||||
let png = image::load_from_memory(&std::fs::read(dir.join("2x").join("Asset-2@2x.png")).unwrap()).unwrap();
|
||||
assert_eq!((png.width(), png.height()), (60, 40), "the asset's art at 2x");
|
||||
assert_eq!(log.borrow().len(), 1, "Open Location shows the file");
|
||||
// The rows are remembered for the dialog and the Asset Export panel alike.
|
||||
|
||||
@@ -63,7 +63,8 @@ fn opens_on_defaults_and_draws_every_view() {
|
||||
assert_eq!((field(&app, "select"), field(&app, "range"), field(&app, "boards")), (json!("all"), json!("1-3"), json!([true, true, true])));
|
||||
assert_eq!(field(&app, "formats"), json!([{"format": "png", "scale": "1x", "suffix": ""}]));
|
||||
assert_eq!(field(&app, "folder"), json!(fileio::export_folder().unwrap_or_default()), "the Desktop, else home");
|
||||
assert_eq!((field(&app, "openLocation"), field(&app, "subfolders"), field(&app, "preset")), (json!(true), json!(false), json!("")));
|
||||
assert_eq!((field(&app, "openLocation"), field(&app, "subfolders"), field(&app, "preset")), (json!(true), json!(true), json!("")));
|
||||
assert_eq!((field(&app, "subfolderBy"), field(&app, "pdfs"), field(&app, "view")), (json!("scale"), json!("multiple"), json!("grid")));
|
||||
let settings = field(&app, "settings");
|
||||
for f in ["png", "png8", "jpg", "webp", "gif", "tiff", "svg", "pdf"] {
|
||||
assert!(settings[f].is_object(), "{f} has settings");
|
||||
@@ -128,7 +129,8 @@ fn opens_with_the_artboards_the_artboard_tool_selected_and_exports_only_them() {
|
||||
frame(&mut app);
|
||||
assert_eq!(field(&app, "boards"), json!([false, true, true, false]), "drawing the dialog keeps them");
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["files"], json!(["Artboard-2.png", "Artboard-3.png"]));
|
||||
// Create Sub-folders is on by default, a folder per scale.
|
||||
assert_eq!(r["files"], json!(["1x/Artboard-2.png", "1x/Artboard-3.png"]));
|
||||
assert_eq!(log.borrow().len(), 1, "one zip");
|
||||
// With every artboard selected it opens on All.
|
||||
click(&mut app, 0, true);
|
||||
@@ -433,7 +435,7 @@ fn the_web_downloads_one_zip_or_the_file() {
|
||||
assert_eq!(name, "Untitled-1.zip");
|
||||
assert_eq!(&vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap(), zip);
|
||||
}
|
||||
assert_eq!(r["files"], json!(["Artboard-1.png", "Artboard-1@2x.jpg", "Artboard-2.png", "Artboard-2@2x.jpg"]));
|
||||
assert_eq!(r["files"], json!(["1x/Artboard-1.png", "2x/Artboard-1@2x.jpg", "1x/Artboard-2.png", "2x/Artboard-2@2x.jpg"]));
|
||||
// One file downloads as itself, with the format's settings.
|
||||
log.borrow_mut().clear();
|
||||
open(&mut app);
|
||||
@@ -602,7 +604,7 @@ fn screen_format_gear(out: &egui::FullOutput, width: f32) -> egui::Pos2 {
|
||||
}
|
||||
}
|
||||
let label = out.shapes.iter().find_map(|s| label(&s.shape)).expect("painted Formats row");
|
||||
// The existing 24 pt gear is right-aligned in the 350 pt dialog or 236 pt panel.
|
||||
// The existing 24 pt gear is right-aligned in the dialog's 370 pt column or the 236 pt panel.
|
||||
egui::pos2(label.left() + width - 12.0, label.center().y)
|
||||
}
|
||||
|
||||
@@ -635,7 +637,7 @@ fn saved_tiff_alias_gear(alias: &str, asset_panel: bool) {
|
||||
};
|
||||
draw(&mut h, &mut app, vec![]);
|
||||
let out = draw(&mut h, &mut app, vec![]);
|
||||
let gear = screen_format_gear(&out, if asset_panel { 236.0 } else { 350.0 });
|
||||
let gear = screen_format_gear(&out, if asset_panel { 236.0 } else { 370.0 });
|
||||
draw(&mut h, &mut app, vec![egui::Event::PointerMoved(gear)]);
|
||||
for pressed in [true, false] {
|
||||
draw(
|
||||
@@ -707,11 +709,12 @@ fn tiff_dropdown_exports_scaled_artboards_with_options_and_reopens() {
|
||||
// Begin with an existing PNG row; TIFF must be chosen from the actual popup.
|
||||
set(&mut app, "formats", json!([{"format": "png", "scale": "2x", "suffix": "@2x"}]));
|
||||
set(&mut app, "prefix", json!("proof-"));
|
||||
set(&mut app, "subfolders", json!(false));
|
||||
let mut h = ScreensFrame::new();
|
||||
h.choose(&mut app, "PNG", "TIFF");
|
||||
assert_eq!(field(&app, "formats"), json!([{"format": "tiff", "scale": "2x", "suffix": "@2x"}]));
|
||||
// Leave only the first of two boards checked.
|
||||
h.click_label(&mut app, "Artboard 2");
|
||||
h.click_label(&mut app, "2 Artboard 2");
|
||||
assert_eq!(field(&app, "boards"), json!([true, false]));
|
||||
assert_eq!(field(&app, "range"), json!("1"));
|
||||
|
||||
@@ -766,168 +769,114 @@ fn tiff_dropdown_exports_scaled_artboards_with_options_and_reopens() {
|
||||
/// name, a prefix with characters Windows does not allow, a range, a Windows device name as an item
|
||||
/// folder, the rows' folders and sizes, Create Sub-folders, a vector row's size suffix, the counter
|
||||
/// typed as text, and the Layers and Selection tabs.
|
||||
/// The dialog is laid out as Illustrator's: two tabs (Artboards, Assets), Clear Selection and the
|
||||
/// prefix under the thumbnails, Create Sub-folders by scale or format, Export PDFs As, the iOS and
|
||||
/// Android rows, and how many items and files the export takes; no naming, layer or selection
|
||||
/// pages, presets bar or folders list.
|
||||
#[test]
|
||||
fn the_example_names_the_first_file_the_export_writes() {
|
||||
fn the_dialog_is_laid_out_as_illustrators() {
|
||||
let (mut app, _) = app(3, false);
|
||||
app.run("artboard.setProps", json!({"index": 2, "name": "CON"})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
let dir = temp_folder("example");
|
||||
let folder = dir.to_string_lossy().to_string();
|
||||
let png = json!([{"format": "png", "scale": "1x", "suffix": ""}]);
|
||||
let cases: Vec<Vec<(&str, Value)>> = vec![
|
||||
vec![],
|
||||
vec![("prefix", json!("a?b"))],
|
||||
vec![("select", json!("range")), ("range", json!("2-3"))],
|
||||
vec![("select", json!("range")), ("range", json!("3")), ("folders", json!("item"))],
|
||||
vec![("folders", json!("rows")), ("formats", json!([{"format": "png", "scale": "144ppi", "suffix": ""}]))],
|
||||
vec![("subfolders", json!(true))],
|
||||
vec![("folders", json!("rows")), ("formats", json!([{"format": "png", "scale": "2x", "suffix": "@2x", "folder": "retina"}]))],
|
||||
vec![("formats", json!([{"format": "svg", "scale": "2x", "suffix": "@2x"}]))],
|
||||
vec![("name", json!("<scale>-####")), ("counterStart", json!("7")), ("formats", json!([{"format": "png", "scale": "144ppi", "suffix": ""}]))],
|
||||
vec![("tab", json!("layers"))],
|
||||
vec![("tab", json!("selection")), ("selectionMode", json!("each"))],
|
||||
];
|
||||
let mut wrong = vec![];
|
||||
for case in cases {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
open(&mut app);
|
||||
let defaults = [
|
||||
("folder", json!(folder)),
|
||||
("tab", json!("artboards")),
|
||||
("select", json!("all")),
|
||||
("prefix", json!("")),
|
||||
("name", json!("")),
|
||||
("counterStart", json!(1)),
|
||||
("folders", json!("none")),
|
||||
("subfolders", json!(false)),
|
||||
("formats", png.clone()),
|
||||
];
|
||||
for (k, v) in defaults.into_iter().chain(case.iter().cloned()) {
|
||||
set(&mut app, k, v);
|
||||
}
|
||||
frame(&mut app);
|
||||
let example = export_for_screens::example_name(&app, app.ui.dialog.as_ref().unwrap());
|
||||
let r = confirm(&mut app).unwrap();
|
||||
let first = r["files"][0].as_str().and_then(|f| f.strip_prefix(&format!("{folder}/"))).unwrap_or_default().to_string();
|
||||
if example != first {
|
||||
wrong.push(format!("{case:?}: example {example:?}, written {first:?}"));
|
||||
}
|
||||
open(&mut app);
|
||||
let text = crate::tests_labels::shapes_text(&ScreensFrame::new().settle(&mut app));
|
||||
for label in [
|
||||
"Artboards",
|
||||
"Assets",
|
||||
"Select:",
|
||||
"Full Document",
|
||||
"Include Bleed",
|
||||
"Export to:",
|
||||
"Open Location after Export",
|
||||
"Create Sub-folders",
|
||||
"Scale",
|
||||
"Format",
|
||||
"Export PDFs As:",
|
||||
"Single File",
|
||||
"Multiple Files",
|
||||
"Formats:",
|
||||
"iOS",
|
||||
"Android",
|
||||
"+ Add Scale",
|
||||
"Clear Selection",
|
||||
"Prefix:",
|
||||
"Selected: 3, Total number of files to export: 3",
|
||||
] {
|
||||
assert!(text.lines().any(|l| l.trim() == label), "{label} missing:\n{text}");
|
||||
}
|
||||
for gone in ["Layers", "Selection", "Export Preset:", "File Name:", "Folders:", "Start at:"] {
|
||||
assert!(!text.lines().any(|l| l.trim() == gone), "{gone} shown:\n{text}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert!(wrong.is_empty(), "{wrong:#?}");
|
||||
}
|
||||
|
||||
/// The Example line shows before a folder is chosen and is left out while the settings choose
|
||||
/// nothing to export (the Assets tab with no asset checked); Start at shows the counter the export
|
||||
/// uses when it was set as text.
|
||||
/// iOS and Android set the rows those platforms use (in sub-folders); Clear Selection unchecks
|
||||
/// every artboard, so nothing is exported until one is checked again.
|
||||
#[test]
|
||||
fn the_example_needs_no_folder_and_start_at_shows_the_counter_used() {
|
||||
let (mut app, _) = app(1, false);
|
||||
fn ios_android_and_clear_selection() {
|
||||
let (mut app, _) = app(2, false);
|
||||
open(&mut app);
|
||||
for (k, v) in [("folder", json!("")), ("name", json!("####")), ("counterStart", json!("7"))] {
|
||||
set(&mut app, "subfolders", json!(false));
|
||||
let mut h = ScreensFrame::new();
|
||||
h.click_label(&mut app, "iOS");
|
||||
let scales: Vec<Value> = field(&app, "formats").as_array().unwrap().iter().map(|r| r["scale"].clone()).collect();
|
||||
assert_eq!((field(&app, "preset"), field(&app, "subfolders")), (json!("mobile"), json!(true)));
|
||||
assert_eq!(scales, [json!("1x"), json!("2x"), json!("3x")]);
|
||||
h.click_label(&mut app, "Android");
|
||||
assert_eq!((field(&app, "preset"), field(&app, "formats").as_array().unwrap().len()), (json!("density"), 6));
|
||||
h.click_label(&mut app, "Clear Selection");
|
||||
assert_eq!((field(&app, "boards"), field(&app, "select")), (json!([false, false]), json!("range")));
|
||||
let text = crate::tests_labels::shapes_text(&h.settle(&mut app));
|
||||
assert!(text.contains("Selected: 0, Total number of files to export: 0"), "{text}");
|
||||
set(&mut app, "folder", json!(temp_folder("cleared").to_string_lossy()));
|
||||
assert!(confirm(&mut app).is_err(), "nothing to export");
|
||||
}
|
||||
|
||||
/// Create Sub-folders sorts the files into a folder per scale or per format; Export PDFs As ›
|
||||
/// Single file writes the chosen artboards as one PDF named after the document.
|
||||
#[test]
|
||||
fn sub_folders_and_single_file_pdfs() {
|
||||
let (mut app, _) = app(3, false);
|
||||
let dir = temp_folder("subfolders");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let folder = dir.to_string_lossy().to_string();
|
||||
let title = app.session.active().unwrap().doc.title.clone();
|
||||
let rows = json!([
|
||||
{"format": "png", "scale": "1x", "suffix": ""},
|
||||
{"format": "png", "scale": "2x", "suffix": "@2x"},
|
||||
{"format": "pdf", "scale": "1x", "suffix": ""}
|
||||
]);
|
||||
let files = |app: &mut VectorcraftApp, extra: &[(&str, Value)]| -> Vec<String> {
|
||||
open(app);
|
||||
let base = [("folder", json!(folder)), ("select", json!("range")), ("range", json!("1-2")), ("formats", rows.clone())];
|
||||
for (k, v) in base.into_iter().chain(extra.iter().cloned()) {
|
||||
set(app, k, v);
|
||||
}
|
||||
frame(app);
|
||||
let r = confirm(app).unwrap();
|
||||
r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().strip_prefix(&format!("{folder}/")).unwrap().to_string()).collect()
|
||||
};
|
||||
let by_scale = files(&mut app, &[("subfolders", json!(true)), ("subfolderBy", json!("scale"))]);
|
||||
assert_eq!(&by_scale[..2], ["1x/Artboard-1.png", "2x/Artboard-1@2x.png"]);
|
||||
let by_format = files(&mut app, &[("subfolders", json!(true)), ("subfolderBy", json!("format")), ("pdfs", json!("single"))]);
|
||||
let pdf = format!("pdf/{title}.pdf");
|
||||
assert_eq!(by_format, ["png/Artboard-1.png", "png/Artboard-1@2x.png", pdf.as_str(), "png/Artboard-2.png", "png/Artboard-2@2x.png"]);
|
||||
let flat = files(&mut app, &[("subfolders", json!(false)), ("pdfs", json!("multiple"))]);
|
||||
assert_eq!(flat.len(), 6);
|
||||
assert!(flat.contains(&"Artboard-2.pdf".to_string()), "{flat:?}");
|
||||
// The status line counts the files Single file writes.
|
||||
open(&mut app);
|
||||
for (k, v) in [("select", json!("range")), ("range", json!("1-2")), ("formats", rows.clone()), ("pdfs", json!("single"))] {
|
||||
set(&mut app, k, v);
|
||||
}
|
||||
let mut h = ScreensFrame::new();
|
||||
let text = crate::tests_labels::shapes_text(&h.settle(&mut app));
|
||||
assert!(text.lines().any(|l| l == "Example: 0007.png"), "{text}");
|
||||
assert!(text.lines().any(|l| l.trim() == "7"), "{text}");
|
||||
set(&mut app, "tab", json!("assets"));
|
||||
let text = crate::tests_labels::shapes_text(&h.settle(&mut app));
|
||||
assert!(!text.contains("Example:"), "{text}");
|
||||
}
|
||||
|
||||
/// Batch exporting (#1935): a file name pattern and a folder per format, the Layers and Selection
|
||||
/// tabs, and export presets applied and saved from the dialog.
|
||||
#[test]
|
||||
fn batch_naming_folders_layers_selection_and_presets() {
|
||||
let (mut app, _) = app(3, false);
|
||||
let dir = temp_folder("batch");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let folder = dir.to_string_lossy().to_string();
|
||||
open(&mut app);
|
||||
assert_eq!(field(&app, "folders"), json!("none"));
|
||||
// Every tab draws; the OK button says what is exported.
|
||||
for (tab, ok) in [("layers", "Export Layer"), ("selection", "Export Selection"), ("assets", "Export Asset"), ("artboards", "Export Artboard")] {
|
||||
set(&mut app, "tab", json!(tab));
|
||||
frame(&mut app);
|
||||
assert_eq!((spec(export_for_screens::KIND).ok_label.unwrap())(&app), ok);
|
||||
}
|
||||
// Artboards 1–2 as SVG and print PDF, named by pattern, a folder per format.
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "select", json!("range"));
|
||||
set(&mut app, "range", json!("1-2"));
|
||||
set(&mut app, "name", json!("ACME-<name>-##"));
|
||||
set(&mut app, "folders", json!("format"));
|
||||
set(
|
||||
&mut app,
|
||||
"formats",
|
||||
json!([{"format": "svg", "scale": "1x", "suffix": ""}, {"format": "pdf", "scale": "1x", "suffix": "-print", "marks": {"trim": true}}]),
|
||||
);
|
||||
frame(&mut app);
|
||||
{
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(export_for_screens::example_name(&app, d), "svg/ACME-Artboard 1-01.svg");
|
||||
}
|
||||
let r = confirm(&mut app).unwrap();
|
||||
let files: Vec<String> = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().to_string()).collect();
|
||||
assert_eq!(
|
||||
files,
|
||||
[
|
||||
format!("{folder}/svg/ACME-Artboard 1-01.svg"),
|
||||
format!("{folder}/pdf/ACME-Artboard 1-01-print.pdf"),
|
||||
format!("{folder}/svg/ACME-Artboard 2-02.svg"),
|
||||
format!("{folder}/pdf/ACME-Artboard 2-02-print.pdf")
|
||||
]
|
||||
);
|
||||
// It reopens on the pattern and the folders.
|
||||
open(&mut app);
|
||||
assert_eq!((field(&app, "name"), field(&app, "folders")), (json!("ACME-<name>-##"), json!("format")));
|
||||
|
||||
// The Layers tab: one file of a layer's art.
|
||||
let layer = app.session.active().unwrap().doc.layers[0].id.0;
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "tab", json!("layers"));
|
||||
set(&mut app, "layers", json!([layer]));
|
||||
set(&mut app, "name", json!(""));
|
||||
set(&mut app, "folders", json!("none"));
|
||||
set(&mut app, "formats", json!([{"format": "png", "scale": "1x", "suffix": ""}]));
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["files"][0], json!(format!("{folder}/Layer-1.png")));
|
||||
|
||||
// The Selection tab: nothing selected keeps the dialog open; a file per selected object.
|
||||
open(&mut app);
|
||||
app.run("select.none", json!({})).unwrap();
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "tab", json!("selection"));
|
||||
frame(&mut app);
|
||||
assert!(confirm(&mut app).is_err());
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
set(&mut app, "selectionMode", json!("each"));
|
||||
set(&mut app, "name", json!("piece-#"));
|
||||
set(&mut app, "formats", json!([{"format": "svg", "scale": "1x", "suffix": ""}]));
|
||||
frame(&mut app);
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["files"][0], json!(format!("{folder}/piece-1.svg")));
|
||||
|
||||
// Presets: the Logo Package sets the formats, naming and folders; the dialog saves its own.
|
||||
open(&mut app);
|
||||
let text = crate::tests_labels::shapes_text(&ScreensFrame::new().settle(&mut app));
|
||||
assert!(text.contains("Selected: 2, Total number of files to export: 5"), "{text}");
|
||||
// A format preset sets the rows, their settings and the sub-folders, not a prefix it lacks.
|
||||
set(&mut app, "prefix", json!("x-"));
|
||||
let presets = field(&app, "__exportPresets");
|
||||
let logo = presets.as_array().unwrap().iter().find(|p| p["name"] == "Logo Package").unwrap().clone();
|
||||
export_for_screens::apply_preset(app.ui.dialog.as_mut().unwrap(), &logo);
|
||||
assert_eq!(field(&app, "formats").as_array().unwrap().len(), 5);
|
||||
assert_eq!(
|
||||
(field(&app, "folders"), field(&app, "name"), field(&app, "exportPreset")),
|
||||
(json!("format"), json!("<filename>-<name>"), json!("Logo Package"))
|
||||
(field(&app, "subfolders"), field(&app, "subfolderBy"), field(&app, "exportPreset")),
|
||||
(json!(true), json!("format"), json!("Logo Package"))
|
||||
);
|
||||
frame(&mut app);
|
||||
set(&mut app, "folder", json!(folder));
|
||||
set(&mut app, "__savePreset", json!("Client Pack"));
|
||||
frame(&mut app);
|
||||
let saved = app
|
||||
.run("document.exportPresets.save", json!({"name": "Client Pack", "settings": {"formats": field(&app, "formats"), "folders": "item"}}))
|
||||
.unwrap();
|
||||
assert_eq!(saved["name"], "Client Pack");
|
||||
let list = export_for_screens::export_presets(&mut app);
|
||||
assert!(list.as_array().unwrap().iter().any(|p| p["name"] == "Client Pack" && p["builtIn"] == false));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
@@ -649,6 +649,13 @@ font-technology Variable Променливи
|
||||
Create Report Създаване на отчет
|
||||
Create Slice Създаване на отрязък
|
||||
Create Sub-folders Създаване на подпапки
|
||||
Clear Selection Изчистване на избора
|
||||
Export PDFs As: Експортиране на PDF като:
|
||||
Single File Един файл
|
||||
Multiple Files Няколко файла
|
||||
Format Presets Предварителни настройки на формата
|
||||
Grid View Изглед на мрежа
|
||||
Selected: {n}, Total number of files to export: {m} Избрани: {n}, общ брой файлове за експортиране: {m}
|
||||
Create Text Създаване на текст
|
||||
Create Trim Marks Създаване на режещи знаци
|
||||
Created Създадено
|
||||
@@ -3952,6 +3959,7 @@ axis Both И двете
|
||||
@msg cannot split an open path at its end point отворен път не може да бъде разделен в крайната му точка
|
||||
@msg closing needs three anchors затварянето изисква три опорни точки
|
||||
@msg check an asset to export изберете актив за експортиране
|
||||
@msg check an artboard to export изберете монтажна област за експортиране
|
||||
@msg check a layer to export отметнете слой за експортиране
|
||||
@msg select the art to export first първо изберете графиката за експортиране
|
||||
@msg choice must be "merge" or "add", not `{choice}` изборът трябва да бъде "merge" или "add", а не `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2228,6 +2228,13 @@ lighting Light Svjetlo
|
||||
Use Artboards Koristi radne ploče
|
||||
Choose a folder Odaberite folder
|
||||
Create Sub-folders Kreiraj podfoldere
|
||||
Clear Selection Poništi odabir
|
||||
Export PDFs As: Izvezi PDF kao:
|
||||
Single File Jedna datoteka
|
||||
Multiple Files Više datoteka
|
||||
Format Presets Postavke formata
|
||||
Grid View Prikaz mreže
|
||||
Selected: {n}, Total number of files to export: {m} Odabrano: {n}, ukupan broj datoteka za izvoz: {m}
|
||||
Export Asset Izvezi resurs
|
||||
Export to: Izvezi u:
|
||||
Format Settings Postavke formata
|
||||
@@ -4339,6 +4346,7 @@ imageTrace Outline Obris
|
||||
@msg cannot split an open path at its end point nije moguće razdvojiti otvorenu putanju na njenoj krajnjoj tački
|
||||
@msg closing needs three anchors zatvaranje zahtijeva tri sidra
|
||||
@msg check an asset to export označite resurs za izvoz
|
||||
@msg check an artboard to export označite radnu površinu za izvoz
|
||||
@msg check a layer to export označite sloj za izvoz
|
||||
@msg select the art to export first prvo odaberite crtež za izvoz
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice mora biti "merge" ili "add", a ne `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -918,6 +918,13 @@
|
||||
Create Slice from Selection Crea un tall a partir de la selecció
|
||||
Create Slices from Guides Crea talls a partir de guies
|
||||
Create Sub-folders Crea subcarpetes
|
||||
Clear Selection Esborra la selecció
|
||||
Export PDFs As: Exporta els PDF com a:
|
||||
Single File Un sol fitxer
|
||||
Multiple Files Diversos fitxers
|
||||
Format Presets Preajustos de format
|
||||
Grid View Vista de quadrícula
|
||||
Selected: {n}, Total number of files to export: {m} Seleccionats: {n}, nombre total de fitxers per exportar: {m}
|
||||
Create Text Crea text
|
||||
Create Trim Marks Crea marques de retall
|
||||
Create: Crea:
|
||||
@@ -4885,6 +4892,7 @@
|
||||
@msg cannot split an open path at its end point no es pot dividir un traçat obert al seu punt final
|
||||
@msg check a layer to export marqueu una capa per exportar
|
||||
@msg check an asset to export marqueu un recurs per exportar
|
||||
@msg check an artboard to export marqueu una mesa de treball per exportar
|
||||
@msg choice must be "merge" or "add", not `{choice}` l’opció ha de ser "merge" o "add", no `{choice}`
|
||||
@msg Choose a circle or a perfect arc Trieu un cercle o un arc perfecte
|
||||
@msg choose a destination folder trieu una carpeta de destinació
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2257,6 +2257,13 @@ lighting Light Světlo
|
||||
Use Artboards Použít pracovní plochy
|
||||
Choose a folder Vyberte složku
|
||||
Create Sub-folders Vytvořit podsložky
|
||||
Clear Selection Vymazat výběr
|
||||
Export PDFs As: Exportovat PDF jako:
|
||||
Single File Jeden soubor
|
||||
Multiple Files Více souborů
|
||||
Format Presets Přednastavení formátů
|
||||
Grid View Zobrazení mřížky
|
||||
Selected: {n}, Total number of files to export: {m} Vybráno: {n}, celkový počet souborů k exportu: {m}
|
||||
Export Asset Exportovat prvek
|
||||
Export to: Exportovat do:
|
||||
Format Settings Nastavení formátu
|
||||
@@ -4375,6 +4382,7 @@ imageTrace Outline Obrys
|
||||
@msg cannot split an open path at its end point otevřenou cestu nelze rozdělit v jejím koncovém bodu
|
||||
@msg closing needs three anchors uzavření vyžaduje tři kotevní body
|
||||
@msg check an asset to export zaškrtněte prvek k exportu
|
||||
@msg check an artboard to export zaškrtněte kreslicí plátno k exportu
|
||||
@msg check a layer to export zaškrtněte vrstvu k exportu
|
||||
@msg select the art to export first nejprve vyberte kresbu k exportu
|
||||
@msg choice must be "merge" or "add", not `{choice}` volba musí být "merge" nebo "add", ne `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2061,6 +2061,13 @@ lighting Light Lys
|
||||
Use Artboards Brug tegnebrætter
|
||||
Choose a folder Vælg en mappe
|
||||
Create Sub-folders Opret undermapper
|
||||
Clear Selection Ryd markering
|
||||
Export PDFs As: Eksportér PDF'er som:
|
||||
Single File Én fil
|
||||
Multiple Files Flere filer
|
||||
Format Presets Formatforudindstillinger
|
||||
Grid View Gittervisning
|
||||
Selected: {n}, Total number of files to export: {m} Valgt: {n}, samlet antal filer, der eksporteres: {m}
|
||||
Export Asset Eksportér aktiv
|
||||
Export to: Eksportér til:
|
||||
Format Settings Formatindstillinger
|
||||
@@ -4067,6 +4074,7 @@ imageTrace Outline Kontur
|
||||
@msg cannot split an open path at its end point kan ikke opdele en åben kurve i dens endepunkt
|
||||
@msg closing needs three anchors lukning kræver tre ankerpunkter
|
||||
@msg check an asset to export afkryds et aktiv, der skal eksporteres
|
||||
@msg check an artboard to export afkryds en tegneflade, der skal eksporteres
|
||||
@msg choice must be "merge" or "add", not `{choice}` valget skal være "merge" eller "add", ikke `{choice}`
|
||||
@msg choose a direction to divide in vælg en retning at opdele i
|
||||
@msg choose a folder to export to vælg en mappe at eksportere til
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -642,6 +642,13 @@ font-technology Variable Variabel
|
||||
Create Report Bericht erstellen
|
||||
Create Slice Slice erstellen
|
||||
Create Sub-folders Unterordner erstellen
|
||||
Clear Selection Auswahl aufheben
|
||||
Export PDFs As: PDFs exportieren als:
|
||||
Single File Einzelne Datei
|
||||
Multiple Files Mehrere Dateien
|
||||
Format Presets Formatvorgaben
|
||||
Grid View Rasteransicht
|
||||
Selected: {n}, Total number of files to export: {m} Ausgewählt: {n}, Gesamtzahl der zu exportierenden Dateien: {m}
|
||||
Create Text Text erstellen
|
||||
Create Trim Marks Schnittmarken erstellen
|
||||
Created Erstellt
|
||||
@@ -3988,6 +3995,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg cannot split an open path at its end point ein offener Pfad kann nicht an seinem Endpunkt geteilt werden
|
||||
@msg closing needs three anchors zum Schließen sind drei Ankerpunkte nötig
|
||||
@msg check an asset to export markieren Sie ein Asset für den Export
|
||||
@msg check an artboard to export markieren Sie eine Zeichenfläche für den Export
|
||||
@msg check a layer to export wählen Sie eine Ebene für den Export
|
||||
@msg select the art to export first wählen Sie zuerst das zu exportierende Bildmaterial aus
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice muss "merge" oder "add" sein, nicht `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -623,6 +623,13 @@ font-technology Variable Μεταβλητές
|
||||
Create Report Δημιουργία αναφοράς
|
||||
Create Slice Δημιουργία τομής
|
||||
Create Sub-folders Δημιουργία υποφακέλων
|
||||
Clear Selection Απαλοιφή επιλογής
|
||||
Export PDFs As: Εξαγωγή PDF ως:
|
||||
Single File Ένα αρχείο
|
||||
Multiple Files Πολλά αρχεία
|
||||
Format Presets Προεπιλογές μορφής
|
||||
Grid View Προβολή πλέγματος
|
||||
Selected: {n}, Total number of files to export: {m} Επιλεγμένα: {n}, συνολικός αριθμός αρχείων για εξαγωγή: {m}
|
||||
Create Text Δημιουργία κειμένου
|
||||
Create Trim Marks Δημιουργία σημαδιών ξακρίσματος
|
||||
Created Δημιουργήθηκε
|
||||
@@ -4028,6 +4035,7 @@ theme Appearance Εμφάνιση
|
||||
@msg cannot split an open path at its end point δεν είναι δυνατή η διαίρεση ανοιχτής διαδρομής στο τελικό σημείο της
|
||||
@msg closing needs three anchors το κλείσιμο απαιτεί τρία σημεία αγκύρωσης
|
||||
@msg check an asset to export επιλέξτε στοιχείο για εξαγωγή
|
||||
@msg check an artboard to export επιλέξτε πινακίδα σχεδίασης για εξαγωγή
|
||||
@msg check a layer to export επιλέξτε ένα επίπεδο για εξαγωγή
|
||||
@msg select the art to export first επιλέξτε πρώτα το γραφικό για εξαγωγή
|
||||
@msg choice must be "merge" or "add", not `{choice}` η επιλογή πρέπει να είναι "merge" ή "add", όχι `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 164 and column 5.
|
@@ -663,6 +663,13 @@ font-technology Variable Variables
|
||||
Create Report Crear informe
|
||||
Create Slice Crear sector
|
||||
Create Sub-folders Crear subcarpetas
|
||||
Clear Selection Borrar selección
|
||||
Export PDFs As: Exportar PDF como:
|
||||
Single File Un solo archivo
|
||||
Multiple Files Varios archivos
|
||||
Format Presets Preajustes de formato
|
||||
Grid View Vista de cuadrícula
|
||||
Selected: {n}, Total number of files to export: {m} Seleccionados: {n}, número total de archivos para exportar: {m}
|
||||
Create Text Crear texto
|
||||
Create Trim Marks Crear marcas de recorte
|
||||
Created Creado
|
||||
@@ -4068,6 +4075,7 @@ axis Both Ambos
|
||||
@msg cannot split an open path at its end point no se puede dividir un trazado abierto en su punto final
|
||||
@msg closing needs three anchors cerrar necesita tres puntos de ancla
|
||||
@msg check an asset to export marca un recurso para exportar
|
||||
@msg check an artboard to export marca una mesa de trabajo para exportar
|
||||
@msg check a layer to export marque una capa para exportar
|
||||
@msg select the art to export first seleccione primero la ilustración que desea exportar
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice debe ser "merge" o "add", no `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2213,6 +2213,13 @@ lighting Light Valo
|
||||
Use Artboards Käytä piirtoalustoja
|
||||
Choose a folder Valitse kansio
|
||||
Create Sub-folders Luo alikansiot
|
||||
Clear Selection Tyhjennä valinta
|
||||
Export PDFs As: Vie PDF-tiedostot muodossa:
|
||||
Single File Yksi tiedosto
|
||||
Multiple Files Useita tiedostoja
|
||||
Format Presets Muotoesiasetukset
|
||||
Grid View Ruudukkonäkymä
|
||||
Selected: {n}, Total number of files to export: {m} Valittu: {n}, vietävien tiedostojen määrä yhteensä: {m}
|
||||
Export Asset Vie resurssi
|
||||
Export to: Vie kohteeseen:
|
||||
Format Settings Muotoasetukset
|
||||
@@ -4284,6 +4291,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg cannot split an open path at its end point avointa polkua ei voi jakaa sen päätepisteestä
|
||||
@msg closing needs three anchors sulkeminen vaatii kolme ankkuria
|
||||
@msg check an asset to export valitse vietävä resurssi
|
||||
@msg check an artboard to export valitse vietävä piirtoalue
|
||||
@msg check a layer to export valitse vietävä taso
|
||||
@msg select the art to export first valitse ensin vietävä kuvitus
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice-arvon on oltava "merge" tai "add", ei `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -665,6 +665,13 @@ font-technology Variable Variables
|
||||
Create Report Créer un rapport
|
||||
Create Slice Créer une tranche
|
||||
Create Sub-folders Créer des sous-dossiers
|
||||
Clear Selection Effacer la sélection
|
||||
Export PDFs As: Exporter les PDF sous :
|
||||
Single File Un seul fichier
|
||||
Multiple Files Plusieurs fichiers
|
||||
Format Presets Paramètres prédéfinis de format
|
||||
Grid View Vue en grille
|
||||
Selected: {n}, Total number of files to export: {m} Sélectionnés : {n}, nombre total de fichiers à exporter : {m}
|
||||
Create Text Créer du texte
|
||||
Create Trim Marks Créer des traits de coupe
|
||||
Created Création
|
||||
@@ -4065,6 +4072,7 @@ axis Both Les deux
|
||||
@msg cannot split an open path at its end point impossible de scinder un tracé ouvert à son extrémité
|
||||
@msg closing needs three anchors la fermeture nécessite trois points d’ancrage
|
||||
@msg check an asset to export cochez une ressource à exporter
|
||||
@msg check an artboard to export cochez un plan de travail à exporter
|
||||
@msg check a layer to export cochez un calque à exporter
|
||||
@msg select the art to export first sélectionnez d’abord l’illustration à exporter
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice doit valoir "merge" ou "add", pas `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -859,6 +859,13 @@ font-technology Variable Változó
|
||||
Create Report Jelentés készítése
|
||||
Create Slice Szelet létrehozása
|
||||
Create Sub-folders Almappák létrehozása
|
||||
Clear Selection Kijelölés törlése
|
||||
Export PDFs As: PDF-ek exportálása:
|
||||
Single File Egy fájlba
|
||||
Multiple Files Több fájlba
|
||||
Format Presets Formátum-előbeállítások
|
||||
Grid View Rácsnézet
|
||||
Selected: {n}, Total number of files to export: {m} Kijelölve: {n}, exportálandó fájlok száma összesen: {m}
|
||||
Create Text Szöveg létrehozása
|
||||
Create Trim Marks Vágójelek létrehozása
|
||||
Create: Létrehozás:
|
||||
@@ -4624,6 +4631,7 @@ imageTrace Outline Körvonal
|
||||
@msg cannot read PDF: {0} a PDF nem olvasható: {0}
|
||||
@msg cannot split an open path at its end point nyitott útvonal a végpontjánál nem vágható ketté
|
||||
@msg check an asset to export jelölj be egy exportálandó elemet
|
||||
@msg check an artboard to export jelölj be egy exportálandó rajztáblát
|
||||
@msg check a layer to export jelöljön be egy exportálandó réteget
|
||||
@msg select the art to export first először jelölje ki az exportálandó grafikát
|
||||
@msg choice must be "merge" or "add", not `{choice}` a choice értéke "merge" vagy "add" lehet, ez nem az: `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2061,6 +2061,13 @@ lighting Light Cahaya
|
||||
Use Artboards Gunakan artboard
|
||||
Choose a folder Pilih folder
|
||||
Create Sub-folders Buat subfolder
|
||||
Clear Selection Hapus pilihan
|
||||
Export PDFs As: Ekspor PDF sebagai:
|
||||
Single File Satu file
|
||||
Multiple Files Beberapa file
|
||||
Format Presets Preset format
|
||||
Grid View Tampilan kisi
|
||||
Selected: {n}, Total number of files to export: {m} Dipilih: {n}, jumlah total file yang akan diekspor: {m}
|
||||
Export Asset Ekspor aset
|
||||
Export to: Ekspor ke:
|
||||
Format Settings Pengaturan format
|
||||
@@ -4067,6 +4074,7 @@ imageTrace Outline Kontur
|
||||
@msg cannot split an open path at its end point tidak dapat membelah jalur terbuka pada titik ujungnya
|
||||
@msg closing needs three anchors penutupan memerlukan tiga jangkar
|
||||
@msg check an asset to export centang aset yang akan diekspor
|
||||
@msg check an artboard to export centang artboard yang akan diekspor
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice harus "merge" atau "add", bukan `{choice}`
|
||||
@msg choose a direction to divide in pilih arah untuk membagi
|
||||
@msg choose a folder to export to pilih folder tujuan ekspor
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2175 and column 5.
|
@@ -663,6 +663,13 @@ font-technology Variable Variabili
|
||||
Create Report Crea rapporto
|
||||
Create Slice Crea sezione
|
||||
Create Sub-folders Crea sottocartelle
|
||||
Clear Selection Cancella selezione
|
||||
Export PDFs As: Esporta PDF come:
|
||||
Single File File singolo
|
||||
Multiple Files Più file
|
||||
Format Presets Predefiniti formato
|
||||
Grid View Vista griglia
|
||||
Selected: {n}, Total number of files to export: {m} Selezionati: {n}, numero totale di file da esportare: {m}
|
||||
Create Text Crea testo
|
||||
Create Trim Marks Crea segni di taglio
|
||||
Created Creato
|
||||
@@ -4108,6 +4115,7 @@ axis Both Entrambi
|
||||
@msg cannot split an open path at its end point impossibile dividere un tracciato aperto nel suo punto finale
|
||||
@msg closing needs three anchors per chiudere servono tre punti di ancoraggio
|
||||
@msg check an asset to export spunta una risorsa da esportare
|
||||
@msg check an artboard to export spunta una tavola da disegno da esportare
|
||||
@msg check a layer to export seleziona un livello da esportare
|
||||
@msg select the art to export first seleziona prima la grafica da esportare
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice deve essere "merge" o "add", non `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1231,6 +1231,13 @@ font-technology Variable バリアブル
|
||||
Create Path パスを作成
|
||||
Create Slice スライスを作成
|
||||
Create Sub-folders サブフォルダーを作成
|
||||
Clear Selection 選択を解除
|
||||
Export PDFs As: PDF の書き出し形式:
|
||||
Single File 単一ファイル
|
||||
Multiple Files 複数ファイル
|
||||
Format Presets 形式プリセット
|
||||
Grid View グリッド表示
|
||||
Selected: {n}, Total number of files to export: {m} 選択済み:{n}、書き出すファイルの合計数:{m}
|
||||
Create Text テキストを作成
|
||||
Created 作成日
|
||||
Created: 作成日:
|
||||
@@ -4425,6 +4432,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg nothing to export: the document has no visible art (or turn on Use Artboards) 書き出すものがありません: ドキュメントに表示されているアートがありません (または「アートボードを使用」をオンにしてください)
|
||||
@msg add a format to export 書き出す形式を追加してください
|
||||
@msg check an asset to export 書き出すアセットにチェックを入れてください
|
||||
@msg check an artboard to export 書き出すアートボードにチェックを入れてください
|
||||
@msg check a layer to export 書き出すレイヤーにチェックを入れてください
|
||||
@msg select the art to export first 先に書き出すアートを選択してください
|
||||
@msg `{tool}` isn't a freehand tool `{tool}` はフリーハンドツールではありません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1961,6 +1961,13 @@ lighting Light სინათლე
|
||||
Use Artboards სამუშაო დაფების გამოყენება
|
||||
Choose a folder საქაღალდის არჩევა
|
||||
Create Sub-folders ქვესაქაღალდეების შექმნა
|
||||
Clear Selection მონიშვნის გასუფთავება
|
||||
Export PDFs As: PDF-ების ექსპორტი როგორც:
|
||||
Single File ერთი ფაილი
|
||||
Multiple Files რამდენიმე ფაილი
|
||||
Format Presets ფორმატის პრესეტები
|
||||
Grid View ბადის ხედი
|
||||
Selected: {n}, Total number of files to export: {m} არჩეულია: {n}, ექსპორტის ფაილების საერთო რაოდენობა: {m}
|
||||
Export Asset რესურსის ექსპორტირება
|
||||
Export to: ექსპორტის ადგილი:
|
||||
Format Settings ფორმატის პარამეტრები
|
||||
@@ -3851,6 +3858,7 @@ font-technology Variable ცვლადი
|
||||
@msg cannot split an open path at its end point ღია ტრაექტორიის ბოლო წერტილში გაყოფა შეუძლებელია
|
||||
@msg closing needs three anchors შეკვრას სამი საყრდენი წერტილი სჭირდება
|
||||
@msg check an asset to export მონიშნეთ ექსპორტის რესურსი
|
||||
@msg check an artboard to export მონიშნეთ ექსპორტის სახატავი დაფა
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice უნდა იყოს "merge" ან "add" და არა `{choice}`
|
||||
@msg choose a direction to divide in აირჩიეთ დაყოფის მიმართულება
|
||||
@msg choose a folder to export to აირჩიეთ ექსპორტის საქაღალდე
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1222,6 +1222,13 @@ font-technology Variable 가변
|
||||
Create Path 패스 만들기
|
||||
Create Slice 슬라이스 만들기
|
||||
Create Sub-folders 하위 폴더 만들기
|
||||
Clear Selection 선택 지우기
|
||||
Export PDFs As: PDF 내보내기 방식:
|
||||
Single File 단일 파일
|
||||
Multiple Files 여러 파일
|
||||
Format Presets 형식 사전 설정
|
||||
Grid View 격자 보기
|
||||
Selected: {n}, Total number of files to export: {m} 선택됨: {n}, 내보낼 총 파일 수: {m}
|
||||
Create Text 텍스트 만들기
|
||||
Created 만든 날짜
|
||||
Created: 만든 날짜:
|
||||
@@ -4353,6 +4360,7 @@ theme Appearance 외관
|
||||
@msg nothing to export: the document has no visible art (or turn on Use Artboards) 내보낼 항목이 없습니다: 문서에 보이는 아트웍이 없습니다(또는 아트보드 사용을 켜세요)
|
||||
@msg add a format to export 내보낼 형식을 추가하세요
|
||||
@msg check an asset to export 내보낼 자산을 체크하세요
|
||||
@msg check an artboard to export 내보낼 대지를 체크하세요
|
||||
@msg check a layer to export 내보낼 레이어를 선택하십시오
|
||||
@msg select the art to export first 먼저 내보낼 아트를 선택하십시오
|
||||
@msg `{tool}` isn't a freehand tool `{tool}`은(는) 프리핸드 도구가 아닙니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2075,6 +2075,13 @@ lighting Light Licht
|
||||
Use Artboards Tekenborden gebruiken
|
||||
Choose a folder Kies een map
|
||||
Create Sub-folders Submappen maken
|
||||
Clear Selection Selectie wissen
|
||||
Export PDFs As: PDF's exporteren als:
|
||||
Single File Eén bestand
|
||||
Multiple Files Meerdere bestanden
|
||||
Format Presets Indelingsvoorinstellingen
|
||||
Grid View Rasterweergave
|
||||
Selected: {n}, Total number of files to export: {m} Geselecteerd: {n}, totaal aantal te exporteren bestanden: {m}
|
||||
Export Asset Asset exporteren
|
||||
Export to: Exporteren naar:
|
||||
Format Settings Indelingsinstellingen
|
||||
@@ -4081,6 +4088,7 @@ imageTrace Outline Contour
|
||||
@msg cannot split an open path at its end point kan een open pad niet splitsen op zijn eindpunt
|
||||
@msg closing needs three anchors voor sluiten zijn drie ankers nodig
|
||||
@msg check an asset to export vink een asset aan om te exporteren
|
||||
@msg check an artboard to export vink een tekengebied aan om te exporteren
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice moet "merge" of "add" zijn, niet `{choice}`
|
||||
@msg choose a direction to divide in kies een richting om in te verdelen
|
||||
@msg choose a folder to export to kies een map om naar te exporteren
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2174,6 +2174,13 @@ lighting Light Światło
|
||||
Use Artboards Użyj obszarów kompozycji
|
||||
Choose a folder Wybierz folder
|
||||
Create Sub-folders Utwórz podfoldery
|
||||
Clear Selection Wyczyść zaznaczenie
|
||||
Export PDFs As: Eksportuj pliki PDF jako:
|
||||
Single File Jeden plik
|
||||
Multiple Files Wiele plików
|
||||
Format Presets Ustawienia formatów
|
||||
Grid View Widok siatki
|
||||
Selected: {n}, Total number of files to export: {m} Zaznaczono: {n}, łączna liczba plików do eksportu: {m}
|
||||
Export Asset Eksportuj zasób
|
||||
Export to: Eksportuj do:
|
||||
Format Settings Ustawienia formatu
|
||||
@@ -4199,6 +4206,7 @@ imageTrace Outline Kontur
|
||||
@msg cannot split an open path at its end point nie można podzielić otwartej ścieżki w jej punkcie końcowym
|
||||
@msg closing needs three anchors zamknięcie wymaga trzech punktów
|
||||
@msg check an asset to export zaznacz zasób do eksportu
|
||||
@msg check an artboard to export zaznacz obszar kompozycji do eksportu
|
||||
@msg check a layer to export zaznacz warstwę do eksportu
|
||||
@msg select the art to export first najpierw zaznacz grafikę do eksportu
|
||||
@msg choice must be "merge" or "add", not `{choice}` wybór musi mieć wartość "merge" lub "add", nie `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -585,6 +585,13 @@
|
||||
Create Path Criar Caminho
|
||||
Create Slice Criar Fatia
|
||||
Create Sub-folders Criar Subpastas
|
||||
Clear Selection Limpar Seleção
|
||||
Export PDFs As: Exportar PDFs Como:
|
||||
Single File Arquivo Único
|
||||
Multiple Files Vários Arquivos
|
||||
Format Presets Predefinições de Formato
|
||||
Grid View Exibição em Grade
|
||||
Selected: {n}, Total number of files to export: {m} Selecionados: {n}, número total de arquivos a exportar: {m}
|
||||
Create Text Criar Texto
|
||||
Create Trim Marks Criar Marcas de Corte
|
||||
Create from Guides Criar a partir de Guias
|
||||
@@ -3646,6 +3653,7 @@ axis Both Ambos
|
||||
@msg cannot split an open path at its end point não é possível dividir um traçado aberto em sua extremidade
|
||||
@msg closing needs three anchors fechar exige três âncoras
|
||||
@msg check an asset to export marque um recurso para exportar
|
||||
@msg check an artboard to export marque uma prancheta para exportar
|
||||
@msg check a layer to export marque uma camada para exportar
|
||||
@msg select the art to export first selecione primeiro a arte a exportar
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice deve ser "merge" ou "add", não `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -661,6 +661,13 @@ font-technology Variable Вариативные
|
||||
Create Report Создать отчёт
|
||||
Create Slice Создать фрагмент
|
||||
Create Sub-folders Создать вложенные папки
|
||||
Clear Selection Снять выделение
|
||||
Export PDFs As: Экспортировать PDF как:
|
||||
Single File Один файл
|
||||
Multiple Files Несколько файлов
|
||||
Format Presets Наборы форматов
|
||||
Grid View Сетка
|
||||
Selected: {n}, Total number of files to export: {m} Выбрано: {n}, всего файлов для экспорта: {m}
|
||||
Create Text Создать текст
|
||||
Create Trim Marks Создать метки подрезки
|
||||
Created Создано
|
||||
@@ -4104,6 +4111,7 @@ axis Both Оба
|
||||
@msg cannot split an open path at its end point нельзя разделить открытый контур в его конечной точке
|
||||
@msg closing needs three anchors для замыкания нужны три опорные точки
|
||||
@msg check an asset to export отметьте ресурс для экспорта
|
||||
@msg check an artboard to export отметьте монтажную область для экспорта
|
||||
@msg check a layer to export отметьте слой для экспорта
|
||||
@msg select the art to export first сначала выделите объекты для экспорта
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice должно быть "merge" или "add", а не `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -651,6 +651,13 @@ font-technology Variable Variabla
|
||||
Create Report Skapa rapport
|
||||
Create Slice Skapa skiva
|
||||
Create Sub-folders Skapa undermappar
|
||||
Clear Selection Rensa markering
|
||||
Export PDFs As: Exportera PDF-filer som:
|
||||
Single File En fil
|
||||
Multiple Files Flera filer
|
||||
Format Presets Formatförinställningar
|
||||
Grid View Rutnätsvy
|
||||
Selected: {n}, Total number of files to export: {m} Markerade: {n}, totalt antal filer att exportera: {m}
|
||||
Create Text Skapa text
|
||||
Create Trim Marks Skapa skärmärken
|
||||
Created Skapad
|
||||
@@ -3979,6 +3986,7 @@ axis Both Båda
|
||||
@msg cannot split an open path at its end point kan inte dela en öppen bana vid dess slutpunkt
|
||||
@msg closing needs three anchors att sluta banan kräver tre ankarpunkter
|
||||
@msg check an asset to export kryssa för en resurs att exportera
|
||||
@msg check an artboard to export kryssa för en rityta att exportera
|
||||
@msg check a layer to export markera ett lager att exportera
|
||||
@msg select the art to export first markera först teckningen som ska exporteras
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice måste vara "merge" eller "add", inte `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -643,6 +643,13 @@ font-technology Variable Değişken
|
||||
Create Report Rapor Oluştur
|
||||
Create Slice Dilim Oluştur
|
||||
Create Sub-folders Alt Klasörler Oluşturun
|
||||
Clear Selection Seçimi Temizle
|
||||
Export PDFs As: PDF'leri Şu Şekilde Dışa Aktar:
|
||||
Single File Tek Dosya
|
||||
Multiple Files Birden Fazla Dosya
|
||||
Format Presets Biçim Ön Ayarları
|
||||
Grid View Izgara Görünümü
|
||||
Selected: {n}, Total number of files to export: {m} Seçilen: {n}, dışa aktarılacak toplam dosya sayısı: {m}
|
||||
Create Text Metin Oluştur
|
||||
Create Trim Marks Kesim İşaretleri Oluşturun
|
||||
Created Oluşturuldu
|
||||
@@ -3848,6 +3855,7 @@ axis Both Her İkisi
|
||||
@msg cannot split an open path at its end point açık bir yolu bitiş noktasında bölemez
|
||||
@msg closing needs three anchors Kapanış üç çapaya ihtiyaç duyar
|
||||
@msg check an asset to export dışa aktarılacak bir varlığı kontrol etme
|
||||
@msg check an artboard to export dışa aktarılacak bir çalışma yüzeyi işaretleyin
|
||||
@msg check a layer to export dışa aktarılacak katmanı kontrol edin
|
||||
@msg select the art to export first önce dışa aktarılacak resmi seçin
|
||||
@msg choice must be "merge" or "add", not `{choice}` seçim "{choice}" değil "birleştir" veya "ekle" olmalıdır
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -657,6 +657,13 @@ font-technology Variable Варіативні
|
||||
Create Report Створити звіт
|
||||
Create Slice Створити фрагмент
|
||||
Create Sub-folders Створити вкладені папки
|
||||
Clear Selection Зняти виділення
|
||||
Export PDFs As: Експортувати PDF як:
|
||||
Single File Один файл
|
||||
Multiple Files Кілька файлів
|
||||
Format Presets Набори форматів
|
||||
Grid View Сітка
|
||||
Selected: {n}, Total number of files to export: {m} Вибрано: {n}, усього файлів для експорту: {m}
|
||||
Create Text Створити текст
|
||||
Create Trim Marks Створити мітки підрізування
|
||||
Created Створено
|
||||
@@ -4092,6 +4099,7 @@ axis Both Обидві
|
||||
@msg cannot split an open path at its end point не можна розділити відкритий контур у його кінцевій точці
|
||||
@msg closing needs three anchors для замикання потрібні три опорні точки
|
||||
@msg check an asset to export позначте ресурс для експорту
|
||||
@msg check an artboard to export позначте монтажну область для експорту
|
||||
@msg check a layer to export позначте шар для експорту
|
||||
@msg select the art to export first спочатку виділіть об’єкти для експорту
|
||||
@msg choice must be "merge" or "add", not `{choice}` choice має бути "merge" або "add", а не `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 178 and column 5.
|
@@ -760,6 +760,13 @@ font-technology Variable Biến thiên
|
||||
Create Report Tạo báo cáo
|
||||
Create Slice Tạo lát cắt
|
||||
Create Sub-folders Tạo thư mục con
|
||||
Clear Selection Xóa lựa chọn
|
||||
Export PDFs As: Xuất PDF dưới dạng:
|
||||
Single File Một tệp
|
||||
Multiple Files Nhiều tệp
|
||||
Format Presets Thiết lập sẵn định dạng
|
||||
Grid View Chế độ lưới
|
||||
Selected: {n}, Total number of files to export: {m} Đã chọn: {n}, tổng số tệp sẽ xuất: {m}
|
||||
Create Text Tạo văn bản
|
||||
Create Trim Marks Tạo dấu cắt xén
|
||||
Create: Tạo:
|
||||
@@ -4274,6 +4281,7 @@ theme Appearance Giao diện
|
||||
@msg cannot read PDF: {0} không thể đọc PDF: {0}
|
||||
@msg cannot split an open path at its end point không thể tách đường dẫn mở tại điểm cuối
|
||||
@msg check an asset to export hãy chọn một asset để xuất
|
||||
@msg check an artboard to export hãy chọn một bảng vẽ để xuất
|
||||
@msg check a layer to export hãy đánh dấu một lớp để xuất
|
||||
@msg select the art to export first hãy chọn hình cần xuất trước
|
||||
@msg choice must be "merge" or "add", not `{choice}` lựa chọn phải là "merge" hoặc "add", không phải `{choice}`
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 200 and column 5.
|
@@ -608,6 +608,13 @@
|
||||
Create Report 创建报告
|
||||
Create Slice 创建切片
|
||||
Create Sub-folders 创建子文件夹
|
||||
Clear Selection 清除选择
|
||||
Export PDFs As: PDF 导出为:
|
||||
Single File 单个文件
|
||||
Multiple Files 多个文件
|
||||
Format Presets 格式预设
|
||||
Grid View 网格视图
|
||||
Selected: {n}, Total number of files to export: {m} 已选择:{n},要导出的文件总数:{m}
|
||||
Create Text 创建文字
|
||||
Create Trim Marks 创建裁切标记
|
||||
Created 创建时间
|
||||
@@ -4148,6 +4155,7 @@ imageTrace Outline 轮廓
|
||||
@msg nothing to export: the document has no visible art (or turn on Use Artboards) 没有可导出的内容:文档中没有可见的图稿 (或请打开“使用画板”)
|
||||
@msg add a format to export 请添加要导出的格式
|
||||
@msg check an asset to export 请勾选要导出的资源
|
||||
@msg check an artboard to export 请勾选要导出的画板
|
||||
@msg `{tool}` isn't a freehand tool `{tool}` 不是手绘工具
|
||||
@msg bad colour `{hex}` (#rrggbb) 颜色 `{hex}` 无效 (#rrggbb)
|
||||
@msg no gradient stop or freeform point is selected 未选择渐变色标或任意形状点
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -625,6 +625,13 @@ font-technology Variable 可變
|
||||
Create Report 建立報告
|
||||
Create Slice 建立切片
|
||||
Create Sub-folders 建立子資料夾
|
||||
Clear Selection 清除選取
|
||||
Export PDFs As: PDF 轉存為:
|
||||
Single File 單一檔案
|
||||
Multiple Files 多個檔案
|
||||
Format Presets 格式預設集
|
||||
Grid View 格點檢視
|
||||
Selected: {n}, Total number of files to export: {m} 已選取:{n},要轉存的檔案總數:{m}
|
||||
Create Text 建立文字
|
||||
Create Trim Marks 建立剪裁標記
|
||||
Created 建立時間
|
||||
@@ -2088,6 +2095,7 @@ file-filter Templates 範本
|
||||
@msg nothing to export: the document has no visible art (or turn on Use Artboards) 沒有可匯出的內容:文件中沒有可見的圖稿 (或請開啟「使用工作區域」)
|
||||
@msg add a format to export 請新增要匯出的格式
|
||||
@msg check an asset to export 請勾選要匯出的資產
|
||||
@msg check an artboard to export 請勾選要匯出的工作區域
|
||||
@msg `{tool}` isn't a freehand tool `{tool}` 不是手繪工具
|
||||
@msg bad colour `{hex}` (#rrggbb) 顏色 `{hex}` 無效 (#rrggbb)
|
||||
@msg no gradient stop or freeform point is selected 未選取漸層色標或任意形狀點
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -285,6 +285,8 @@ fn report_exported(app: &mut VectorcraftApp, r: Value, folder: &str, open_locati
|
||||
}
|
||||
app.status(status);
|
||||
} else if let Some(download) = app.services.download.as_mut() {
|
||||
// A browser download has no folders: a file from a sub-folder (`2x/Logo@2x.png`) is named alone.
|
||||
let returned: Vec<(&str, &str)> = returned.iter().map(|(name, data)| (name.rsplit('/').next().unwrap_or(name), *data)).collect();
|
||||
for (name, data) in &returned {
|
||||
let bytes = vectorcraft_format::base64_decode(data).ok_or("the export returned unreadable data")?;
|
||||
download(name, &bytes)?;
|
||||
|
||||
@@ -263,7 +263,7 @@ fn settings(ui: &mut Ui, st: &DocState) -> (Option<Settings>, usize) {
|
||||
let preset = preset.unwrap_or_default().to_string();
|
||||
let n = rows.len();
|
||||
let mut d = Dialog::new(EXPORT_FOR_SCREENS, json!({ "formats": &rows, "preset": preset, "__settings": "" }));
|
||||
screen_formats(ui, &mut d);
|
||||
screen_formats(ui, &mut d, None);
|
||||
let (shown, picked) = (d.str("__settings"), d.str("preset"));
|
||||
let formats = d.fields.remove("formats").unwrap_or(Value::Null);
|
||||
let change = if !shown.is_empty() {
|
||||
|
||||
@@ -209,7 +209,7 @@ fn the_rows_are_the_shared_export_settings() {
|
||||
// A preset set by an agent shows as its rows.
|
||||
app.run("assets.settings.set", json!({"preset": "mobile"})).unwrap();
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
assert!(has(&texts, "Mobile 1x/2x/3x") && has(&texts, "@3x"));
|
||||
assert!(has(&texts, "iOS") && has(&texts, "@3x"));
|
||||
// Export (nothing selected: every asset) downloads one zip on the web.
|
||||
let texts = frame(&ctx, &mut app, vec![], false);
|
||||
frame(&ctx, &mut app, click(at(&texts, "Export")), false);
|
||||
|
||||
+3
-2
@@ -3987,8 +3987,9 @@ each file deflated, or stored when deflating does not make it smaller.
|
||||
A row is `{format, scale?}` where `scale` is a factor (`2`, `"2x"`), a pixel width (`"100w"` or `width: 100`), a
|
||||
height (`"100h"`) or a resolution (`"72ppi"`); raster rows name their files `@2x`, `@100w`… unless `suffix` says
|
||||
otherwise. `fullDocument` writes one file per row instead (a PDF of every artboard, other formats the bounds of all
|
||||
art), `includeBleed` grows each artboard by the document's bleed, `subfolders` puts each row's files in a sub-folder
|
||||
(its size or format, or its own `folder`), `preset: "mobile"` or `"density"` (Android-style ldpi…xxxhdpi
|
||||
art), `includeBleed` grows each artboard by the document's bleed, `pdfs: "single"` (Export PDFs As › Single file)
|
||||
writes the artboards a PDF row exports as the pages of one PDF named after the document, `subfolders` puts each
|
||||
row's files in a sub-folder (its size or format, or its own `folder`; `folders: "format"` sorts by format instead), `preset: "mobile"` or `"density"` (Android-style ldpi…xxxhdpi
|
||||
sub-folders) replaces the rows, and `settings: {png: {…}, jpg: {…}, svg: {…}, pdf: {preset}}` gives every row of a
|
||||
format its options. On every platform, `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters from a
|
||||
prefix, a suffix, a name pattern or an item's name become `-` in file and folder names, a folder name ends without
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Artwork Cleanup add-on: Join Point Text, #4826; scores unchanged; previously: G13: Direct Selection snaps a dragged line end square to straight paths and grazing curves; Path Precision Tools add-on: Shared Edge Merge Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Export for Screens laid out as Illustrator's, with Export PDFs As and sub-folders by scale or format; previously: Artwork Cleanup add-on: Join Point Text, #4826; scores unchanged; previously: G13: Direct Selection snaps a dragged line end square to straight paths and grazing curves; Path Precision Tools add-on: Shared Edge Merge Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -19,6 +19,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | Export for Screens is laid out as Illustrator's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged. |
|
||||
| 2026-10-31 | minor | Artwork Cleanup add-on: Join Point Text (`addon.cleanup.joinText`) joins stacked one-line point type into one object, a paragraph per line keeping its baseline (#4826); scores unchanged. |
|
||||
| 2026-10-31 | minor | Width tool: a stroke with a width profile keeps its shape while the view zooms out; the minimum visible width goes to its widest point instead of scaling the whole profile (#995); scores unchanged. |
|
||||
| 2026-10-31 | minor | Patterns: Grid Repeat Options has a Preview checkbox, as the public grid options page lists (#2651); scores unchanged. |
|
||||
@@ -542,7 +543,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`.
|
||||
- Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command.
|
||||
- Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used.
|
||||
- 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 for Screens is laid out as Illustrator'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, 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`).
|
||||
- Variables (data merge): text, visibility and image variables bound to objects (an object may carry several), dataset rows applied in one undo step (`variable.*`, `dataset.*`) from a Variables panel and Window › Variables, with dialogs to define, bind and edit, Make Text/Visibility/Image Dynamic, Capture/Update Data Set, CSV import/export (`dataset.import` / `dataset.export`), and a row in italics when an edit leaves the art not matching it; an image dataset value is a file path and apply keeps the object's bounds; bindings let go of deleted art and locked art is left alone; every command, like the rest, is drivable over MCP and the CLI.
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Properties: Blend Options in Quick Actions for one selected blend with any tool, #4817; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Export for Screens laid out as Illustrator's; previously: Properties: Blend Options in Quick Actions for one selected blend with any tool, #4817; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -376,6 +376,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-31 | minor | Export for Screens is laid out as Illustrator's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged |
|
||||
| 2026-10-31 | minor | Properties: Quick Actions has Blend Options for one selected blend with any tool, not only the Blend tool, as the blending page describes (#4817); scores unchanged |
|
||||
| 2026-10-31 | minor | Smart guides and snapping: Snap to Perpendicular and Snap to Tangent for an existing line's end dragged with Direct Selection (#2530), so the tangent and perpendicular row is done; scores and estimates unchanged |
|
||||
| 2026-10-31 | minor | Grid Repeat Options has a Preview checkbox (#2651); scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user