Export for Screens: the dialog is laid out as Illustrator's

This commit is contained in:
bflatastic
2026-10-31 21:43:13 +07:00
parent b8e5f7088f
commit 7a2acbc178
33 changed files with 673 additions and 555 deletions
+349 -391
View File
@@ -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.
+103 -154
View File
@@ -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);
}
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+8
View File
@@ -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.
+2
View File
@@ -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)?;
+1 -1
View File
@@ -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);