diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs
index fdbec59bfc..3397ada7a5 100644
--- a/apps/vectorcraft/src/main.rs
+++ b/apps/vectorcraft/src/main.rs
@@ -373,11 +373,14 @@ fn main() -> std::process::ExitCode {
// Display Server › X11 opens the window through XWayland instead, where file drops work (#1151),
// and the other choices do so when a pen is attached, where the pen draws (#2028, #4967).
#[cfg(target_os = "linux")]
+ let mut forced_to_x11 = false;
+ #[cfg(target_os = "linux")]
let options = {
let mut options = options;
let pref = saved.as_ref().and_then(|ui| ui.engine_prefs.get("linuxDisplayServer")).and_then(serde_json::Value::as_str);
if xwayland::wanted(pref, |name| std::env::var_os(name), xwayland::pen_attached) {
if xwayland::x11_libraries_found() {
+ forced_to_x11 = true;
if pref != Some("x11") {
log::info!("pen found: opening the window through XWayland, where the pen works (VECTORCRAFT_NATIVE_WAYLAND=1 keeps Wayland)");
} else {
@@ -393,6 +396,11 @@ fn main() -> std::process::ExitCode {
}
options
};
+ #[cfg(not(target_os = "linux"))]
+ let forced_to_x11 = false;
+ // Preferences › User Interface notes under System Title Bar when the desktop draws the frame
+ // with it on or off.
+ let desktop_keeps_frame = titlebar::desktop_keeps_frame(forced_to_x11);
// With Windows Ink, winit reports a pen as a touch and drops its barrel button and which end
// is down; the message hook notes each pointer contact and whether it starts with the barrel
// button held, and writes the pen's end into the pen feed (`windows_stylus`). It also notes the
@@ -595,6 +603,7 @@ fn main() -> std::process::ExitCode {
app.custom_titlebar = custom_titlebar;
app.caption_layout = caption_layout;
app.desktop_frame = !custom_titlebar;
+ app.desktop_keeps_frame = desktop_keeps_frame;
if let Some(port) = control_port {
let rx = control_server::start(port, cc.egui_ctx.clone());
app = app.with_control(rx).with_automation_roots(roots.clone());
diff --git a/apps/vectorcraft/src/tests.rs b/apps/vectorcraft/src/tests.rs
index 99ca880bc9..1994569c0c 100644
--- a/apps/vectorcraft/src/tests.rs
+++ b/apps/vectorcraft/src/tests.rs
@@ -346,6 +346,34 @@ fn the_titlebar_variable_decides_the_frame_before_the_window_opens() {
}
}
+/// Preferences says under System Title Bar that the desktop draws the frame with it on or off
+/// exactly where the start frame without VectorCraft Title Bar on Every Desktop is the desktop's
+/// with System Title Bar off as well as on and `VECTORCRAFT_TITLEBAR` is unset: Linux and the BSDs
+/// only.
+#[test]
+fn the_desktop_keeps_the_frame_where_system_title_bar_does_not_change_it() {
+ use crate::titlebar::desktop_keeps_frame_from;
+ use craft_window::{Frame, Layout};
+ let linux = cfg!(not(any(target_os = "macos", target_os = "windows")));
+ for probed in [Frame::Desktop, Frame::App(Layout::default())] {
+ for forced_to_x11 in [false, true] {
+ let frame = super::start_frame(probed.clone(), None, forced_to_x11, false);
+ let either_way = !super::custom_titlebar_from(&frame, false) && !super::custom_titlebar_from(&frame, true);
+ assert_eq!(desktop_keeps_frame_from(&probed, None, forced_to_x11), linux && either_way, "{probed:?} {forced_to_x11}");
+ }
+ }
+ // KDE, Sway and X11, and GNOME through XWayland, keep the desktop's frame; GNOME on Wayland does not.
+ assert_eq!(desktop_keeps_frame_from(&Frame::Desktop, None, false), linux);
+ assert_eq!(desktop_keeps_frame_from(&Frame::App(Layout::default()), None, true), linux);
+ assert!(!desktop_keeps_frame_from(&Frame::App(Layout::default()), None, false));
+ // While `VECTORCRAFT_TITLEBAR` chooses the frame, System Title Bar does not apply and there is no note.
+ for (frame, overridden) in [(Frame::App(Layout::default()), Some(true)), (Frame::Desktop, Some(false))] {
+ for forced_to_x11 in [false, true] {
+ assert!(!desktop_keeps_frame_from(&frame, overridden, forced_to_x11), "{overridden:?} {forced_to_x11}");
+ }
+ }
+}
+
/// The file extensions the macOS bundle declares: its document types and its own exported type.
fn plist_extensions(plist: &str) -> Vec<&str> {
["CFBundleTypeExtensions ", "public.filename-extension "]
diff --git a/apps/vectorcraft/src/titlebar.rs b/apps/vectorcraft/src/titlebar.rs
index 88dce26897..cb98fa2648 100644
--- a/apps/vectorcraft/src/titlebar.rs
+++ b/apps/vectorcraft/src/titlebar.rs
@@ -191,6 +191,25 @@ pub(crate) fn custom_titlebar(saved: Option<&vectorcraft_ui_egui::UiState>) -> (
(custom_titlebar_from(&frame, opted_into_system), caption_layout_from(&frame))
}
+/// Whether the desktop would draw this window's frame with System Title Bar on or off while
+/// VectorCraft Title Bar on Every Desktop is off: `VECTORCRAFT_TITLEBAR` is not set to `app`,
+/// `system` or `desktop` (`overridden` is `None`), and the probe chose
+/// [`craft_window::Frame::Desktop`] or the window opens through XWayland (`forced_to_x11`)
+/// ([`app_draws_frame`]). The saved VectorCraft Title Bar on Every Desktop is left out, so the value
+/// stays `true` on such a desktop while that preference gives the app the frame. Always `false` on
+/// macOS and Windows, where System Title Bar decides the frame by itself.
+pub(crate) fn desktop_keeps_frame_from(probed: &craft_window::Frame, overridden: Option, forced_to_x11: bool) -> bool {
+ let probed_app = matches!(probed, craft_window::Frame::App(_));
+ cfg!(not(any(target_os = "macos", target_os = "windows"))) && overridden.is_none() && !app_draws_frame(probed_app, None, forced_to_x11, false)
+}
+
+/// [`desktop_keeps_frame_from`] for this start's probe, given whether the launch path opens the
+/// window through XWayland.
+pub(crate) fn desktop_keeps_frame(forced_to_x11: bool) -> bool {
+ let choice = probed_choice();
+ desktop_keeps_frame_from(choice.frame(), choice.overridden(), forced_to_x11)
+}
+
pub(crate) fn native_options(custom_titlebar: bool) -> eframe::NativeOptions {
eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
diff --git a/crates/brush/src/colorize.rs b/crates/brush/src/colorize.rs
index 2cf9f43c97..00edeb3113 100644
--- a/crates/brush/src/colorize.rs
+++ b/crates/brush/src/colorize.rs
@@ -114,51 +114,9 @@ fn strip_brushes(n: &mut Node) {
}
}
-/// Mix every solid paint under `n` towards `color` by `amount` (0..1) — the Symbol Stainer.
-pub fn tint_node(n: &mut Node, color: &Color, amount: f32) {
- let amount = amount.clamp(0.0, 1.0);
- for it in &mut n.appearance.items {
- let p = match it {
- AppearanceItem::Fill(f) => &mut f.paint,
- AppearanceItem::Stroke(s) => &mut s.paint,
- };
- if let Paint::Solid { color: c, .. } = p {
- *p = Paint::solid(c.lerp(color, amount));
- }
- }
- if let vectorcraft_doc::NodeKind::Text(t) = &mut n.kind {
- for r in &mut t.runs {
- for p in [&mut r.style.fill, &mut r.style.stroke] {
- if let Paint::Solid { color: c, .. } = p {
- *p = Paint::solid(c.lerp(color, amount));
- }
- }
- }
- }
- if let Some(ch) = n.children_mut() {
- for c in ch.iter_mut() {
- tint_node(Arc::make_mut(c), color, amount);
- }
- }
-}
-
-/// The stain instance `inst` puts on its symbol's art: its visible fill colour and that fill's
-/// opacity (how far the art mixes towards it). `None` for an unstained instance, including one
-/// whose fill has an opacity of 0, so its art keeps its paints and their swatch links.
-pub fn stain(inst: &Node) -> Option<(Color, f32)> {
- let f = inst.appearance.fill().filter(|f| f.visible && f.opacity > 0.0)?;
- Some((f.paint.color()?, f.opacity))
-}
-
-/// A symbol's art for instance `inst`: with the instance's own appearance changes to it (a dynamic
-/// symbol's, [`vectorcraft_doc::symbol_overrides`]), stained by the instance's fill ([`stain`]).
-pub fn instance_art(art: &Node, inst: &Node) -> Node {
- let mut a = vectorcraft_doc::symbol_overrides::dynamic_art(art, inst);
- if let Some((c, amount)) = stain(inst) {
- tint_node(&mut a, &c, amount);
- }
- a
-}
+/// The Symbol Stainer's mix, an instance's stain and its stained art, from
+/// [`vectorcraft_doc::symbol_overrides`], which the effects that reshape an instance also read.
+pub use vectorcraft_doc::symbol_overrides::{instance_art, stain, tint_node};
/// The solid colours of the fills and strokes under `nodes`, most used first (then in the order
/// they first appear): the colours Brush Options offers as Hue Shift's key colour, the first being
diff --git a/crates/cad/src/scene.rs b/crates/cad/src/scene.rs
index 4860e4989a..ea3c962053 100644
--- a/crates/cad/src/scene.rs
+++ b/crates/cad/src/scene.rs
@@ -683,7 +683,7 @@ impl<'a> Scene<'a> {
{
// Symbols holding instances of symbols are written in full for each instance,
// empty or not: every instance's art counts against one budget for the export.
- let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
+ let art = vectorcraft_doc::symbol_overrides::instance_art(&sym.art, n);
let count = art.count_with_masks() as u64;
// Counted as `place_symbol_art_unclipped` places it: no clip groups.
let most = doc.symbol_slice_map(symbol, *xf).map_or(count, |m| m.most_placed_unclipped(&art, doc));
diff --git a/crates/cad/src/tests.rs b/crates/cad/src/tests.rs
index 96c037a4a7..a14af8d929 100644
--- a/crates/cad/src/tests.rs
+++ b/crates/cad/src/tests.rs
@@ -1395,6 +1395,21 @@ fn crop_keeps_an_instance_whose_art_reaches_the_region() {
assert!(!kinds(&export(&d, &o).unwrap()).is_empty());
}
+/// An instance stained blue by its fill writes its symbol's red art in blue, as the canvas draws it.
+#[test]
+fn a_stained_instance_is_written_in_its_stained_color() {
+ let mut d = doc_with(|d| {
+ let mut n = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Mark".into(), xf: Affine::translate((30.0, 30.0)) });
+ n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0).into();
+ vec![n]
+ });
+ let art = path(&mut d, shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 40.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
+ d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Mark".into(), art: Arc::new(art) });
+ let out = export(&d, &opts(&d)).unwrap();
+ let colors: Vec> = entities(&out).iter().map(|e| get(e, 420).map(str::to_string)).collect();
+ assert!(!colors.is_empty() && colors.iter().all(|c| c.as_deref() == Some("255")), "{colors:?}");
+}
+
#[test]
fn symbols_fanning_out_into_empty_symbols_stop_at_the_symbol_budget() {
// Five levels of 100 instances of the level below, ending in an empty symbol: 10^10 empty
diff --git a/crates/doc/src/orient.rs b/crates/doc/src/orient.rs
index e524d6ca37..a2ff76d83f 100644
--- a/crates/doc/src/orient.rs
+++ b/crates/doc/src/orient.rs
@@ -3,6 +3,8 @@
//! square to it: scaling by a side handle stretches it along its own width or height. Path
//! geometry is still baked; Object ▸ Transform ▸ Reset Bounding Box sets the angle back to 0.
+use std::sync::Arc;
+
use vectorcraft_geom::{Affine, Point, Rect, Vec2, normalize_deg, union_opt};
use crate::{Document, Node, NodeId, NodeKind, SymbolBoxes};
@@ -84,7 +86,35 @@ pub fn transformed_angle(angle: f64, a: Affine) -> f64 {
}
}
+/// The turn (counter-clockwise degrees) from the boxes of art placed by a symbol instance's
+/// transform `xf` to the instance's own box at `angle`: 0 while the box turns with the transform,
+/// otherwise the change Reset Bounding Box made to it. Under a mirroring `xf` the instance's box can
+/// stand 180° from the one `xf` gives in one step ([`transformed_angle`] keeps the angle nearer the
+/// one before at each step); the turn is then 180°, so the art's boxes match the instance's.
+pub fn placed_turn(angle: f64, xf: Affine) -> f64 {
+ let turn = normalize_deg(angle - transformed_angle(0.0, xf));
+ if turn.is_finite() && turn.abs() > EPS_DEG { turn } else { 0.0 }
+}
+
impl Node {
+ /// Turns the box of this object, and of every object that turns with it ([`Node::transform`]:
+ /// its children and the art of a linked opacity mask), by `by` degrees counter-clockwise.
+ /// Layers have no box.
+ pub fn turn_boxes(&mut self, by: f64) {
+ if !self.is_layer() {
+ let a = normalize_deg(self.bbox_angle + by);
+ self.bbox_angle = if a.abs() <= EPS_DEG { 0.0 } else { a };
+ }
+ for c in self.children_mut().into_iter().flatten() {
+ Arc::make_mut(c).turn_boxes(by);
+ }
+ if let Some(m) = self.extras_if_any().and_then(|e| e.mask.as_mut())
+ && m.linked
+ {
+ Arc::make_mut(&mut m.art).turn_boxes(by);
+ }
+ }
+
/// Bounds of this object as `a` would place it (geometric, or `visual` with its strokes),
/// without changing it, symbol instances measured by their art from `symbols`
/// ([`Node::geometric_bounds_with`]).
@@ -185,6 +215,44 @@ mod tests {
assert_eq!(transformed_angle(30.0, Affine::scale(0.0)), 30.0);
}
+ #[test]
+ fn placed_turn_is_what_reset_bounding_box_took_off_the_instance() {
+ let xf = Affine::translate((40.0, 10.0)) * rot(20.0) * Affine::scale_non_uniform(2.0, 0.5);
+ // The box turns with the transform: no turn.
+ assert_eq!(placed_turn(transformed_angle(0.0, xf), xf), 0.0);
+ assert_eq!(placed_turn(0.0, Affine::scale(3.0)), 0.0);
+ // Reset to 0: the placed art turns back by the 20°.
+ assert!(close(placed_turn(0.0, xf), -20.0));
+ // Mirrored: turned 170° and then flipped, the instance's box stands 180° from the box the
+ // whole transform gives in one step, and the art turns the half turn to match it.
+ let flip = Affine::scale_non_uniform(1.0, -1.0);
+ let flipped = flip * rot(170.0);
+ let stepwise = transformed_angle(transformed_angle(0.0, rot(170.0)), flip);
+ assert!(close(stepwise, -170.0) && close(transformed_angle(0.0, flipped), 10.0));
+ assert!(close(placed_turn(stepwise, flipped).abs(), 180.0));
+ assert!(close(normalize_deg(placed_turn(0.0, flipped) + transformed_angle(0.0, flipped)), 0.0));
+ // Degenerate and non-finite transforms turn nothing.
+ assert_eq!(placed_turn(0.0, Affine::scale(0.0)), 0.0);
+ assert_eq!(placed_turn(f64::NAN, xf), 0.0);
+ }
+
+ #[test]
+ fn turn_boxes_turns_children_and_linked_mask_art_but_not_layers() {
+ let mut d = Document::new(500.0, 500.0);
+ let a = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
+ let mut g = Node::group(d.alloc_id(), vec![Arc::new(a.clone())]);
+ g.bbox_angle = 170.0;
+ g.extras_mut().mask = Some(Box::new(crate::OpacityMask::new(a.clone(), true)));
+ let mut layer = (*d.layers[0]).clone();
+ layer.children_mut().unwrap().push(Arc::new(g));
+ layer.turn_boxes(20.0);
+ assert_eq!(layer.bbox_angle, 0.0);
+ let g = &layer.children().unwrap()[0];
+ assert!(close(g.bbox_angle, -170.0), "wraps to (-180, 180]");
+ assert!(close(g.children().unwrap()[0].bbox_angle, 20.0));
+ assert!(close(g.extras().mask.as_ref().unwrap().art.bbox_angle, 20.0));
+ }
+
#[test]
fn oriented_bounds_hug_a_rotated_rectangle() {
let mut d = Document::new(500.0, 500.0);
diff --git a/crates/doc/src/symbol_nine_slice.rs b/crates/doc/src/symbol_nine_slice.rs
index 53f84e869c..3c8c1b324d 100644
--- a/crates/doc/src/symbol_nine_slice.rs
+++ b/crates/doc/src/symbol_nine_slice.rs
@@ -614,6 +614,8 @@ impl NineSliceMap {
/// `n`, whose box at the natural size is `b`, drawn in each cell it reaches: a clip group of
/// the cell around a copy placed by the cell's map, all in a group with `n`'s id. `n` placed
/// by `center` when it draws symbol art ([`draws_symbol_art`]), and in [`Self::place_unclipped`].
+ /// The clip groups and clip paths take the angle of the cell map that places them. The group
+ /// and each copy keep `n`'s box angle, turned by the map that places them.
///
/// The drop shadows, outer glows and blurs ([`PAINTED_EFFECTS`]) at the end of `n`'s own
/// effects, after its last other visible effect, go on that group with its opacity, blending
@@ -647,11 +649,15 @@ impl NineSliceMap {
area.transform(local);
let mut clip = Node::new(NodeId(0), NodeKind::Path { path: area, rule: FillRule::NonZero, live: None, clipping: true, guide: false });
clip.appearance = Appearance::default().into();
+ clip.bbox_angle = crate::orient::transformed_angle(0.0, local);
let copy = transformed(n, local * self.natural);
- cells.push(Arc::new(Node::new(NodeId(0), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(copy)], clip: true })));
+ let mut cell = Node::new(NodeId(0), NodeKind::Group { children: vec![Arc::new(clip), Arc::new(copy)], clip: true });
+ cell.bbox_angle = crate::orient::transformed_angle(0.0, local);
+ cells.push(Arc::new(cell));
}
}
let mut out = Node::group(n.id, cells);
+ out.bbox_angle = crate::orient::transformed_angle(n.bbox_angle, center);
out.visible = n.visible;
out.locked = n.locked;
out.name = n.name.clone();
diff --git a/crates/doc/src/symbol_overrides.rs b/crates/doc/src/symbol_overrides.rs
index 33f0fd7695..241c61fba8 100644
--- a/crates/doc/src/symbol_overrides.rs
+++ b/crates/doc/src/symbol_overrides.rs
@@ -2,14 +2,15 @@
//! symbol keeps appearance changes of its own to objects in the symbol's art, while the art stays
//! the symbol's. Editing the symbol reshapes every instance and each keeps its changes; Reset drops
//! them. The changes are stored on the instance ([`crate::node::NodeExtras::symbol_overrides`]) and
-//! applied wherever an instance is drawn or exported ([`dynamic_art`]).
+//! applied wherever an instance is drawn or exported ([`dynamic_art`]), and an instance's fill
+//! stains the art after them ([`instance_art`]).
use std::sync::Arc;
use serde::{Deserialize, Serialize};
-use vectorcraft_color::{BlendMode, Paint};
+use vectorcraft_color::{BlendMode, Color, Paint};
-use crate::{Node, NodeKind};
+use crate::{AppearanceItem, Node, NodeKind};
/// Deepest object an override may name: groups nested this deep in the symbol's art.
pub const MAX_PATH_DEPTH: usize = 64;
@@ -150,6 +151,52 @@ pub fn dynamic_art(art: &Node, inst: &Node) -> Node {
a
}
+/// Mix every solid paint under `n` towards `color` by `amount` (0..1): the Symbol Stainer.
+pub fn tint_node(n: &mut Node, color: &Color, amount: f32) {
+ let amount = amount.clamp(0.0, 1.0);
+ for it in &mut n.appearance.items {
+ let p = match it {
+ AppearanceItem::Fill(f) => &mut f.paint,
+ AppearanceItem::Stroke(s) => &mut s.paint,
+ };
+ if let Paint::Solid { color: c, .. } = p {
+ *p = Paint::solid(c.lerp(color, amount));
+ }
+ }
+ if let NodeKind::Text(t) = &mut n.kind {
+ for r in &mut t.runs {
+ for p in [&mut r.style.fill, &mut r.style.stroke] {
+ if let Paint::Solid { color: c, .. } = p {
+ *p = Paint::solid(c.lerp(color, amount));
+ }
+ }
+ }
+ }
+ if let Some(ch) = n.children_mut() {
+ for c in ch.iter_mut() {
+ tint_node(Arc::make_mut(c), color, amount);
+ }
+ }
+}
+
+/// The stain instance `inst` puts on its symbol's art: its visible fill color and that fill's
+/// opacity (how far the art mixes towards it). `None` for an unstained instance, including one
+/// whose fill has an opacity of 0, so its art keeps its paints and their swatch links.
+pub fn stain(inst: &Node) -> Option<(Color, f32)> {
+ let f = inst.appearance.fill().filter(|f| f.visible && f.opacity > 0.0)?;
+ Some((f.paint.color()?, f.opacity))
+}
+
+/// A symbol's art for instance `inst`, as the canvas draws it: with the instance's own appearance
+/// changes to it ([`dynamic_art`]), stained by the instance's fill ([`stain`]).
+pub fn instance_art(art: &Node, inst: &Node) -> Node {
+ let mut a = dynamic_art(art, inst);
+ if let Some((c, amount)) = stain(inst) {
+ tint_node(&mut a, &c, amount);
+ }
+ a
+}
+
/// Keep the overrides that name an object of `art` (a symbol's new art), dropping the rest.
pub fn retain_parts(overrides: &mut Vec, art: &Node) {
overrides.retain(|o| part(art, &o.path).is_some());
@@ -159,7 +206,6 @@ pub fn retain_parts(overrides: &mut Vec, art: &Node) {
mod tests {
use super::*;
use crate::{Appearance, NodeId};
- use vectorcraft_color::Color;
use vectorcraft_geom::{Rect, shapes};
fn square(id: u64) -> Arc {
diff --git a/crates/effects/src/adjust.rs b/crates/effects/src/adjust.rs
index 4193b085ef..1b9ac80452 100644
--- a/crates/effects/src/adjust.rs
+++ b/crates/effects/src/adjust.rs
@@ -523,13 +523,14 @@ fn color_mask_keys<'a>(n: &'a Node, out: &mut Vec<&'a str>) {
/// [`adjust`] for export and Expand Appearance: recoloured embedded images become new images in
/// `d` (named by their content), and a symbol instance stands for its symbol's art: `symbol_art`
-/// when given, otherwise the symbol's art with the instance's own appearance changes.
+/// when given, otherwise the symbol's art with the instance's own appearance changes, stained by
+/// the instance's fill ([`vectorcraft_doc::symbol_overrides::instance_art`]).
pub fn adjust_in_document(d: &mut Document, n: &Node, symbol_art: Option) -> Option {
let art;
let src = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } if has_adjustment(n) => {
let symbol = symbol_art
- .or_else(|| d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n)));
+ .or_else(|| d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::instance_art(&s.art, n)));
art = crate::reshape::as_art(Some(d), n, symbol.as_ref())?;
&art
}
diff --git a/crates/effects/src/bake.rs b/crates/effects/src/bake.rs
index 7e42147443..90a0d85ec8 100644
--- a/crates/effects/src/bake.rs
+++ b/crates/effects/src/bake.rs
@@ -148,9 +148,11 @@ fn bake_node(d: &mut Document, n: &Node) -> Option {
if let Some(m) = moved_type_group(n) {
return Some(bake_pieces(d, m));
}
+ // A symbol instance's art as the canvas draws it: with its own appearance changes, stained
+ // by its fill.
let symbol = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => {
- d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::dynamic_art(&s.art, n))
+ d.symbols.iter().find(|s| s.name == *symbol).map(|s| vectorcraft_doc::symbol_overrides::instance_art(&s.art, n))
}
_ => None,
};
diff --git a/crates/effects/src/bake/tests.rs b/crates/effects/src/bake/tests.rs
index 2bcc026340..f3855f57ca 100644
--- a/crates/effects/src/bake/tests.rs
+++ b/crates/effects/src/bake/tests.rs
@@ -59,6 +59,46 @@ fn object_effects_become_geometry_and_raster_effects_stay() {
assert_eq!(m.appearance.effects[0].id, "stylize.dropShadow");
}
+/// The fill colors of the paths under the layers of `d`, in paint order.
+fn path_fills(d: &Document) -> Vec {
+ let mut out = vec![];
+ d.walk(|n| {
+ if matches!(n.kind, NodeKind::Path { .. })
+ && let Some(c) = n.appearance.fill_paint().color()
+ {
+ out.push(c);
+ }
+ });
+ out
+}
+
+/// A symbol instance half stained red is baked in its stained colors, as the canvas draws it:
+/// moved by a Transform effect, and recolored by a color adjustment (Invert Colors) after the
+/// stain.
+#[test]
+fn a_stained_instance_is_baked_in_its_stained_colors() {
+ let blue = Color::rgb8(30, 90, 220);
+ let art = Node::path(NodeId(2), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::basic(Paint::solid(blue), Paint::None, 0.0));
+ let mut inst = Node::new(NodeId(50), NodeKind::SymbolInstance { symbol: "Mark".into(), xf: vectorcraft_geom::Affine::translate((20.0, 20.0)) });
+ inst.appearance = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0).into();
+ inst.appearance.items.retain(|i| i.is_fill());
+ if let vectorcraft_doc::AppearanceItem::Fill(f) = &mut inst.appearance.items[0] {
+ f.opacity = 0.5;
+ }
+ let stained = vectorcraft_doc::symbol_overrides::instance_art(&art, &inst).appearance.fill_paint().color().unwrap();
+ assert_ne!(stained.to_hex(), blue.to_hex());
+ let [r, g, b] = stained.to_rgb();
+ let inverted = Color::rgb(1.0 - r, 1.0 - g, 1.0 - b);
+ for (id, params, want) in [("distort.transform", json!({"moveH": 30.0}), stained), (crate::addons::color::INVERT, json!({}), inverted)] {
+ let mut n = inst.clone();
+ n.appearance.effects.push(fx(id, params));
+ let mut d = doc_with(n);
+ d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Mark".into(), art: Arc::new(art.clone()) });
+ let fills = path_fills(&bake_document(&d).unwrap());
+ assert_eq!(fills.iter().map(Color::to_hex).collect::>(), [want.to_hex()], "{id}");
+ }
+}
+
#[test]
fn a_baked_transform_moves_a_placed_gradient_with_the_path() {
use vectorcraft_color::{Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop};
diff --git a/crates/effects/src/live.rs b/crates/effects/src/live.rs
index 2c02f886c0..5a878d42d3 100644
--- a/crates/effects/src/live.rs
+++ b/crates/effects/src/live.rs
@@ -23,15 +23,22 @@ pub fn text_outliner() -> Outliner<'static> {
/// Type outlined, or a symbol instance as its symbol's art (from `doc`) placed by the instance,
/// through the symbol's 9-slice guides when it has them ([`Document::place_symbol_art`], `None`
/// past its object limit). As an [`OutlineHook`](crate::OutlineHook), it gives compound shapes and
-/// Pathfinder operations the symbol instances among their members.
+/// Pathfinder operations the symbol instances among their members. The group around the art takes
+/// the instance's box angle, and the art's boxes turn with it, as Break Link turns them.
pub fn outline_object(doc: Option<&Document>, n: &Node) -> Option {
match &n.kind {
NodeKind::SymbolInstance { symbol, xf } => {
let doc = doc?;
let sym = doc.symbols.iter().find(|s| s.name == *symbol)?;
let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
- let art = doc.place_symbol_art(symbol, &art, *xf)?;
- Some(Node::group(n.id, vec![Arc::new(art)]))
+ let mut art = doc.place_symbol_art(symbol, &art, *xf)?;
+ let turn = vectorcraft_doc::orient::placed_turn(n.bbox_angle, *xf);
+ if turn != 0.0 {
+ art.turn_boxes(turn);
+ }
+ let mut out = Node::group(n.id, vec![Arc::new(art)]);
+ out.bbox_angle = n.bbox_angle;
+ Some(out)
}
_ => outline_text(n),
}
diff --git a/crates/effects/src/tests.rs b/crates/effects/src/tests.rs
index 627cc1be03..0023fc5b16 100644
--- a/crates/effects/src/tests.rs
+++ b/crates/effects/src/tests.rs
@@ -1570,6 +1570,27 @@ fn outline_object_is_a_geometry_effect_that_keeps_the_path() {
assert_eq!(apply_geometry(&[fx("path.outlineObject", json!({})), offset.clone()], &square(), b), apply_geometry(&[offset], &square(), b));
}
+/// A symbol instance given to Pathfinder and compound shapes as its art: the group takes the
+/// instance's box angle and the art turns with it, turned with the instance or reset to 0.
+#[test]
+fn outline_object_turns_an_instances_art_with_its_box() {
+ use std::sync::Arc;
+
+ use vectorcraft_doc::orient::transformed_angle;
+ use vectorcraft_doc::{Appearance, Document, NodeId, Symbol};
+ let mut d = Document::new(500.0, 500.0);
+ d.symbols.push(Symbol { dynamic: false, name: "s".into(), art: Arc::new(Node::path(NodeId(0), square(), Appearance::default())) });
+ let xf = Affine::translate((200.0, 100.0)) * Affine::rotate(0.35);
+ for box_angle in [transformed_angle(0.0, xf), 0.0] {
+ let mut inst = Node::new(NodeId(3), NodeKind::SymbolInstance { symbol: "s".into(), xf });
+ inst.bbox_angle = box_angle;
+ let out = outline_object(Some(&d), &inst).unwrap();
+ let mut angles = vec![];
+ out.walk(&mut |c| angles.push(c.bbox_angle));
+ assert!(angles.len() == 2 && angles.iter().all(|a| close(*a, box_angle, 1e-9)), "{box_angle}: {angles:?}");
+ }
+}
+
#[test]
fn convert_to_shape_sizes() {
let r = run("convertToShape.rectangle", json!({"extraW": 20, "extraH": 10}), &square()).bounds().unwrap();
diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs
index 94d8add68b..d4fb2c39cd 100644
--- a/crates/engine/src/cmd.rs
+++ b/crates/engine/src/cmd.rs
@@ -440,6 +440,15 @@ pub(crate) fn f64_req(p: &Value, key: &str, cmd: &str) -> Result {
Some(v) => v.as_f64().ok_or_else(|| bad(cmd, format!("`{key}` must be a number, not {v}"))),
}
}
+/// The whole number of 0 or more that `p` gives for `key`. A missing key or null is "missing integer
+/// `{key}`"; any other value, such as `"3"`, `2.5`, `-1` or `true`, is "`{key}` must be a whole
+/// number", as [`object_param`] words it.
+pub(crate) fn u64_req(p: &Value, key: &str, cmd: &str) -> Result {
+ match p.get(key) {
+ None | Some(Value::Null) => Err(bad(cmd, format!("missing integer `{key}`"))),
+ Some(v) => v.as_u64().ok_or_else(|| bad(cmd, format!("`{key}` must be a whole number"))),
+ }
+}
/// The number `p` gives for `key`, or `default` when `p` leaves it out or gives null. Any other
/// value, such as `"40"`, is an error: "`{key}` must be a number, not {v}".
pub(crate) fn number_or(p: &Value, key: &str, default: f64, cmd: &str) -> Result {
diff --git a/crates/engine/src/cmd/appearance.rs b/crates/engine/src/cmd/appearance.rs
index 02bc111746..bed4b33b76 100644
--- a/crates/engine/src/cmd/appearance.rs
+++ b/crates/engine/src/cmd/appearance.rs
@@ -36,7 +36,7 @@ use super::paint::paint_from;
use super::{AppearanceAttrs, EyedropperAttrs, FillAttrs, StrokeAttrs};
use super::{
CommandSpec, NodeId, Result, Session, bad, check_numbers, check_switches, cmd, has_doc, has_selection, ids_param, index, leaf_targets,
- object_param, ok, paint_targets, str_param, switch_or, targets,
+ object_param, ok, paint_targets, str_param, switch_or, targets, u64_req,
};
use crate::EngineError;
@@ -604,9 +604,9 @@ pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bo
Ok(ids.get(item.anchor(s, &ids)).and_then(|id| d.node(*id)?.appearance.items.get(index)).map_or(default, |it| !it.is_fill()))
}
-/// The whole number of 0 or more that `key` holds, read with [`index`].
+/// The whole number of 0 or more that `key` holds, read with [`u64_req`] and [`index`].
pub(crate) fn index_param(p: &Value, key: &str, cmd: &str) -> Result {
- p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
+ u64_req(p, key, cmd).map(index)
}
fn set_active_item(s: &mut Session, p: &Value) -> Result {
diff --git a/crates/engine/src/cmd/assets.rs b/crates/engine/src/cmd/assets.rs
index 1ebde5c026..e30e6efcdc 100644
--- a/crates/engine/src/cmd/assets.rs
+++ b/crates/engine/src/cmd/assets.rs
@@ -53,7 +53,7 @@ pub fn specs() -> Vec {
"Export Assets",
[],
None,
- "{assets?: [asset id…] (default all but the 3D assets, which assets.export3D writes), folder?, zip?, openLocation?, formats?, preset?, settings?, prefix?, subfolders?, antiAlias?} each asset's art alone, cropped to it, in every format row: document.exportForScreens {assets} at the document's export settings (document.exportSettings) under the ones given; with subfolders false every file goes into the folder itself, mobile and density rows too → what document.exportForScreens returns (no folder: the files, or one zip, as dataBase64). Leaves the remembered settings as they are (assets.settings.set changes them)",
+ "{assets?: [asset id…] (default all but the 3D assets, which assets.export3D writes), folder?, zip?, openLocation?, formats?, preset?, settings?, prefix?, subfolders?, antiAlias?} each asset's art alone, cropped to it, in every format row: document.exportForScreens {assets} at the document's export settings (document.exportSettings) under the ones given; with subfolders false every file goes into the folder itself, mobile and density rows too → what document.exportForScreens returns (no folder: the files, or one zip, as dataBase64; warnings name a key in the rows or settings given that only another format takes, not one in the remembered settings). Leaves the remembered settings as they are (assets.settings.set changes them)",
has_doc,
export
),
@@ -429,7 +429,7 @@ fn export(s: &mut Session, p: &Value) -> Result {
}
q.insert("assets".into(), json!(all));
}
- export_screens(s, &Value::Object(q)).map_err(|e| of_cmd(C, e))
+ export_screens(s, &Value::Object(q), p).map_err(|e| of_cmd(C, e))
}
fn settings_set(s: &mut Session, p: &Value) -> Result {
diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs
index 50c63b59cd..32b0f9fcd0 100644
--- a/crates/engine/src/cmd/brushsym.rs
+++ b/crates/engine/src/cmd/brushsym.rs
@@ -226,7 +226,7 @@ pub fn specs() -> Vec {
"Break Link to Symbol",
[],
None,
- "{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol), placed through the symbol's 9-slice guides when it has them, with the instance's opacity mask; a hidden or locked instance's art stays hidden or locked and is not selected → {ids}",
+ "{ids?|id?} expand selected instances into plain art (with their appearance changes to a dynamic symbol), placed through the symbol's 9-slice guides when it has them, with the instance's opacity mask and its own bounding box (square after Reset Bounding Box); a hidden or locked instance's art stays hidden or locked and is not selected → {ids}",
has_selection,
symbol_break_link
),
diff --git a/crates/engine/src/cmd/brushsym/symbols.rs b/crates/engine/src/cmd/brushsym/symbols.rs
index 2fb31c5f6b..1bfb8edea7 100644
--- a/crates/engine/src/cmd/brushsym/symbols.rs
+++ b/crates/engine/src/cmd/brushsym/symbols.rs
@@ -393,8 +393,10 @@ pub(super) fn symbol_place(s: &mut Session, p: &Value) -> Result {
/// The plain art of instance `inst` in document space (fresh ids), with the instance's own
/// appearance changes to a dynamic symbol's art when `own`, placed through its symbol's 9-slice
-/// guides when it has them. The instance's opacity mask goes on the art, or on a group around it
-/// when the art has a mask of its own.
+/// guides when it has them. The art's bounding boxes turn with the instance's own box, so after
+/// Reset Bounding Box they stand as the instance's box does. The instance's opacity mask goes on
+/// the art, or on a group around it when the art has a mask of its own; a group added around the
+/// art takes the instance's box angle.
fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option {
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return None };
let sym = d.symbols.iter().find(|s| &s.name == symbol)?;
@@ -402,12 +404,17 @@ fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option {
if let Some((color, amount)) = brush::stain(inst) {
brush::tint_node(&mut art, &color, amount);
}
- let art = d.place_symbol_art(symbol, &art, *xf)?;
+ let mut art = d.place_symbol_art(symbol, &art, *xf)?;
+ let turn = vectorcraft_doc::orient::placed_turn(inst.bbox_angle, *xf);
+ if turn != 0.0 {
+ art.turn_boxes(turn);
+ }
let mut art = d.reid(&art);
if inst.opacity < 1.0 || inst.blend != vectorcraft_color::BlendMode::Normal {
if !matches!(art.kind, NodeKind::Group { .. }) {
let id = d.alloc_id();
art = Node::group(id, vec![Arc::new(art)]);
+ art.bbox_angle = inst.bbox_angle;
}
art.opacity *= inst.opacity;
art.blend = inst.blend;
@@ -416,6 +423,7 @@ fn expanded_instance(d: &mut Document, inst: &Node, own: bool) -> Option {
if art.extras().mask.is_some() {
let id = d.alloc_id();
art = Node::group(id, vec![Arc::new(art)]);
+ art.bbox_angle = inst.bbox_angle;
}
art.extras_mut().mask = Some(mask.clone());
}
diff --git a/crates/engine/src/cmd/draw2/common.rs b/crates/engine/src/cmd/draw2/common.rs
index 0ca376b243..fd8967f3fa 100644
--- a/crates/engine/src/cmd/draw2/common.rs
+++ b/crates/engine/src/cmd/draw2/common.rs
@@ -231,9 +231,9 @@ pub(super) fn within_input_bound(q: Point) -> bool {
q.x.is_finite() && q.y.is_finite() && q.x.abs() < MAX_INPUT && q.y.abs() < MAX_INPUT
}
-/// The whole number of 0 or more that `key` holds, read with [`index`].
+/// The whole number of 0 or more that `key` holds, read with [`u64_req`](crate::cmd::u64_req) and [`index`].
pub(super) fn usize_req(p: &Value, key: &str, cmd: &str) -> Result {
- p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
+ crate::cmd::u64_req(p, key, cmd).map(index)
}
/// The whole number of 0 or more that `key` holds, or `None` when `key` is absent or null. Any other
diff --git a/crates/engine/src/cmd/draw2/freehand.rs b/crates/engine/src/cmd/draw2/freehand.rs
index 31e184e195..0439aaa081 100644
--- a/crates/engine/src/cmd/draw2/freehand.rs
+++ b/crates/engine/src/cmd/draw2/freehand.rs
@@ -11,7 +11,9 @@ use super::{
// ---------- Pencil / Paintbrush ----------
/// Fit a freehand polyline: drop jitter, build a spline through the samples (sharp turns stay
-/// corners) and simplify it to `tol`.
+/// corners) and simplify it to `tol`. A closed stroke is fitted as the open one is and closed by a
+/// straight segment from its end back to its start: fitting round the gap would curve that segment
+/// on from both ends, past them and back.
fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath {
let min = (tol * 0.3).max(vectorcraft_tools::draw2::MIN_FIT_TOLERANCE * 0.3);
let (Some(&p0), Some(&pn)) = (pts.first(), pts.last()) else { return SubPath::new(vec![], closed) };
@@ -41,15 +43,26 @@ fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath {
(kept[i], corner)
})
.collect();
- let sp = vectorcraft_tools::draw2::catmull_rom(&marked, closed);
+ let sp = vectorcraft_tools::draw2::catmull_rom(&marked, false);
if sp.anchors.len() < 3 {
- return sp;
+ // Two anchors closed would run back over the line itself.
+ return SubPath::new(sp.anchors, closed && n > 2);
}
let fitted = po::simplify_with(
&PathData::single(sp.clone()),
&SimplifyOptions { tolerance: tol, corner_angle_deg: 60.0, straight_lines: false, ..Default::default() },
);
- fitted.subpaths.into_iter().next().filter(|s| s.anchors.len() >= 2).unwrap_or(sp)
+ let mut anchors = fitted.subpaths.into_iter().next().filter(|s| s.anchors.len() >= 2).unwrap_or(sp).anchors;
+ if closed {
+ // No handle on either side of the closing segment.
+ if let Some(l) = anchors.last_mut() {
+ l.h_out = l.p;
+ }
+ if let Some(f) = anchors.first_mut() {
+ f.h_in = f.p;
+ }
+ }
+ SubPath::new(anchors, closed)
}
/// The path of a freehand stroke: fitted at `tol` ([`fit_freehand`]), or with no `tol` the points
diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs
index 990ac1a1c6..576b05ffd6 100644
--- a/crates/engine/src/cmd/edit.rs
+++ b/crates/engine/src/cmd/edit.rs
@@ -11,7 +11,7 @@ use super::clipboard::SwatchChoices;
use super::{
CommandSpec, Result, Session, always, bad, can_clear_history, can_redo, can_undo, check_numbers, checked_ids_param, clock_date, cmd, date_param,
f64_req, finite_numbers, has_clipboard, has_doc, has_selection, has_selection_guides_or_artboard_tool, has_selection_or_guides, index, number_or,
- object_param, ok, point_param, str_param, switch_or,
+ object_param, ok, point_param, str_param, switch_or, u64_req,
};
use crate::{Clipboard, EngineError, SourceLayer, inspect};
@@ -249,7 +249,10 @@ fn file_close(s: &mut Session, p: &Value) -> Result {
}
fn doc_activate(s: &mut Session, p: &Value) -> Result {
- let i = p.get("index").and_then(Value::as_u64).map(index).ok_or_else(|| bad("document.activate", "missing index"))?;
+ let i = match p.get("index") {
+ None | Some(Value::Null) => return Err(bad("document.activate", "missing index")),
+ Some(v) => v.as_u64().map(index).ok_or_else(|| bad("document.activate", "`index` must be a whole number"))?,
+ };
if s.set_active(i) { ok() } else { Err(bad("document.activate", "no such document")) }
}
@@ -262,7 +265,10 @@ fn doc_move(s: &mut Session, p: &Value) -> Result {
if from >= n {
return Err(bad("document.move", "no such document"));
}
- let to = p.get("to").and_then(Value::as_u64).map(index).ok_or_else(|| bad("document.move", "missing `to`: a non-negative integer"))?;
+ let to = match p.get("to") {
+ None | Some(Value::Null) => return Err(bad("document.move", "missing `to`: a non-negative integer")),
+ Some(v) => v.as_u64().map(index).ok_or_else(|| bad("document.move", "`to` must be a whole number"))?,
+ };
let to = to.min(n.saturating_sub(1));
if !s.move_document(from, to) {
return Err(bad("document.move", "no such document"));
@@ -1190,8 +1196,7 @@ fn clear(s: &mut Session, p: &Value) -> Result {
/// Document ids scope node ids: two tabs may contain objects with the same node id.
fn drop_selection(s: &mut Session, p: &Value) -> Result {
const CMD: &str = "edit.dropSelection";
- let key = "sourceDocument";
- let uid = p[key].as_u64().ok_or_else(|| bad(CMD, format!("missing integer `{key}`")))?;
+ let uid = u64_req(p, "sourceDocument", CMD)?;
let point = |key: &str| finite_numbers::<2>(&p[key]).ok_or_else(|| bad(CMD, format!("{key} must be [x, y] with finite coordinates")));
let grab = point("grab")?;
let at = point("at")?;
diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs
index d20c6c46b2..1ff1897c85 100644
--- a/crates/engine/src/cmd/fileio.rs
+++ b/crates/engine/src/cmd/fileio.rs
@@ -92,7 +92,7 @@ pub use pdfimport::{LoadOptions, page_document};
pub(crate) use save::job_for;
pub(crate) use save::save as save_document;
pub use save::{
- SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, dialog_folder, documents_dialog_folder, export_folder,
+ SAVE_FORMATS, SaveJob, SaveMode, SavePlan, StreamWrite, TEMPLATE_FORMATS, dialog_folder, documents_dialog_folder, export_folder, file_reads,
leave_out_unreadable_save_paths, reads_option as save_reads_option, remembered_save_options, save_filters, save_format, save_job, save_plan,
save_with, stamp_save_dates, template_filters, templates_dialog_folder, templates_folder,
};
diff --git a/crates/engine/src/cmd/fileio/commands.rs b/crates/engine/src/cmd/fileio/commands.rs
index 28e12c115d..03736c6022 100644
--- a/crates/engine/src/cmd/fileio/commands.rs
+++ b/crates/engine/src/cmd/fileio/commands.rs
@@ -54,7 +54,7 @@ pub fn specs() -> Vec {
"Export for Screens",
["File", "Export"],
None,
- "{folder?, zip?: false (one .zip of every file, each deflated or stored, whichever is smaller: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboard?: index | artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), includeArtboardFill?: true (Include Artboard Fill: each artboard's background color behind its art; false leaves it out), pdfs?: multiple|single (Export PDFs As: with several artboards, single writes them as the pages of one PDF per PDF row, named after the document), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with for the document, for the artboard, asset, layer or object, from 1, #### for the counter zero-padded to the run of #, , , (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, , , , , , , and ; a trailing .ext is dropped, as in `COMPANY-Logo--####-YYYYMMDD`; different files it names alike get -2, -3…), counterStart?: 1, counterPer?: item (every file of an item has its number)|file, date?: YYYY-MM-DD (the date the pattern uses; default today, UTC), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N; a row whose file would take a name another row's file has, in any case, gets -2, -3… too, and identical rows write one file; row folders that differ only in case are one folder, spelled as the first row spells it) → {files: [path…]} (into the folder of exportPreset, outside the automation read roots: no ZIP path, and files relative to that folder); no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png); every reply has warnings: [the encoders' warnings for the files, each once: an image that couldn't be decoded, type written in the fallback font…]. eps, ai and vectorcraft rows write a document of the item alone (its one artboard). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step). A key it doesn't take, a key no format takes in a format row or in settings, settings for a format it doesn't write, and an unknown key inside a PDF, EPS or SVG option's object, are refused. Under automation read roots, an svg or svgz row is refused, before the folder is made or any file is written, while the document links a JavaScript file outside them, as document.export refuses it",
+ "{folder?, zip?: false (one .zip of every file, each deflated or stored, whichever is smaller: no folder → {name, dataBase64, bytes, files: [name…]}; with a folder it is written there → {path, bytes, files}), artboard?: index | artboards?: [index…] | range?: \"1-3\" (default all), assets?: [asset id…] (instead of artboards: each asset's art alone, cropped to its bounds and named after the asset; see assets.list), layers?: [layer id…] (instead of artboards: each layer's art alone, sublayers included, named after the layer), selection?: one|each (instead of artboards: the selected art as one file, or a file per selected object, named after it), fullDocument?: false (one file per format instead: a PDF of every artboard, other formats the bounds of all visible art, named after the document), includeBleed?: false (artboards grown by the document's bleed), includeArtboardFill?: true (Include Artboard Fill: each artboard's background color behind its art; false leaves it out), pdfs?: multiple|single (Export PDFs As: with several artboards, single writes them as the pages of one PDF per PDF row, named after the document), exportPreset?: (a preset of document.exportPresets.list: its settings, under these params), subfolders?: false (each row's files in a sub-folder: its folder, else its size for raster (1x, 2x, 100w…) or its format (SVG, PDF)), folders?: none|rows (as subfolders)|format (a folder per extension: png, svg, pdf…)|item (a folder per artboard, asset, layer or object)|itemFormat (item/extension) (default: rows with subfolders or a preset, else none), name?: (a file name pattern instead of the item's name, with for the document, for the artboard, asset, layer or object, from 1, #### for the counter zero-padded to the run of #, , , (else the row's suffix ends the name), YYYYMMDD, YYYY-MM-DD, YYYY, , , , , , , and ; a trailing .ext is dropped, as in `COMPANY-Logo--####-YYYYMMDD`; different files it names alike get -2, -3…), counterStart?: 1, counterPer?: item (every file of an item has its number)|file, date?: YYYY-MM-DD (the date the pattern uses; default today, UTC), preset?: mobile (PNG 1x, 2x, 3x) | density (PNG 0.75x–4x in ldpi…xxxhdpi sub-folders) instead of formats (turns subfolders on), formats?: [{format: png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft, scale?: 1 | \"2x\" | \"100w\" | \"100h\" | \"72ppi\", width?: px | height?: px | ppi? (raster only; width, then height, then ppi win over scale), suffix?: (raster default: @2x, @100w, @100h, none at 1x; vector formats drop size suffixes), folder?: (its sub-folder), quality?: (jpg 0–100), …the format's options (antiAlias, background, preset (pdf)…)}], settings?: {png|png8|jpg|webp|gif|tiff|bmp|tga|psd|svg|svgz|pdf|eps|ai|vectorcraft: {…options for every row of that format}} (rows' own options win), prefix?, antiAlias?: none|art|type (raster rows without their own), openLocation?: (remembered; the app shows the files after exporting)} one file per artboard and format (a PDF holds its artboard alone; artboards with the same name, in any case, get -2, -3…; an unnamed one is Artboard-N; a row whose file would take a name another row's file has, in any case, gets -2, -3… too, and identical rows write one file; row folders that differ only in case are one folder, spelled as the first row spells it) → {files: [path…]} (into the folder of exportPreset, outside the automation read roots: no ZIP path, and files relative to that folder); no folder → {files: [{name, dataBase64}]} (names include sub-folders: 2x/Icon@2x.png); every reply has warnings: [the encoders' warnings for the files, each once: an image that couldn't be decoded, type written in the fallback font…]. eps, ai and vectorcraft rows write a document of the item alone (its one artboard). The params (but zip and assets) are remembered in the document (document.exportSettings; not an undo step). A key it doesn't take, a key no format takes in a format row or in settings, settings for a format it doesn't write, and an unknown key inside a PDF, EPS or SVG option's object, are refused; a key only another format takes in a row or in settings is not used, and warnings name it as given (formats[0].quality; a key in settings only when no row of that format reads it; not a key of an exportPreset). Under automation read roots, an svg or svgz row is refused, before the folder is made or any file is written, while the document links a JavaScript file outside them, as document.export refuses it",
has_doc,
screens::export_for_screens
),
diff --git a/crates/engine/src/cmd/fileio/export.rs b/crates/engine/src/cmd/fileio/export.rs
index d8af86802f..594193cd7b 100644
--- a/crates/engine/src/cmd/fileio/export.rs
+++ b/crates/engine/src/cmd/fileio/export.rs
@@ -32,26 +32,25 @@ pub fn unused_option_notes(cmd: &str, f: &Format, p: &Value) -> Vec {
}
let listed = |k: &str| super::FORMATS.iter().filter(|f| f.write).flat_map(|f| f.options).any(|o| o.name == k);
let written = super::save::written_as(cmd, f, p);
- let legacy = f.id == "ai" && super::legacy_ai::wanted(cmd, f, p).unwrap_or(false);
- let reads = |k: &str| match (f.id, k) {
- // A legacy `.ai` file is not a PDF: it reads its own options only.
- ("ai", _) if legacy => super::legacy_ai::reads(k),
- // The other `.ai` files don't read the legacy file's options, and `preserveEditing` is
- // on for them whatever is given.
- ("ai", "preserveEditing") => false,
- ("ai", _) if super::legacy_ai::LEGACY_ONLY.contains(&k) => false,
- // An export writes one file: only a save writes each artboard to a file of its own, and
- // a native file's `range` names those files.
- ("vectorcraft" | "template" | "ai", "separateArtboards") | ("vectorcraft" | "template", "range") => false,
- _ => super::save::reads_option(f, k),
- };
+ let legacy = super::save::legacy_file(cmd, f, p);
given
.iter()
- .filter(|(k, v)| !v.is_null() && !EXPORT_PARAMS.contains(&k.as_str()) && listed(k) && !reads(k))
+ .filter(|(k, v)| !v.is_null() && !EXPORT_PARAMS.contains(&k.as_str()) && listed(k) && !export_reads(f, legacy, k))
.map(|(k, _)| super::save::unused_option_note(k, &written))
.collect()
}
+/// Does the file an export writes in `f` read option `key`, `legacy` when it is a legacy `.ai`
+/// file? As a save's file does ([`super::save::file_reads`]), except that an export writes one
+/// file: only a save writes each artboard to a file of its own, and a native file's `range` names
+/// those files.
+pub(super) fn export_reads(f: &Format, legacy: bool, key: &str) -> bool {
+ match (f.id, key) {
+ ("vectorcraft" | "template" | "ai", "separateArtboards") | ("vectorcraft" | "template", "range") => false,
+ _ => super::save::file_reads(f, legacy, key),
+ }
+}
+
pub(super) fn serialize(s: &mut Session, p: &Value) -> Result {
let f = writable("document.serialize", Some(text_param(p, "format", "document.serialize")?.unwrap_or("vectorcraft")), None)?;
check_linked_scripts("document.serialize", f.id, s.doc()?)?;
diff --git a/crates/engine/src/cmd/fileio/save.rs b/crates/engine/src/cmd/fileio/save.rs
index e56f6467a1..af9ed8e58a 100644
--- a/crates/engine/src/cmd/fileio/save.rs
+++ b/crates/engine/src/cmd/fileio/save.rs
@@ -95,14 +95,18 @@ pub struct SavePlan {
/// The `modified` param (as `date_param` reads it): the File Info date a Save or Save As to a file
/// stamps; `None`: the time it is written.
pub modified: Option>,
- /// Warnings about the params themselves: each native option (`compress`, `version`,
- /// `preview`, `precision`) given beside the path, and each key of `options`, that the format
- /// doesn't read, naming the file the save writes.
+ /// Warnings about the params themselves: each option a save format takes given beside the
+ /// path, and each key of `options`, that the file the save writes doesn't read
+ /// ([`file_reads`]), naming that file.
pub notes: Vec,
/// The file the document was read from and what reading it left out, when the save needs
/// `acknowledgeLoss` to write over it and was not given it: every file the save writes is
/// checked against it before the first is written.
pub lossy_source: Option<(String, Vec)>,
+ /// The `.ai` options the file doesn't read, with the values the document remembers from its
+ /// last `.ai` save: not in `options`, they go back into the remembered options once the file
+ /// is the document's own.
+ pub kept: Map,
}
impl SavePlan {
@@ -279,7 +283,8 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result {
}
}
// None given: the ones the document was last saved with in this format.
- let mut options = if given.is_empty() && format.id == st.format { remembered_save_options(st) } else { given };
+ let from_memory = given.is_empty() && format.id == st.format;
+ let mut options = if from_memory { remembered_save_options(st) } else { given };
// Remember the effective choice, including the encoder's default, so another document's
// later save cannot change what this one writes. Invalid non-null values still reach validation.
if matches!(format.id, "vectorcraft" | "template") {
@@ -311,21 +316,54 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result {
let given = str_param(p, "path").is_some_and(|g| !g.is_empty());
super::check_not_lossy_overwrite(st, path, p, cmd).map_err(|e| if given { e } else { super::access::naming_in_error(e, path) })?;
}
- let written = written_as(cmd, format, &Value::Object(options.clone()));
- let top = NATIVE_OPTIONS.iter().filter(|k| p.get(**k).is_some_and(|v| !v.is_null())).map(|k| k.to_string());
+ let chosen = Value::Object(options.clone());
+ let written = written_as(cmd, format, &chosen);
+ let legacy = legacy_file(cmd, format, &chosen);
+ // Beside the path, a key some save format takes; in `options`, any key.
+ let top = p.as_object().into_iter().flatten().filter(|(k, v)| !v.is_null() && save_listed(k)).map(|(k, _)| k.to_string());
let nested = p.get("options").and_then(Value::as_object).into_iter().flatten().filter(|(_, v)| !v.is_null()).map(|(k, _)| k);
let notes = top
- .filter(|k| !reads_option(format, k))
- .chain(nested.filter(|k| !reads_option(format, k)).map(|k| format!("options.{k}")))
+ .filter(|k| !file_reads(format, legacy, k))
+ .chain(nested.filter(|k| !file_reads(format, legacy, k)).map(|k| format!("options.{k}")))
.map(|k| unused_option_note(&k, &written))
.collect();
+ // When the document's own file is a `.ai`, the `.ai` options this file doesn't read and the
+ // request doesn't give keep their remembered values ([`SavePlan::kept`]), so that Save Options
+ // still shows them (it sends only the ones the file reads). They are neither written nor sent
+ // again.
+ let mut kept = Map::new();
+ if format.id == "ai" && format.id == st.format {
+ let last = remembered_save_options(st);
+ for o in format.options.iter().filter(|o| !file_reads(format, legacy, o.name)) {
+ let value = if from_memory {
+ options.remove(o.name)
+ } else if options.contains_key(o.name) {
+ None
+ } else {
+ last.get(o.name).cloned()
+ };
+ if let Some(v) = value {
+ kept.insert(o.name.to_string(), v);
+ }
+ }
+ }
let lossy_source = super::lossy_source(st, p);
- Ok(SavePlan { mode, path, format, options, name, folder, modified, notes, lossy_source })
+ Ok(SavePlan { mode, path, format, options, name, folder, modified, notes, lossy_source, kept })
}
/// The native save options, which a save also takes beside the path.
const NATIVE_OPTIONS: [&str; 4] = ["compress", "version", "preview", "precision"];
+/// Does some save format (or template format) take option `key`?
+fn save_listed(key: &str) -> bool {
+ NATIVE_OPTIONS.contains(&key)
+ || super::FORMATS
+ .iter()
+ .filter(|f| SAVE_FORMATS.contains(&f.id) || TEMPLATE_FORMATS.contains(&f.id))
+ .flat_map(|f| f.options)
+ .any(|o| o.name == key)
+}
+
/// The first option `p` gives both beside the path and in `options` with different values, as
/// `(key, top-level value, options value)`. For an SVG save (`svg`), an SVG option's value on
/// either side is the one its `svg: {…}` object gives, else the flat one.
@@ -467,6 +505,26 @@ pub fn reads_option(f: &Format, key: &str) -> bool {
f.options.iter().any(|o| o.name == key) || (is_pdf(f) && (super::pdf::OPTIONS.iter().any(|o| o.name == key) || super::pdf::is_setting(key)))
}
+/// Does the file a save or an export writes in `f` read option `key`, `legacy` when it is a legacy
+/// `.ai` file ([`super::legacy_ai::wanted`])? As [`reads_option`], except for `.ai` and `.ait`
+/// files: a legacy file reads its own options only ([`super::legacy_ai::reads`]), and the others
+/// don't read the legacy file's options or `preserveEditing`, which is on for them whatever is
+/// given.
+pub fn file_reads(f: &Format, legacy: bool, key: &str) -> bool {
+ match (f.id, key) {
+ ("ai", _) if legacy => super::legacy_ai::reads(key),
+ ("ai" | "ait", "preserveEditing") => false,
+ ("ai" | "ait", _) if super::legacy_ai::LEGACY_ONLY.contains(&key) => false,
+ _ => reads_option(f, key),
+ }
+}
+
+/// Is a save or an export of `cmd` in `f` with options `p` to write a legacy `.ai` file? An
+/// unknown version is not one (the save or export refuses it).
+pub(super) fn legacy_file(cmd: &str, f: &Format, p: &Value) -> bool {
+ f.id == "ai" && super::legacy_ai::wanted(cmd, f, p).unwrap_or(false)
+}
+
/// The note a save or an export gives for option `key`, as the request names it, which the file it
/// writes (`written`, see [`written_as`]) doesn't read.
pub(super) fn unused_option_note(key: &str, written: &Format) -> String {
@@ -1037,6 +1095,7 @@ impl SaveJob {
st.path = Some(path);
st.format = self.plan.format.id;
st.save_options = self.plan.options;
+ st.save_options.extend(self.plan.kept);
// A later save rounds only when it gives `precision` too.
st.save_options.remove("precision");
st.save_artboards = self.snapshot.artboards.iter().map(|a| a.id).collect();
@@ -1239,7 +1298,7 @@ pub(super) fn specs() -> Vec {
"Save Document",
[],
None,
- "{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings say when the format doesn't read compress, version, preview or precision given flat, or a key given in options), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to -. beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file and EPS (below): a .ai file is a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS takes the options of document.exportEps: by default one file whose bounding box fits the visible art (the first artboard when no art shows), carrying the whole editable document; useArtboards: true (also when artboard, artboards or range names artboards; file.formatOptions then reports it true) makes the chosen artboard the page, drawn with its background color (without Use Artboards, only the first artboard's page when no art shows has its color), and with two or more chosen the file at the path is the central file of the visible art (again the first artboard when none shows) and each artboard is also written to _.eps beside it, holding that artboard; selectedOnly is refused; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. Under automation read roots, a reply leaves out the document's own file outside them when no path is given (files and linked then name the files by their file names), and a suggested folder outside them; an svg or svgz save (a document whose own format is SVG or SVGZ included) is refused, before any file is written, while the document links a JavaScript file outside them (svgInteractivity; a URL is not checked), also by file.saveAs and file.saveCopy. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused. A terminal filesystem permission/read-only refusal returns READ_ONLY_LOCATION: followed by the reason; recovery-copy failures keep their original error. Non-UI callers never open a save panel",
+ "{path?, format?: vectorcraft|template|pdf|svg|svgz|ai|eps (default: the path's extension, else the document's own format), options?: {…the format's options, see file.formatOptions; default: as last saved}, svg?: {…SVG options} (an SVG save keeps hidden layers, display:none, unless hiddenLayers is false), …the SVG and PDF options given flat instead (an option given flat or in svg and in options with different values is an error), compress?: bool (native, also in options: gzip; default: the useCompression preference; warnings name an option given flat or in options that the file doesn't read: a legacy .ai reads only its own options, and other .ai files don't read outlineText, hiddenObjects, transparentImages or preserveEditing), version?: 4 (3, 2 or 1: for older VectorCraft versions; 1 and 2 are never compressed), preview?: false (embed a PNG of the first artboard, at most 256 px), precision?: 0..9 (native: round path anchors and handles to this many decimal places, a smaller file; colors, transforms and type stay exact; default: exact, and a later save without it writes exactly again), separateArtboards?: false (native and .ai: also save each artboard of `range` to -. beside the file, holding that artboard and the art touching it; the result's files lists every file written), range?: \"1-3, 5\"|\"all\" (default; with separateArtboards), includeLinked?: false (keep linked files' own pixels, not just their previews; they stay linked), embedProfiles?: true (carry the ICC profiles loaded from files that the document is tagged with; opening the file installs them where missing), pdfCompatible?: true (native: also carry a PDF of every artboard; false omits it) | true for .ai (false: blank pages, only the native document; a .ai file's compress, default true, is its compression.compressText), modified?: Unix seconds|null (the File Info modified date, and created date when there is none, a save to a file stamps; default now, recorded in the journal so a replay matches; null: leave the dates)} → {path, format, bytes, warnings, files?: [path…] (the file and its artboards' files), linked?: [path…] (images an SVG links to)}. Save writes one artboard, except a .ai file and EPS (below): a .ai file is a PDF-compatible file of every artboard carrying the native document (preserveEditing always on; PDF options flat or in options), which document.open restores exactly, on the active artboard (the app's, else the one the document opened on; with editingData: false, the first); separateArtboards writes more files, one per artboard. EPS takes the options of document.exportEps: by default one file whose bounding box fits the visible art (the first artboard when no art shows), carrying the whole editable document; useArtboards: true (also when artboard, artboards or range names artboards; file.formatOptions then reports it true) makes the chosen artboard the page, drawn with its background color (without Use Artboards, only the first artboard's page when no art shows has its color), and with two or more chosen the file at the path is the central file of the visible art (again the first artboard when none shows) and each artboard is also written to _.eps beside it, holding that artboard; selectedOnly is refused; its PostScript appearance flattens transparency, as EPS requires. Without a path it writes the document's own file in its own format: a document opened from or saved as SVG/PDF/EPS saves as that again (warnings name what the format loses). No path known (never saved, converted from an older version, restored by Data Recovery, or another format) → {dataBase64, format, name, folder?, warnings} and the document stays modified. Under automation read roots, a reply leaves out the document's own file outside them when no path is given (files and linked then name the files by their file names), and a suggested folder outside them; an svg or svgz save (a document whose own format is SVG or SVGZ included) is refused, before any file is written, while the document links a JavaScript file outside them (svgInteractivity; a URL is not checked), also by file.saveAs and file.saveCopy. A key that no save format takes, beside the path or in options, and an unknown key inside a PDF option's object or svg, are refused. A terminal filesystem permission/read-only refusal returns READ_ONLY_LOCATION: followed by the reason; recovery-copy failures keep their original error. Non-UI callers never open a save panel",
has_doc,
|s, p| save(s, SaveMode::Save, p)
),
diff --git a/crates/engine/src/cmd/fileio/screens.rs b/crates/engine/src/cmd/fileio/screens.rs
index dcf3d6c9ae..c07dd8854a 100644
--- a/crates/engine/src/cmd/fileio/screens.rs
+++ b/crates/engine/src/cmd/fileio/screens.rs
@@ -18,7 +18,7 @@ use vectorcraft_geom::Rect;
use super::super::{EngineError, NodeId, Result, Session, bad, bool_or, str_param};
use super::encode::{add_note, single_artboard};
-use super::export::{export_crop, isolated, selected_artwork, without_artboards};
+use super::export::{export_crop, export_reads, isolated, selected_artwork, without_artboards};
use super::naming::{self, NameParts};
use super::{
ARTBOARD_PARAMS, ArtboardPick, Format, Note, artboard_file_names_keeping_dots, create_dir, distinct_names_by, encode_with_notes, merge,
@@ -524,6 +524,77 @@ fn rows_of(s: &Session, p: &Value, preset: Option<&str>) -> Result> {
Ok(rows)
}
+/// A note for each option of a format row (`formats`) or of a format's `settings` in the request
+/// `asked` that some format takes but the file the row writes, or every row of that format writes,
+/// doesn't read (`{format: png, quality: 80}`), in the wording of a save's note and named as the
+/// request names it (`formats[0].quality`, `settings.png.quality`). The rows and settings the
+/// export writes with are those of `p` (`asked` with a preset or the remembered settings laid
+/// under it). A row's own keys (`scale`, `suffix`…), the keys the export reads for every format
+/// (`includeArtboardFill`, `richBlack`, `params`) and an artboard choice (the export picks the
+/// artboards) get none, and so does an exporter plug-in's format.
+pub(crate) fn unused_option_notes(asked: &Value, p: &Value) -> Vec {
+ let listed = |k: &str| super::FORMATS.iter().filter(|f| f.write).flat_map(|f| f.options).any(|o| o.name == k);
+ let own = |k: &str| {
+ ROW_KEYS.contains(&k) || ARTBOARD_PARAMS.contains(&k) || matches!(k, "useArtboards" | "includeArtboardFill" | "richBlack" | "params")
+ };
+ // The keys of `given` that `f`, writing with `options` (the format's settings and the row's
+ // own), doesn't read.
+ let unread = |f: &Format, given: &Map, options: &Value| -> Vec {
+ if vectorcraft_plugins::export::by_format(f.id).is_some() {
+ return Vec::new();
+ }
+ let legacy = super::save::legacy_file(C, f, options);
+ given.iter().filter(|(k, v)| !v.is_null() && !own(k) && listed(k) && !export_reads(f, legacy, k)).map(|(k, _)| k.clone()).collect()
+ };
+ let note = |key: String, f: &Format, options: &Value| super::save::unused_option_note(&key, &super::save::written_as(C, f, options));
+ let settings = p.get("settings").and_then(Value::as_object);
+ let of_format = |id: &str| settings.and_then(|s| s.get(id)).and_then(Value::as_object).cloned().unwrap_or_default();
+ // A row's format and the options it writes with.
+ let row_of = |row: &Value| -> Option<(&'static Format, Value)> {
+ let (Some(row), Ok(f)) = (row.as_object(), writable(C, Some(str_param(row, "format").unwrap_or("png")), None)) else { return None };
+ let mut options = of_format(f.id);
+ options.extend(row.iter().map(|(k, v)| (k.clone(), v.clone())));
+ Some((f, Value::Object(options)))
+ };
+ let mut out: Vec = Vec::new();
+ if let Some(Value::Array(rows)) = asked.get("formats") {
+ for (i, row) in rows.iter().enumerate() {
+ let (Some((f, options)), Some(given)) = (row_of(row), row.as_object()) else { continue };
+ out.extend(unread(f, given, &options).into_iter().map(|k| note(format!("formats[{i}].{k}"), f, &options)));
+ }
+ }
+ // The rows written, each with its own keys: a format's settings are read as each of its rows
+ // writes them (a row may name another `.ai` version), else as they are.
+ let written: Vec<(&Format, Value, Map)> = match p.get("formats") {
+ Some(Value::Array(rows)) => {
+ rows.iter().filter_map(|row| row_of(row).map(|(f, options)| (f, options, row.as_object().cloned().unwrap_or_default()))).collect()
+ }
+ _ => Vec::new(),
+ };
+ for (id, given) in asked.get("settings").and_then(Value::as_object).into_iter().flatten() {
+ let (Some(given), Ok(f)) = (given.as_object(), writable(C, Some(id), None)) else { continue };
+ let mut rows: Vec<(&Value, Option<&Map>)> =
+ written.iter().filter(|(g, ..)| g.id == f.id).map(|(_, o, row)| (o, Some(row))).collect();
+ let alone = Value::Object(of_format(f.id));
+ if rows.is_empty() {
+ rows.push((&alone, None));
+ }
+ let unread_by: Vec> = rows.iter().map(|(o, _)| unread(f, given, o)).collect();
+ // A setting a row reads and doesn't give itself is used. One no row uses is noted when a
+ // row doesn't read it, naming the first such row's file.
+ for (k, v) in given {
+ // A row's own key, null too, replaces the setting.
+ let overridden = |row: Option<&Map>| row.is_some_and(|r| r.contains_key(k));
+ let used = rows.iter().zip(&unread_by).any(|((_, row), u)| !u.contains(k) && !overridden(*row));
+ let unread_row = rows.iter().zip(&unread_by).find(|(_, u)| u.contains(k));
+ if let (false, false, Some(((options, _), _))) = (v.is_null(), used, unread_row) {
+ out.push(note(format!("settings.{id}.{k}"), f, options));
+ }
+ }
+ }
+ out
+}
+
/// Check the export settings in `p` (formats, preset, settings…) as an export would read them.
pub(crate) fn check_settings(s: &Session, p: &Value) -> Result<()> {
rows_of(s, p, ScreenParams::of(p)?.preset.as_deref()).map(|_| ())
@@ -646,9 +717,10 @@ impl Output {
pub(super) fn export_for_screens(s: &mut Session, p: &Value) -> Result {
// A folder the client gave, as against the one its `exportPreset` keeps.
let given = p.get("folder").is_some();
+ let asked = p;
let p = with_preset(s, p)?;
let hidden = if given { None } else { str_param(&p, "folder").filter(|f| !f.is_empty() && !super::access::shown(f)) };
- let mut r = export(s, &p).map_err(|e| match hidden {
+ let mut r = export(s, &p, asked).map_err(|e| match hidden {
Some(folder) => super::access::naming_in_error(e, folder),
None => e,
})?;
@@ -1022,9 +1094,12 @@ pub(crate) fn with_export_preset(s: &Session, cmd: &str, settings: &mut Map Result {
+/// first ([`super::note_texts`]), and a note for each option of the request `asked` (`p` before a
+/// preset or the remembered settings were laid under it) that the files the rows write don't read
+/// ([`unused_option_notes`]).
+pub(crate) fn export(s: &mut Session, p: &Value, asked: &Value) -> Result {
check_passwords(p)?;
+ let unused = unused_option_notes(asked, p);
let o = ScreenParams::of(p)?;
let rows = rows_of(s, p, o.preset.as_deref())?;
o.one_source()?;
@@ -1054,7 +1129,7 @@ pub(crate) fn export(s: &mut Session, p: &Value) -> Result {
// Include Artboard Fill off: the encoders leave the background colors out.
let fill = if o.include_artboard_fill == Some(false) { json!({ "includeArtboardFill": false }) } else { json!({}) };
let rows = rows.into_iter().map(|r| Row { options: merge(with_black_output(s, &r.options), fill.clone()), ..r }).collect();
- let export = ScreenExport { cmd: C, o, rows, naming, items, hidden: None };
+ let export = ScreenExport { cmd: C, o, rows, naming, items, unused, hidden: None };
match &mut s.screens_hand_off {
Some(slot) => {
*slot = Some(export);
@@ -1075,6 +1150,8 @@ pub struct ScreenExport {
rows: Vec,
naming: Naming,
items: Vec- ,
+ /// The notes on options the files don't read ([`unused_option_notes`]).
+ unused: Vec
,
/// The export folder and the name the request that prepared the export may see it by, when the
/// folder came from the `exportPreset` and is outside that request's read roots: the reply and
/// the errors of [`ScreenExport::write`] name it as [`export_for_screens`] does.
@@ -1139,14 +1216,17 @@ impl ScreenExport {
/// [`ScreenExport::write`].
fn encode_and_write(export: ScreenExport, writer: &mut dyn FnMut(&str, &[u8]) -> Result<()>) -> Result {
- let ScreenExport { o, rows, naming, items, .. } = export;
+ let ScreenExport { o, rows, naming, items, unused, .. } = export;
let folder = o.folder.as_deref().filter(|f| !f.is_empty());
if let Some(dir) = folder {
create_dir(dir)?;
}
let mut out = Output::default();
- // The encoders' notes for every file, each once.
+ // The notes on the options, then the encoders' notes for every file, each once.
let mut notes: Vec = Vec::new();
+ for text in unused {
+ add_note(&mut notes, Note::setting(text));
+ }
// Include Bleed: a grown artboard already holds the bleed. Assets, layers and a selection are
// written on a page of their own bounds, which it doesn't grow.
let bleed_on_page = o.include_bleed && o.assets.is_none() && o.layers.is_none() && o.selection.is_none();
diff --git a/crates/engine/src/cmd/fileio/tests.rs b/crates/engine/src/cmd/fileio/tests.rs
index 71c1bd06e5..d17af84b66 100644
--- a/crates/engine/src/cmd/fileio/tests.rs
+++ b/crates/engine/src/cmd/fileio/tests.rs
@@ -748,8 +748,8 @@ fn a_native_save_streams_the_bytes_the_encoder_makes() {
}
/// An option given beside the path (an SVG option flat or in `svg`) and in `options` with different
-/// values is refused before anything is written; `compress`, `version`, `preview` or `precision` beside the path of a format
-/// that doesn't read it is named in the warnings.
+/// values is refused before anything is written; a native option beside the path of a format that doesn't read it is named
+/// in the warnings.
#[test]
fn a_save_refuses_disagreeing_options_and_names_unread_native_ones() {
let d = tmp_dir("save-options");
diff --git a/crates/engine/src/cmd/fileio/tests_exportas.rs b/crates/engine/src/cmd/fileio/tests_exportas.rs
index cc1345060d..1908c39685 100644
--- a/crates/engine/src/cmd/fileio/tests_exportas.rs
+++ b/crates/engine/src/cmd/fileio/tests_exportas.rs
@@ -280,3 +280,102 @@ fn note_texts_give_the_losses_first_each_kind_in_order() {
assert_eq!(note_texts(&[Note::setting("s1"), Note::setting("s2")]), ["s1", "s2"]);
assert!(note_texts(&[]).is_empty());
}
+
+/// A save notes the `.ai` options its file doesn't read as an export does: a PDF-compatible file
+/// doesn't read the legacy file's options or `preserveEditing`, and a legacy file reads its own
+/// options only. The notes cover the options beside the path and in `options`.
+#[test]
+fn a_save_notes_the_ai_options_its_file_does_not_read() {
+ let (mut s, notes) = (with_art(), unused);
+ let r = s
+ .execute("file.saveCopy", &json!({"format": "ai", "outlineText": true, "preserveEditing": false, "options": {"hiddenObjects": "omit"}}))
+ .unwrap();
+ assert_eq!(
+ notes(&r),
+ [
+ "`outlineText` was not used: PDF-compatible .ai files don't read it",
+ "`preserveEditing` was not used: PDF-compatible .ai files don't read it",
+ "`options.hiddenObjects` was not used: PDF-compatible .ai files don't read it"
+ ],
+ "{r}"
+ );
+ let p = json!({"format": "ai", "aiVersion": "7", "compress": true, "marks": {"trim": true}, "options": {"pdfCompatible": false}});
+ let r = s.execute("file.saveCopy", &p).unwrap();
+ assert_eq!(
+ notes(&r),
+ [
+ "`compress` was not used: legacy .ai (version 7) files don't read it",
+ "`marks` was not used: legacy .ai (version 7) files don't read it",
+ "`options.pdfCompatible` was not used: legacy .ai (version 7) files don't read it"
+ ],
+ "{r}"
+ );
+ // The file's own options get none.
+ for p in [
+ json!({"format": "ai", "aiVersion": "7", "outlineText": true, "options": {"hiddenObjects": "omit", "range": "1"}}),
+ json!({"format": "ai", "compress": true, "marks": {"trim": true}, "options": {"editingData": false}}),
+ json!({"format": "pdf", "marks": {"trim": true}, "preserveEditing": false}),
+ ] {
+ let r = s.execute("file.saveCopy", &p).unwrap();
+ assert!(notes(&r).is_empty(), "{p}: {r}");
+ }
+}
+
+/// A save notes an option beside the path that only another save format takes, as it notes one in
+/// `options`.
+#[test]
+fn a_save_notes_an_option_beside_the_path_that_only_another_save_format_takes() {
+ let (mut s, notes) = (with_art(), unused);
+ let r = s.execute("file.saveCopy", &json!({"format": "vectorcraft", "marks": {"trim": true}, "svg": {"minify": true}})).unwrap();
+ assert_eq!(notes(&r), ["`marks` was not used: VectorCraft files don't read it", "`svg` was not used: VectorCraft files don't read it"], "{r}");
+}
+
+/// The `.ai` options a save's file doesn't read keep the values the document's last save in that
+/// format gave them, so a save from Save Options, which sends only the options the file reads,
+/// leaves the other version's choices as they were.
+#[test]
+fn a_save_keeps_the_ai_options_its_file_does_not_read_from_the_last_save() {
+ let mut s = with_art();
+ let dir = tmp_dir("ai-kept");
+ let path = dir.join("kept.ai").to_string_lossy().to_string();
+ let value = |s: &mut Session, key: &str| s.execute("file.formatOptions", &json!({"format": "ai"})).unwrap()["options"][key]["value"].clone();
+ s.execute("file.saveAs", &json!({"path": path, "format": "ai", "options": {"aiVersion": "7", "outlineText": true}})).unwrap();
+ s.execute("file.saveAs", &json!({"path": path, "format": "ai", "options": {"aiVersion": "pdf", "editingData": false}})).unwrap();
+ assert_eq!(value(&mut s, "outlineText"), json!(true));
+ assert_eq!(value(&mut s, "editingData"), json!(false));
+ s.execute("file.saveAs", &json!({"path": path, "format": "ai", "options": {"aiVersion": "7", "hiddenObjects": "omit"}})).unwrap();
+ assert_eq!(value(&mut s, "editingData"), json!(false));
+ // An option the request gives is the one saved.
+ s.execute("file.saveAs", &json!({"path": path, "format": "ai", "options": {"aiVersion": "7", "outlineText": false}})).unwrap();
+ assert_eq!(value(&mut s, "outlineText"), json!(false));
+ // The kept options are neither written nor sent again: a plain save's options (which a retry
+ // in another folder sends to Save As) hold only what the legacy file reads.
+ for p in [json!({}), json!({"options": {"aiVersion": "7"}})] {
+ let plan = save_plan(&s, SaveMode::Save, &p).unwrap();
+ assert!(!plan.options.contains_key("editingData") && !plan.options.contains_key("compress"), "{:?}", plan.options);
+ assert_eq!(plan.kept.get("editingData"), Some(&json!(false)));
+ assert!(plan.notes.is_empty(), "{:?}", plan.notes);
+ let retry = json!({"path": path, "format": "ai", "options": plan.options});
+ let r = s.execute("file.saveAs", &retry).unwrap();
+ assert!(!r["warnings"].to_string().contains("was not used"), "{r}");
+ assert_eq!(value(&mut s, "editingData"), json!(false));
+ }
+ std::fs::remove_dir_all(&dir).unwrap();
+}
+
+/// The first `.ai` save of a document whose own file is another format remembers only the options
+/// it was given: nothing from that format's options is kept.
+#[test]
+fn a_first_ai_save_keeps_only_the_options_it_is_given() {
+ let mut s = with_art();
+ let dir = tmp_dir("ai-first");
+ let native = dir.join("first.vectorcraft").to_string_lossy().to_string();
+ let path = dir.join("first.ai").to_string_lossy().to_string();
+ s.execute("file.saveAs", &json!({"path": native, "options": {"compress": false}})).unwrap();
+ let plan = save_plan(&s, SaveMode::SaveAs, &json!({"path": path, "format": "ai", "options": {"aiVersion": "7"}})).unwrap();
+ assert!(plan.kept.is_empty(), "{:?}", plan.kept);
+ s.execute("file.saveAs", &json!({"path": path, "format": "ai", "options": {"aiVersion": "7"}})).unwrap();
+ let o = s.execute("file.formatOptions", &json!({"format": "ai"})).unwrap();
+ assert_eq!(o["options"]["compress"]["value"], json!(true), "{o}");
+ std::fs::remove_dir_all(&dir).unwrap();
+}
diff --git a/crates/engine/src/cmd/fileio/tests_screens.rs b/crates/engine/src/cmd/fileio/tests_screens.rs
index 51954d8da3..1746be6c97 100644
--- a/crates/engine/src/cmd/fileio/tests_screens.rs
+++ b/crates/engine/src/cmd/fileio/tests_screens.rs
@@ -1163,3 +1163,93 @@ fn single_file_pdfs_hold_every_chosen_artboard() {
assert_eq!(names(&r), ["Artboard-1.pdf", "Artboard-2.pdf"]);
assert!(s.execute("document.exportForScreens", &json!({"pdfs": "both", "formats": [{"format": "pdf"}]})).is_err());
}
+
+/// The warnings of reply `r` that name an option the file doesn't read.
+fn unused(r: &Value) -> Vec {
+ r["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).filter(|w| w.contains("was not used")).map(str::to_string).collect()
+}
+
+/// An option of a format row, or of a format's settings, that only another format reads is
+/// accepted, and the reply's warnings name it as the request does, in the words of a save's note.
+#[test]
+fn a_row_option_only_another_format_reads_gets_a_note() {
+ let mut s = session(1);
+ let p = json!({
+ "formats": [{"format": "png", "quality": 80}, {"format": "jpg", "quality": 80}, {"format": "ai", "aiVersion": "7", "compress": true}],
+ "settings": {"png": {"quality": 50, "interlaced": true}, "svg": {"interlaced": true}},
+ });
+ let r = export(&mut s, p);
+ assert_eq!(
+ unused(&r),
+ [
+ "`formats[0].quality` was not used: PNG files don't read it",
+ "`formats[2].compress` was not used: legacy .ai (version 7) files don't read it",
+ "`settings.png.quality` was not used: PNG files don't read it",
+ "`settings.svg.interlaced` was not used: SVG files don't read it"
+ ],
+ "{r}"
+ );
+ // A row's own keys, the keys every row takes and the artboard choices get none.
+ let p = json!({"formats": [
+ {"format": "png", "scale": "2x", "suffix": "@2x", "folder": "x", "includeArtboardFill": false, "artboard": 0, "useArtboards": true},
+ {"format": "pdf", "marks": {"trim": true}, "preset": "High Quality Print"},
+ {"format": "vectorcraft", "compress": true, "range": "1"},
+ ]});
+ let r = export(&mut s, p);
+ assert!(unused(&r).is_empty(), "{r}");
+ // A format's settings are read as each row of that format writes them: a row can name another
+ // `.ai` version than the settings do.
+ let r = export(&mut s, json!({"formats": [{"format": "ai", "aiVersion": "7"}], "settings": {"ai": {"marks": {"trim": true}}}}));
+ assert_eq!(unused(&r), ["`settings.ai.marks` was not used: legacy .ai (version 7) files don't read it"], "{r}");
+ let r = export(&mut s, json!({"formats": [{"format": "ai", "aiVersion": "pdf"}], "settings": {"ai": {"aiVersion": "7", "compress": true}}}));
+ assert!(unused(&r).is_empty(), "{r}");
+ // A setting another row of the format reads gets none; one no row reads names the first row's
+ // file.
+ let rows = json!([{"format": "ai", "aiVersion": "7"}, {"format": "ai"}]);
+ let r = export(&mut s, json!({"formats": rows, "settings": {"ai": {"editingData": false}}}));
+ assert!(unused(&r).is_empty(), "{r}");
+ let r = export(&mut s, json!({"formats": rows, "settings": {"ai": {"quality": 80}}}));
+ assert_eq!(unused(&r), ["`settings.ai.quality` was not used: legacy .ai (version 7) files don't read it"], "{r}");
+ // A row that gives the key itself doesn't use the setting.
+ let rows = json!([{"format": "ai", "aiVersion": "7"}, {"format": "ai", "editingData": true}]);
+ let r = export(&mut s, json!({"formats": rows, "settings": {"ai": {"editingData": false}}}));
+ assert_eq!(unused(&r), ["`settings.ai.editingData` was not used: legacy .ai (version 7) files don't read it"], "{r}");
+ let rows = json!([{"format": "ai", "aiVersion": "7"}, {"format": "ai", "editingData": null}]);
+ let r = export(&mut s, json!({"formats": rows, "settings": {"ai": {"editingData": false}}}));
+ assert_eq!(unused(&r), ["`settings.ai.editingData` was not used: legacy .ai (version 7) files don't read it"], "{r}");
+ // An export writes one file per row and item: a native row doesn't read `separateArtboards`.
+ let r = export(&mut s, json!({"formats": [{"format": "vectorcraft", "separateArtboards": true}]}));
+ assert_eq!(unused(&r), ["`formats[0].separateArtboards` was not used: VectorCraft files don't read it"], "{r}");
+}
+
+/// The Asset Export panel's export notes the options of its request, not those of the settings
+/// the document remembers.
+#[test]
+fn an_asset_export_notes_the_options_it_is_given() {
+ let mut s = session(1);
+ s.execute("select.all", &json!({})).unwrap();
+ s.execute("assets.add", &json!({"multiple": false})).unwrap();
+ s.execute("assets.settings.set", &json!({"formats": [{"format": "png", "quality": 80}]})).unwrap();
+ let r = s.execute("assets.export", &json!({})).unwrap();
+ assert!(unused(&r).is_empty(), "{r}");
+ let r = s.execute("assets.export", &json!({"formats": [{"format": "svg", "quality": 80}]})).unwrap();
+ assert_eq!(unused(&r), ["`formats[0].quality` was not used: SVG files don't read it"], "{r}");
+ // Settings given are read as the remembered rows write them.
+ s.execute("assets.settings.set", &json!({"formats": [{"format": "ai", "aiVersion": "7"}]})).unwrap();
+ let r = s.execute("assets.export", &json!({"settings": {"ai": {"editingData": false}}})).unwrap();
+ assert_eq!(unused(&r), ["`settings.ai.editingData` was not used: legacy .ai (version 7) files don't read it"], "{r}");
+ s.execute("assets.settings.set", &json!({"formats": [{"format": "png", "quality": 80}]})).unwrap();
+ // So does the export a client prepares.
+ let p = json!({"formats": [{"format": "svg", "quality": 80}]});
+ let r = s.prepare_screen_export("assets.export", &p).unwrap().write(&mut |_: &str, _: &[u8]| Ok(())).unwrap();
+ assert_eq!(unused(&r), ["`formats[0].quality` was not used: SVG files don't read it"], "{r}");
+}
+
+/// A preset's options add no notes: Export for Screens notes the options of its request.
+#[test]
+fn an_export_preset_adds_no_notes() {
+ let mut s = session(1);
+ s.execute("document.exportPresets.save", &json!({"name": "Mixed", "settings": {"formats": [{"format": "png", "quality": 80}]}})).unwrap();
+ let r = export(&mut s, json!({"exportPreset": "Mixed"}));
+ assert!(unused(&r).is_empty(), "{r}");
+}
diff --git a/crates/engine/src/cmd/recolor.rs b/crates/engine/src/cmd/recolor.rs
index ede74545b6..07f1f4c46b 100644
--- a/crates/engine/src/cmd/recolor.rs
+++ b/crates/engine/src/cmd/recolor.rs
@@ -1724,7 +1724,7 @@ fn apply(s: &mut Session, p: &Value) -> Result {
let mut out = json!({ "changed": changed, "swatches": added });
not_changed.report(&mut out);
if let Some(e) = edit {
- e.defaults(s);
+ e.defaults(s)?;
out["relinked"] = json!(e.relinked());
}
if converted {
diff --git a/crates/engine/src/cmd/recovery.rs b/crates/engine/src/cmd/recovery.rs
index de4d145840..e5107c247b 100644
--- a/crates/engine/src/cmd/recovery.rs
+++ b/crates/engine/src/cmd/recovery.rs
@@ -1176,6 +1176,7 @@ pub fn jobs(s: &mut Session, store: &Arc) -> Result<(Vec Result {
super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(C, e))?;
Ok(unlinked)
})?;
- map_default_paints(s, &mut unlink);
+ map_default_paints(s, &mut unlink)?;
Ok(json!({"deleted": names, "unlinked": unlinked}))
}
@@ -660,20 +660,41 @@ impl Relink {
renamed || recolored
}
- fn defaults(&self, s: &mut Session) {
- map_default_paints(s, &mut |c, l, t| self.apply(c, l, t));
+ fn defaults(&self, s: &mut Session) -> Result<()> {
+ map_default_paints(s, &mut |c, l, t| self.apply(c, l, t))
}
}
-/// Apply a swatch-link rewrite to the default fill and stroke for new art, except during a live
-/// preview (Cancel rolls the document back, not the session).
-pub(super) fn map_default_paints(s: &mut Session, f: &mut LinkFn) {
- if s.in_interaction() {
- return;
+/// Apply a swatch-link rewrite to the fill and stroke new art takes and to the paints of its
+/// template appearance, except during a live preview (Cancel rolls the document back, not the
+/// session). A `command.batch` step rewrites them as the command run on its own does: a failed
+/// batch, and a canceled batch preview, put back the paints the batch started with. A change is
+/// recorded with the step just made, so Undo and Redo put these paints back with the document.
+pub(super) fn map_default_paints(s: &mut Session, f: &mut LinkFn) -> Result<()> {
+ if s.in_interaction() && s.batch_stash.is_none() {
+ return Ok(());
}
- for p in [&mut s.paint.fill, &mut s.paint.stroke] {
- p.map_links(f);
+ let start = super::colormgmt::new_art_colors_before(s)?;
+ let pd = &mut s.paint;
+ let template = pd.appearance.iter_mut().flat_map(|a| a.items.iter_mut()).map(|it| match it {
+ vectorcraft_doc::AppearanceItem::Fill(f) => &mut f.paint,
+ vectorcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint,
+ });
+ let mut changed = false;
+ for p in [&mut pd.fill, &mut pd.stroke].into_iter().chain(template) {
+ changed |= p.map_links(f);
}
+ if changed {
+ super::colormgmt::record_new_art_colors(s, start)?;
+ // The step takes the paints the command finally leaves, after the selection's appearance
+ // is inherited.
+ let st = s.doc()?;
+ if st.interaction.is_none() {
+ let uid = st.uid;
+ s.batch_paint_docs.push(uid);
+ }
+ }
+ Ok(())
}
/// Apply a swatch-link rewrite to the document ([`Document::map_solid_paints`]) and to the art of
@@ -888,7 +909,7 @@ fn swatch_edit(s: &mut Session, p: &Value) -> Result {
super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(C, e))?;
Ok(relinked)
})?;
- relink.defaults(s);
+ relink.defaults(s)?;
Ok(json!({"name": to, "relinked": relinked}))
}
@@ -1084,7 +1105,7 @@ fn swatch_merge(s: &mut Session, p: &Value) -> Result {
super::paint::check_colorized_kept(colorized_ok, d).map_err(|e| bad(C, e))?;
Ok(relinked)
})?;
- map_default_paints(s, &mut relink);
+ map_default_paints(s, &mut relink)?;
Ok(json!({"name": keep, "merged": merged, "relinked": relinked}))
}
@@ -1141,7 +1162,7 @@ fn swatch_spot_options(s: &mut Session, p: &Value) -> Result {
let relinked = map_swatch_links(d, &mut follow_bases(&bases));
Ok((relinked, bases))
})?;
- map_default_paints(s, &mut follow_bases(&bases));
+ map_default_paints(s, &mut follow_bases(&bases))?;
Ok(json!({"useLab": use_lab, "relinked": relinked}))
}
@@ -1165,8 +1186,11 @@ pub(super) struct GroupEdit {
impl GroupEdit {
/// Apply the link changes to the default fill and stroke for new art.
- pub(super) fn defaults(&self, s: &mut Session) {
- self.relinks.iter().for_each(|r| r.defaults(s));
+ pub(super) fn defaults(&self, s: &mut Session) -> Result<()> {
+ if self.relinks.is_empty() {
+ return Ok(());
+ }
+ map_default_paints(s, &mut |c, l, t| self.relinks.iter().any(|r| r.apply(c, l, t)))
}
/// How many paints linked to the group's swatches the rewrite changed.
@@ -1242,6 +1266,6 @@ fn swatch_edit_group(s: &mut Session, p: &Value) -> Result {
colors.into_iter().enumerate().map(|(i, c)| if old.get(i) == Some(&Some(c)) { c } else { super::paint::new_color(s, p, c) }).collect();
let rename = str_param(p, "rename");
let e = s.edit("Edit Color Group", |d, _| rewrite_group(d, group, &colors, rename, C))?;
- e.defaults(s);
+ e.defaults(s)?;
Ok(json!({"name": e.name, "swatches": e.swatches, "relinked": e.relinked}))
}
diff --git a/crates/engine/src/cmd/swatchlib.rs b/crates/engine/src/cmd/swatchlib.rs
index 8101cfff97..ed712d8509 100644
--- a/crates/engine/src/cmd/swatchlib.rs
+++ b/crates/engine/src/cmd/swatchlib.rs
@@ -980,7 +980,7 @@ fn reset_defaults(s: &mut Session, p: &Value) -> Result {
Ok(added)
})?;
if replace {
- map_default_paints(s, &mut |_, l, _| unlink_dead(&live, l));
+ map_default_paints(s, &mut |_, l, _| unlink_dead(&live, l))?;
}
Ok(json!({ "added": added }))
}
diff --git a/crates/engine/src/cmd/typecmd/area_tests.rs b/crates/engine/src/cmd/typecmd/area_tests.rs
index c68334e2c0..db1712a7f8 100644
--- a/crates/engine/src/cmd/typecmd/area_tests.rs
+++ b/crates/engine/src/cmd/typecmd/area_tests.rs
@@ -1688,10 +1688,9 @@ fn warichu_is_a_character_attribute_set_saved_and_exported() {
assert!(["warichu", "warichuLines", "warichuGap", "warichuScale", "warichuAlign"].iter().all(|k| saved.get(*k).is_none()), "{saved}");
let plain_svg = vectorcraft_svg::export(&s.doc().unwrap().doc, &Default::default());
let plain_pdf = vectorcraft_pdf::export(&s.doc().unwrap().doc, &vectorcraft_pdf::PdfOptions::uncompressed()).unwrap();
- let text_ops = |pdf: &[u8]| {
- let s = String::from_utf8_lossy(pdf);
- s.contains("Tj") || s.contains("TJ")
- };
+ // Only text objects count: the file ID in the trailer and the XMP is base64 that changes each
+ // second and now and then spells `Tj` or `TJ`.
+ let text_ops = |pdf: &[u8]| String::from_utf8_lossy(pdf).lines().any(|l| l.starts_with("BT ") && (l.contains("Tj") || l.contains("TJ")));
assert!(plain_svg.contains(" bool {
- self.any_art_to_fit_with(indices, (vectorcraft_plugins::registry::revision(), vectorcraft_text::FontDb::global().generation()))
+ self.any_art_to_fit_with(indices, (vectorcraft_plugins::registry::revision(), vectorcraft_text::layout_inputs()))
}
- /// [`Self::any_art_to_fit`], with `outside` the generations of the plug-ins and the fonts.
+ /// [`Self::any_art_to_fit`], with `outside` the generations of the plug-ins and the layout inputs
+ /// ([`vectorcraft_text::layout_inputs`]).
pub(crate) fn any_art_to_fit_with(&self, indices: &[usize], outside: (u64, u64)) -> bool {
// Measuring runs renderer and plug-in code; should it ever ask again while measuring, it
// is measured without being kept rather than borrowed twice.
diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs
index 5ceaf86aaf..a87b47be0e 100644
--- a/crates/engine/src/session/documents.rs
+++ b/crates/engine/src/session/documents.rs
@@ -25,9 +25,11 @@ impl Session {
// Clicks find an image's own fills and strokes where their effects draw them.
vectorcraft_doc::hit::set_image_paint(vectorcraft_render::effects::image_paint);
// Clicks find a blend's steps and an envelope's distorted art where the canvas draws them,
- // evaluated again when the fonts or the plug-ins change.
+ // evaluated again when the fonts, the hyphenation exceptions or the plug-ins change.
vectorcraft_doc::hit::set_live_art(vectorcraft_render::effects::expand_live, || {
- vectorcraft_text::FontDb::global().generation().rotate_left(32) ^ vectorcraft_render::effects::plugin_revision()
+ let mut h = std::hash::DefaultHasher::new();
+ std::hash::Hash::hash(&(vectorcraft_text::layout_inputs(), vectorcraft_render::effects::plugin_revision()), &mut h);
+ std::hash::Hasher::finish(&h)
});
// Envelopes map type from the bounds of its glyph outlines, underlines and strikethroughs.
vectorcraft_text::register_envelope_ink();
diff --git a/crates/engine/src/tests/bboxrotate.rs b/crates/engine/src/tests/bboxrotate.rs
index aa1c14885c..6dc1c9b410 100644
--- a/crates/engine/src/tests/bboxrotate.rs
+++ b/crates/engine/src/tests/bboxrotate.rs
@@ -560,6 +560,125 @@ fn reset_bounding_box_then_release_intertwine() {
reset_then_release("object.intertwine.make", "object.intertwine.release");
}
+/// A symbol of two rectangles, the first turned `inner` degrees before the symbol is made, placed
+/// as an instance that is then turned 20°.
+fn turned_instance(inner: f64) -> (Session, NodeId) {
+ let mut s = session();
+ let a = rect(&mut s, 100.0, 100.0, 100.0, 50.0);
+ let b = rect(&mut s, 150.0, 220.0, 100.0, 50.0);
+ if inner != 0.0 {
+ sel(&mut s, &[a]);
+ s.execute("object.rotate", &json!({"angle": inner})).unwrap();
+ }
+ sel(&mut s, &[a, b]);
+ let inst = id_of(&s.execute("symbol.new", &json!({"name": "Pair"})).unwrap());
+ sel(&mut s, &[inst]);
+ s.execute("object.rotate", &json!({"angle": 20})).unwrap();
+ assert!(near(angle(&s, inst), 20.0));
+ (s, inst)
+}
+
+/// Break Link and Expand with the parameters that turn instance `inst` into its art.
+fn expand_instance(s: &mut Session, command: &str, inst: NodeId) {
+ sel(s, &[inst]);
+ let params = if command == "symbol.breakLink" { json!({"ids": [inst.0]}) } else { json!({"object": true, "fill": false, "stroke": false}) };
+ s.execute(command, ¶ms).unwrap();
+ assert!(s.doc().unwrap().doc.node(inst).is_none_or(|n| !matches!(n.kind, vectorcraft_doc::NodeKind::SymbolInstance { .. })), "{command}");
+}
+
+/// The angles of the objects inside the document's only top-level object, that object included.
+fn art_angles(s: &Session) -> Vec {
+ let d = &s.doc().unwrap().doc;
+ let layer = d.layers.first().unwrap();
+ let top = layer.children().unwrap();
+ assert_eq!(top.len(), 1);
+ let mut out = vec![];
+ subtree_angles(&top[0], &mut out);
+ out
+}
+
+#[test]
+fn reset_bounding_box_then_expand_symbol_instance() {
+ for command in ["symbol.breakLink", "object.expand"] {
+ let (mut s, inst) = turned_instance(0.0);
+ assert_eq!(s.execute("object.resetBoundingBox", &json!({})).unwrap()["changed"], 1);
+ assert_eq!(angle(&s, inst), 0.0);
+ expand_instance(&mut s, command, inst);
+ let angles = art_angles(&s);
+ assert!(angles.len() >= 2 && angles.iter().all(|a| near(*a, 0.0)), "{command}: {angles:?}");
+ s.execute("select.all", &json!({})).unwrap();
+ assert_eq!(selection_box(&s).angle, 0.0, "{command}");
+ }
+}
+
+#[test]
+fn expanding_a_turned_symbol_instance_keeps_its_angle() {
+ for command in ["symbol.breakLink", "object.expand"] {
+ let (mut s, inst) = turned_instance(0.0);
+ expand_instance(&mut s, command, inst);
+ let angles = art_angles(&s);
+ assert!(angles.len() >= 2 && angles.iter().all(|a| near(*a, 20.0)), "{command}: {angles:?}");
+ }
+}
+
+#[test]
+fn reset_bounding_box_then_expand_keeps_the_angles_inside_the_symbol() {
+ for command in ["symbol.breakLink", "object.expand"] {
+ let (mut s, inst) = turned_instance(30.0);
+ s.execute("object.resetBoundingBox", &json!({})).unwrap();
+ expand_instance(&mut s, command, inst);
+ // The turned rectangle keeps the 30° it had in the symbol; the rest stand square.
+ let mut angles = art_angles(&s);
+ angles.sort_by(f64::total_cmp);
+ assert!(near(*angles.last().unwrap(), 30.0) && angles[..angles.len() - 1].iter().all(|a| near(*a, 0.0)), "{command}: {angles:?}");
+ }
+}
+
+/// The group that an instance's opacity adds around the art of a one-object symbol stands as the
+/// instance's box does, turned or reset.
+#[test]
+fn expanding_a_faded_symbol_instance_gives_its_group_the_instance_box() {
+ for command in ["symbol.breakLink", "object.expand"] {
+ for reset in [false, true] {
+ let mut s = session();
+ let a = rect(&mut s, 100.0, 100.0, 100.0, 50.0);
+ sel(&mut s, &[a]);
+ let inst = id_of(&s.execute("symbol.new", &json!({"name": "One"})).unwrap());
+ sel(&mut s, &[inst]);
+ s.execute("object.rotate", &json!({"angle": 20})).unwrap();
+ s.execute("object.setProps", &json!({"ids": [inst.0], "opacity": 50})).unwrap();
+ if reset {
+ s.execute("object.resetBoundingBox", &json!({})).unwrap();
+ }
+ expand_instance(&mut s, command, inst);
+ let want = if reset { 0.0 } else { 20.0 };
+ let angles = art_angles(&s);
+ assert!(angles.len() == 2 && angles.iter().all(|a| near(*a, want)), "{command}, reset {reset}: {angles:?}");
+ }
+ }
+}
+
+/// A mirrored instance: turned 170° and then reflected, its box stands 180° from the box its
+/// transform gives in one step. The art takes the instance's box, and stands square after a reset.
+#[test]
+fn expanding_a_reflected_symbol_instance_keeps_its_box() {
+ for command in ["symbol.breakLink", "object.expand"] {
+ for reset in [false, true] {
+ let (mut s, inst) = turned_instance(0.0);
+ s.execute("object.rotate", &json!({"angle": 150})).unwrap();
+ s.execute("object.reflect", &json!({"axis": "horizontal"})).unwrap();
+ assert!(near(angle(&s, inst), -170.0), "{}", angle(&s, inst));
+ if reset {
+ s.execute("object.resetBoundingBox", &json!({})).unwrap();
+ }
+ let want = angle(&s, inst);
+ expand_instance(&mut s, command, inst);
+ let angles = art_angles(&s);
+ assert!(angles.len() >= 2 && angles.iter().all(|a| near(*a, want)), "{command}, reset {reset}: {angles:?}");
+ }
+ }
+}
+
#[test]
fn reset_bounding_box_then_release_linked_opacity_mask() {
let mut s = session();
diff --git a/crates/engine/src/tests/draw2.rs b/crates/engine/src/tests/draw2.rs
index c281fc69b8..d47ca0ccaf 100644
--- a/crates/engine/src/tests/draw2.rs
+++ b/crates/engine/src/tests/draw2.rs
@@ -184,6 +184,25 @@ fn pencil_alt_closes_path() {
assert!(ps[0].1.is_closed());
}
+/// A Pencil stroke drawn as a dome and closed with Alt at the release closes with a straight segment
+/// across the bottom, from where the stroke ends to where it starts: the path stays within where the
+/// pen went, rather than curving on past both ends and back like a hat's brim.
+#[test]
+fn pencil_alt_closes_a_stroke_straight_back_to_its_start() {
+ let mut s = session();
+ let pts: Vec<(f64, f64)> =
+ (0..=60).map(|i| f64::from(i) * std::f64::consts::PI / 60.0).map(|a| (300.0 - 150.0 * a.cos(), 400.0 - 200.0 * a.sin())).collect();
+ drag(&mut s, "pencil", &pts, Mods { alt: true, ..Default::default() });
+ let ps = paths(&s);
+ let sp = &ps[0].1.subpaths[0];
+ assert!(sp.closed);
+ let b = ps[0].1.bounds().unwrap();
+ assert!(b.x0 > 149.5 && b.x1 < 450.5 && b.y1 < 400.5, "within the stroke: {b:?}");
+ let (first, last) = (sp.anchors.first().unwrap(), sp.anchors.last().unwrap());
+ assert!(first.p.distance(Point::new(150.0, 400.0)) < 1e-6 && last.p.distance(Point::new(450.0, 400.0)) < 1e-6, "{first:?} {last:?}");
+ assert_eq!((last.h_out, first.h_in), (last.p, first.p), "a straight closing segment");
+}
+
/// #3799: with Shift held, a wavy Pencil drag draws one straight segment at 45° steps from the
/// Constrain Angle (here 30°: a drag at about 60° comes out at 75°).
#[test]
diff --git a/crates/engine/src/tests/params.rs b/crates/engine/src/tests/params.rs
index 6fb8d30467..8a33376075 100644
--- a/crates/engine/src/tests/params.rs
+++ b/crates/engine/src/tests/params.rs
@@ -572,6 +572,64 @@ fn a_malformed_required_number_is_not_reported_as_missing() {
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
}
+/// A required whole number given as something other than a whole number of 0 or more (`"3"`, `2.5`,
+/// `-1`, `true`, `[]`, `{}`) is refused as the wrong type, naming its key, not as a missing integer; one
+/// left out or given as null is still missing. An optional one left out or null keeps its default.
+/// Neither changes the document or records an undo step.
+#[test]
+fn a_malformed_required_integer_is_not_reported_as_missing() {
+ let mut s = Session::new();
+ s.execute("file.new", &json!({})).unwrap();
+ s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
+ let doc = s.doc().unwrap().doc.clone();
+ let undo = s.doc().unwrap().history.undo.len();
+ let with = |base: &serde_json::Value, key: &str, v: Option| {
+ let mut p = base.clone();
+ let o = p.as_object_mut().unwrap();
+ match v {
+ Some(v) => o.insert(key.into(), v),
+ None => o.remove(key),
+ };
+ p
+ };
+ let refused = |s: &mut Session, cmd: &str, p: &serde_json::Value, want: &str| match s.execute(cmd, p) {
+ Err(EngineError::BadParams { cmd: c, msg }) => assert_eq!((c.as_str(), msg.as_str()), (cmd, want), "{p}"),
+ other => panic!("{cmd} {p}: {other:?}"),
+ };
+ let ends = json!({"sourceDocument": 1, "grab": [0, 0], "at": [0, 0], "ids": [1]});
+ let cases = [
+ ("appearance.setItem", json!({"index": 0}), "index", "missing integer `index`"),
+ ("path.removeAnchor", json!({"id": 1, "anchor": 0}), "anchor", "missing integer `anchor`"),
+ ("path.split", json!({"id": 1, "anchor": 0}), "anchor", "need `anchor` or `segment` + `t`"),
+ ("addon.pen.removePoint", json!({"anchor": 0}), "anchor", "missing integer `anchor`"),
+ ("edit.dropSelection", ends, "sourceDocument", "missing integer `sourceDocument`"),
+ ("document.activate", json!({"index": 0}), "index", "missing index"),
+ ("document.move", json!({"to": 0}), "to", "missing `to`: a non-negative integer"),
+ ];
+ let malformed = [json!("3"), json!(2.5), json!(-1), json!(true), json!([]), json!({})];
+ for (cmd, base, key, missing) in &cases {
+ for v in &malformed {
+ refused(&mut s, cmd, &with(base, key, Some(v.clone())), &format!("`{key}` must be a whole number"));
+ }
+ for p in [with(base, key, None), with(base, key, Some(json!(null)))] {
+ refused(&mut s, cmd, &p, missing);
+ }
+ }
+ // An optional `subpath` given as anything else is refused the same way; left out or null, it is
+ // subpath 0 and the command goes on to read `anchor`.
+ let base = json!({"id": 1, "anchor": "3"});
+ for v in &malformed {
+ refused(&mut s, "path.removeAnchor", &with(&base, "subpath", Some(v.clone())), "`subpath` must be a whole number");
+ }
+ for p in [with(&base, "subpath", None), with(&base, "subpath", Some(json!(null)))] {
+ refused(&mut s, "path.removeAnchor", &p, "`anchor` must be a whole number");
+ }
+ assert_eq!(s.doc().unwrap().doc, doc);
+ assert_eq!(s.doc().unwrap().history.undo.len(), undo);
+ // A whole JSON number is a whole number.
+ s.execute("document.activate", &json!({"index": 0})).unwrap();
+}
+
/// A command that takes the keys of one that isn't checked yet isn't checked either.
#[test]
fn a_reference_to_an_unchecked_command_is_unchecked() {
diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs
index 1010d9b547..d88b8250a2 100644
--- a/crates/engine/src/tests/prefs.rs
+++ b/crates/engine/src/tests/prefs.rs
@@ -693,7 +693,6 @@ fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() {
("blobBrush", Cursor::BlobBrush),
("shaper", Cursor::Shaper),
("pencil", Cursor::PencilNew),
- ("smooth", Cursor::Smooth),
("pathEraser", Cursor::PathEraser),
("join", Cursor::Join),
("eraser", Cursor::Eraser),
@@ -738,6 +737,27 @@ fn use_precise_cursors_swaps_the_tools_own_pointers_for_a_crosshair() {
}
}
+/// With no selected path, the Smooth tool shows Not Allowed, and Use Precise Cursors leaves it so; with a
+/// selected path, it shows its own pointer, or a crosshair with the preference on.
+#[test]
+fn use_precise_cursors_keeps_the_smooth_tools_not_allowed_pointer() {
+ use vectorcraft_tools::{Cursor, Mods};
+ let mut s = Session::new();
+ s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
+ let at = vectorcraft_geom::Point::new(300.0, 300.0);
+ s.select_tool("smooth", ViewInfo::default()).unwrap();
+ for precise in [false, true] {
+ s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": precise})).unwrap();
+ assert_eq!(s.cursor(at, Mods::default(), ViewInfo::default()), Cursor::NotAllowed, "precise {precise}");
+ }
+ let id = s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
+ s.execute("select.set", &json!({"ids": [id]})).unwrap();
+ for (precise, c) in [(false, Cursor::Smooth), (true, Cursor::Crosshair)] {
+ s.execute("prefs.set", &json!({"key": "usePreciseCursors", "value": precise})).unwrap();
+ assert_eq!(s.cursor(at, Mods::default(), ViewInfo::default()), c, "precise {precise}");
+ }
+}
+
/// With Alt held while drawing, the Pencil and the Paintbrush show their closing cursors, and Use Precise Cursors
/// turns those into a crosshair too.
#[test]
diff --git a/crates/engine/src/tests/swatches.rs b/crates/engine/src/tests/swatches.rs
index a80769dd98..8abaec348c 100644
--- a/crates/engine/src/tests/swatches.rs
+++ b/crates/engine/src/tests/swatches.rs
@@ -1075,6 +1075,146 @@ fn deleting_a_global_swatch_unlinks_art_that_keeps_its_colour() {
assert_eq!(fill_of(&s, a), linked("#2a6fb0", "Brand"));
}
+/// A session whose default fill and stroke for new art are linked to the global swatch `Brand`
+/// (`#2a6fb0`), in color group `G`, beside the global swatch `Other` (`#ff8800`).
+fn brand_defaults_session() -> Session {
+ let mut s = session();
+ run(&mut s, "swatch.new", json!({"name": "Brand", "color": "#2a6fb0", "global": true}));
+ run(&mut s, "swatch.newGroup", json!({"name": "G", "swatches": ["Brand"]}));
+ run(&mut s, "swatch.new", json!({"name": "Other", "color": "#ff8800", "global": true}));
+ run(&mut s, "select.set", json!({"ids": []}));
+ run(&mut s, "paint.setFill", json!({"swatch": "Brand"}));
+ run(&mut s, "paint.setStroke", json!({"swatch": "Brand"}));
+ assert_eq!(s.paint.fill, linked("#2a6fb0", "Brand"));
+ s
+}
+
+fn fill_and_stroke(s: &Session, id: NodeId) -> (Paint, Paint) {
+ let a = &doc(s).node(id).unwrap().appearance;
+ (a.fill_paint(), a.stroke_paint())
+}
+
+/// Run `steps` and then draw a rectangle, once one command at a time and once as one
+/// `command.batch`, and check that both leave the same default paints and give the rectangle drawn
+/// in the batch, and one drawn after it, the same fill and stroke. Returns the batch's rectangle's
+/// fill and stroke.
+fn batch_matches_steps(steps: &[(&str, Value)]) -> (Paint, Paint) {
+ let shape = ("shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}));
+ let mut one_by_one = brand_defaults_session();
+ for (id, p) in steps {
+ run(&mut one_by_one, id, p.clone());
+ }
+ let single = rect(&mut one_by_one);
+ let mut batched = brand_defaults_session();
+ let commands: Vec = steps.iter().chain([&shape]).map(|(id, p)| json!({"command": id, "params": p})).collect();
+ let r = run(&mut batched, "command.batch", json!({ "commands": commands }));
+ let in_batch = NodeId(r["results"][steps.len()]["id"].as_u64().unwrap());
+ assert_eq!(batched.paint.fill, one_by_one.paint.fill, "the default fill after {steps:?}");
+ assert_eq!(batched.paint.stroke, one_by_one.paint.stroke, "the default stroke after {steps:?}");
+ let expected = fill_and_stroke(&one_by_one, single);
+ assert_eq!(fill_and_stroke(&batched, in_batch), expected, "art drawn in the batch after {steps:?}");
+ let later = rect(&mut batched);
+ assert_eq!(fill_and_stroke(&batched, later), expected, "art drawn after the batch after {steps:?}");
+ expected
+}
+
+#[test]
+fn swatch_steps_in_a_batch_change_the_new_art_paints_as_commands_do() {
+ let blue = Paint::solid(Color::from_hex("#2a6fb0").unwrap());
+ assert_eq!(batch_matches_steps(&[("swatch.delete", json!({"name": "Brand"}))]), (blue.clone(), blue));
+ let link = linked("#2a6fb0", "Brand");
+ assert_eq!(batch_matches_steps(&[("swatch.delete", json!({"name": "Brand", "unlink": false}))]), (link.clone(), link));
+ let signal = linked("#00ff00", "Signal");
+ let edit = ("swatch.edit", json!({"name": "Brand", "newName": "Signal", "color": "#00ff00"}));
+ assert_eq!(batch_matches_steps(&[edit]), (signal.clone(), signal));
+ let other = linked("#ff8800", "Other");
+ assert_eq!(batch_matches_steps(&[("swatch.merge", json!({"names": ["Other", "Brand"]}))]), (other.clone(), other));
+ let gray = linked("#ababab", "Brand");
+ assert_eq!(batch_matches_steps(&[("swatch.editGroup", json!({"group": "G", "colors": ["#ababab"]}))]), (gray.clone(), gray));
+}
+
+#[test]
+fn undo_and_redo_of_a_swatch_edit_put_back_the_new_art_paints() {
+ let edit = json!({"name": "Brand", "newName": "Signal", "color": "#00ff00"});
+ let delete = json!({"name": "Brand"});
+ for (id, p, fill) in
+ [("swatch.edit", &edit, linked("#00ff00", "Signal")), ("swatch.delete", &delete, Paint::solid(Color::from_hex("#2a6fb0").unwrap()))]
+ {
+ for batch in [false, true] {
+ let mut s = brand_defaults_session();
+ let before = s.paint.clone();
+ if batch {
+ run(&mut s, "command.batch", json!({"commands": [{"command": id, "params": p}]}));
+ } else {
+ run(&mut s, id, p.clone());
+ }
+ assert_eq!(s.paint.fill, fill, "{id}, batch: {batch}");
+ let after = s.paint.clone();
+ run(&mut s, "edit.undo", json!({}));
+ assert_eq!(s.paint, before, "Undo of {id}, batch: {batch}");
+ run(&mut s, "edit.redo", json!({}));
+ assert_eq!(s.paint, after, "Redo of {id}, batch: {batch}");
+ }
+ }
+}
+
+#[test]
+fn swatch_edits_reach_the_paints_of_the_new_art_template() {
+ let mut s = brand_defaults_session();
+ s.paint.appearance = Some(vectorcraft_doc::Appearance::basic(linked("#2a6fb0", "Brand"), linked("#2a6fb0", "Brand"), 1.0));
+ run(&mut s, "swatch.edit", json!({"name": "Brand", "newName": "Signal", "color": "#00ff00"}));
+ let template = |s: &Session| -> Vec {
+ s.paint
+ .appearance
+ .iter()
+ .flat_map(|a| a.items.iter())
+ .map(|it| match it {
+ vectorcraft_doc::AppearanceItem::Fill(f) => f.paint.clone(),
+ vectorcraft_doc::AppearanceItem::Stroke(k) => k.paint.clone(),
+ })
+ .collect()
+ };
+ assert_eq!(template(&s), [linked("#00ff00", "Signal"), linked("#00ff00", "Signal")]);
+ run(&mut s, "swatch.delete", json!({"name": "Signal"}));
+ let green = Paint::solid(Color::from_hex("#00ff00").unwrap());
+ assert_eq!(template(&s), [green.clone(), green]);
+}
+
+#[test]
+fn a_failed_batch_puts_back_the_new_art_paints_a_swatch_step_changed() {
+ let mut s = brand_defaults_session();
+ let before = s.paint.clone();
+ let steps = json!([
+ {"command": "swatch.edit", "params": {"name": "Brand", "newName": "Signal", "color": "#00ff00"}},
+ {"command": "no.such.command", "params": {}},
+ ]);
+ assert!(s.execute("command.batch", &json!({ "commands": steps })).is_err());
+ assert_eq!(s.paint, before);
+ assert_eq!(doc(&s).swatch("Brand").unwrap().paint.color(), Some(Color::from_hex("#2a6fb0").unwrap()));
+}
+
+#[test]
+fn canceling_a_swatch_options_preview_keeps_the_new_art_paints() {
+ let mut s = brand_defaults_session();
+ let before = s.paint.clone();
+ s.begin_interaction("Swatch Options").unwrap();
+ s.preview("swatch.edit", &json!({"name": "Brand", "newName": "Signal", "color": "#00ff00"})).unwrap();
+ s.cancel_interaction().unwrap();
+ assert_eq!(s.paint, before);
+ assert!(doc(&s).swatch("Signal").is_none());
+ // A previewed batch that is canceled puts them back too, and one that is accepted keeps them.
+ let steps = json!([{"command": "swatch.edit", "params": {"name": "Brand", "newName": "Signal", "color": "#00ff00"}}]);
+ s.begin_interaction("Batch Preview").unwrap();
+ s.preview("command.batch", &json!({ "commands": steps })).unwrap();
+ s.cancel_interaction().unwrap();
+ assert_eq!(s.paint, before);
+ s.begin_interaction("Batch Preview").unwrap();
+ s.preview("command.batch", &json!({ "commands": steps })).unwrap();
+ s.commit_interaction().unwrap();
+ assert_eq!(s.paint.fill, linked("#00ff00", "Signal"));
+ assert_eq!(s.paint.stroke, linked("#00ff00", "Signal"));
+}
+
#[test]
fn deleting_several_swatches_and_groups() {
let mut s = session();
diff --git a/crates/engine/src/tests/symbol_nine_slice.rs b/crates/engine/src/tests/symbol_nine_slice.rs
index b0689d1587..cba8b8c984 100644
--- a/crates/engine/src/tests/symbol_nine_slice.rs
+++ b/crates/engine/src/tests/symbol_nine_slice.rs
@@ -20,9 +20,18 @@ fn close(a: Rect, b: Rect) -> bool {
/// A 100 × 100 frame with a 10 × 10 square in its top-left corner and one in its center, made
/// into symbol "Panel" with 9-slice guides on, its instance selected at its natural size.
fn panel_symbol() -> (Session, NodeId) {
+ panel_symbol_filled(None)
+}
+
+/// [`panel_symbol`] with the frame filled with `fill` when there is one.
+fn panel_symbol_filled(fill: Option) -> (Session, NodeId) {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let ids = [rect(&mut s, 50.0, 50.0, 100.0, 100.0), rect(&mut s, 50.0, 50.0, 10.0, 10.0), rect(&mut s, 95.0, 95.0, 10.0, 10.0)];
+ if let Some(fill) = fill {
+ let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
+ doc.node_mut(ids[0]).unwrap().appearance = vectorcraft_doc::Appearance::basic(fill, vectorcraft_color::Paint::None, 0.0).into();
+ }
s.select(|_, sel| sel.set(ids)).unwrap();
let r = s.execute("symbol.new", &json!({"name": "Panel", "nineSlice": true})).unwrap();
assert_eq!(r["nineSlice"], json!([0.25, 0.25, 0.25, 0.25]));
@@ -350,3 +359,39 @@ fn guides_outside_the_art_survive_saving_and_reopening_a_native_file() {
assert!(back.symbol_slice_map(symbol, *xf).is_none(), "{bad}: scaled as a whole");
}
}
+
+/// [`panel_symbol`] with a gradient on the frame, so that the frame is drawn in a clip group for
+/// each cell, its instance resized to 200 × 150 and turned 20°.
+fn turned_gradient_panel() -> (Session, NodeId) {
+ let gradient = vectorcraft_color::Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient::default())));
+ let (mut s, id) = panel_symbol_filled(Some(gradient));
+ s.execute("object.setBounds", &json!({"x": 50, "y": 50, "width": 200, "height": 150, "reference": 0})).unwrap();
+ s.execute("object.rotate", &json!({"angle": 20})).unwrap();
+ (s, id)
+}
+
+/// The bounding box angles of the objects under the art that replaced an instance, and whether
+/// that art holds a clip group.
+fn expanded_angles(s: &Session, art: NodeId) -> (Vec, bool) {
+ let (mut angles, mut clips) = (vec![], false);
+ s.doc().unwrap().doc.node(art).unwrap().walk(&mut |c| {
+ angles.push(c.bbox_angle);
+ clips |= matches!(c.kind, NodeKind::Group { clip: true, .. });
+ });
+ (angles, clips)
+}
+
+#[test]
+fn break_link_turns_the_cells_of_a_sliced_instance_with_its_box() {
+ for reset in [false, true] {
+ let (mut s, _) = turned_gradient_panel();
+ if reset {
+ assert_eq!(s.execute("object.resetBoundingBox", &json!({})).unwrap()["changed"], 1);
+ }
+ let r = s.execute("symbol.breakLink", &json!({})).unwrap();
+ let (angles, clips) = expanded_angles(&s, NodeId(r["ids"][0].as_u64().unwrap()));
+ assert!(clips, "the frame is drawn in clip groups");
+ let want = if reset { 0.0 } else { 20.0 };
+ assert!(angles.iter().all(|a| (a - want).abs() < 1e-6), "reset {reset}: {angles:?}");
+ }
+}
diff --git a/crates/eps/src/ai_editing/tests.rs b/crates/eps/src/ai_editing/tests.rs
index 967c982ad4..0f58e00181 100644
--- a/crates/eps/src/ai_editing/tests.rs
+++ b/crates/eps/src/ai_editing/tests.rs
@@ -3255,7 +3255,8 @@ fn images_are_written_again_as_the_same_bytes() {
/// The program of `doc` with LF line ends, and each image's samples (from after `XI` and its line
/// end to `%%EndData`, a `%_` before it kept) and each preview's rows (from after `32 17 ` to the
-/// line end before `%AI26_EndPlacedObjectPreview`) shown as ``.
+/// line end before `%AI26_EndPlacedObjectPreview` or `%_%AI26_EndPlacedObjectPreview`) shown as
+/// ``.
fn program_shown(doc: &Document) -> String {
shown(&export_ai_editing(doc, &opts()).unwrap().program)
}
@@ -3267,10 +3268,13 @@ fn shown(p: &[u8]) -> String {
let mut at = 0;
loop {
let xi = find(at, b"\r\nXI\r\n").map(|i| (i + 6, b"%%EndData".as_slice()));
- let pv = find(at, b" 32 17 ").map(|i| (i + 7, b"\r\n%AI26_EndPlacedObjectPreview".as_slice()));
+ let pv = find(at, b" 32 17 ").map(|i| (i + 7, b"%AI26_EndPlacedObjectPreview".as_slice()));
let Some((start, end)) = [xi, pv].into_iter().flatten().min_by_key(|(s, _)| *s) else { break };
let mut stop = find(start, end).unwrap();
- if end.starts_with(b"%%") && p.get(stop - 2..stop) == Some(b"%_") {
+ if p.get(stop - 2..stop) == Some(b"%_") {
+ stop -= 2;
+ }
+ if end.starts_with(b"%AI26") && p.get(stop - 2..stop) == Some(b"\r\n") {
stop -= 2;
}
out.extend_from_slice(&p[at..start]);
@@ -7354,18 +7358,147 @@ fn an_image_on_hidden_lines_does_not_come_in_beside_its_look() {
assert_eq!(art[0].name.as_deref(), Some("Photo"));
}
-/// A linked image, an image with an opacity mask, and an image with a Warp and another effect are
-/// written as their drawn art, with no art style.
+/// [`warped_photo`] linked to `/Users/someone/Job/art/photo.png`, in folder `art` next to the
+/// document's.
+fn linked_warped_photo(doc: &mut Document, effects: Vec) -> Node {
+ let mut n = warped_photo(doc, effects);
+ if let NodeKind::Image(im) = &mut n.kind {
+ let mut link = vectorcraft_doc::LinkInfo::new("/Users/someone/Job/art/photo.png");
+ link.relative = Some("art/photo.png".into());
+ im.link = Some(link);
+ }
+ n
+}
+
+/// A linked image with one Warp is written with an art style of that Warp, its bent pieces as its
+/// drawn look and the image itself as a placed file on `%_` lines, with its path, folder and
+/// preview: it reads back as the linked image, its pixels, placement, name, opacity and link as
+/// they were, with its Warp live and its settings kept.
+#[test]
+fn a_linked_image_with_a_warp_reads_back_linked_with_its_warp_live() {
+ // GUARDS: a linked image with a Warp was written as its drawn pieces alone, and read back
+ // without its Warp or its link.
+ let params = serde_json::json!({"bend": 40.0});
+ let doc = photo_doc(|d| linked_warped_photo(d, vec![fx("warp.arc", params.clone())]));
+ let out = export_ai_editing(&doc, &opts()).unwrap();
+ assert!(out.warnings.is_empty(), "{:?}", out.warnings);
+ let text = program_shown(&doc);
+ assert!(text.contains("\n%_%AI5_BeginPlace\n"), "{text}");
+ assert!(text.contains("\n%_(/Users/someone/Job/art/photo.png)`\n"), "{text}");
+ assert!(text.contains("\n%_(art/)\n"), "{text}");
+ assert!(text.contains("\n%_8 8 32 32 17 <256 bytes>\n"), "{text}");
+ assert!(text.contains("\n%_%AI5_EndPlace\n") && text.contains("\n1 (Anon 1) XW\n"), "{text}");
+ assert!(!text.contains("BeginRaster\n%_"), "no embedded copy: {text}");
+ let back = read_back(&doc);
+ let art = back.layers[0].children().unwrap();
+ assert_eq!(art.len(), 1, "{:#?}", lines(&back));
+ let (mut want, mut got) = (vec![], vec![]);
+ pictures(&doc, doc.layers[0].children().unwrap(), "", &mut want);
+ pictures(&back, art, "", &mut got);
+ assert_eq!(got, want);
+ let [warp] = art[0].appearance.effects.as_slice() else { panic!("{:?}", art[0].appearance) };
+ assert_eq!(warp.id, "warp.arc");
+ let (w, g) = (vectorcraft_effects::merged_params("warp.arc", ¶ms), vectorcraft_effects::merged_params(&warp.id, &warp.params));
+ for key in ["bend", "horizontal", "vertical"] {
+ assert!((g[key].as_f64().unwrap() - w[key].as_f64().unwrap()).abs() < 6e-3, "{key}: {g} for {w}");
+ }
+}
+
+/// A linked image with one Warp larger than a placed file's preview: its copy's preview and its drawn
+/// look's pieces (placed files of the same image) hold the same smaller preview, so fine detail
+/// reads back with the Warp live, the link kept and the preview's pixels, as a linked image without
+/// a Warp does.
+#[test]
+fn a_large_linked_image_with_a_warp_reads_back_from_its_preview() {
+ // GUARDS: a linked image with a Warp was written as its drawn pieces alone.
+ let mut doc = Document::new(200.0, 100.0);
+ let layer = doc.layers[0].id;
+ let checks =
+ image::RgbaImage::from_fn(600, 300, |x, y| if (x + y) % 2 == 0 { image::Rgba([255, 0, 0, 255]) } else { image::Rgba([0, 0, 255, 255]) });
+ let mut link = vectorcraft_doc::LinkInfo::new("/Users/someone/Job/art/checks.png");
+ link.relative = Some("art/checks.png".into());
+ let xf = Affine::new([0.25, 0.0, 0.0, 0.25, 20.0, 10.0]);
+ let mut n = image_node(&mut doc, ImageBlob::png(png_of(checks.into())), (600, 300), xf, Some(link));
+ n.appearance = Appearance { effects: vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))], ..Appearance::default() }.into();
+ doc.insert(Some(layer), usize::MAX, n).unwrap();
+ let out = export_ai_editing(&doc, &opts()).unwrap();
+ assert!(out.warnings.is_empty(), "{:?}", out.warnings);
+ let text = program_shown(&doc);
+ assert!(text.contains("\n%_256 128 1024 32 17 <131072 bytes>\n"), "the copy's preview");
+ assert!(text.contains("\n256 128 1024 32 17 <131072 bytes>\n"), "the look's pieces' previews");
+ let back = read_back(&doc);
+ let art = back.layers[0].children().unwrap();
+ assert_eq!(art.len(), 1, "{:#?}", lines(&back));
+ let NodeKind::Image(im) = &art[0].kind else { panic!("{:?}", art[0].kind) };
+ assert_eq!((im.width, im.height), (256, 128));
+ assert_eq!(
+ im.link.as_ref().map(|l| (l.path.as_str(), l.relative.as_deref())),
+ Some(("/Users/someone/Job/art/checks.png", Some("art/checks.png")))
+ );
+ assert_eq!(art[0].appearance.effects.iter().map(|e| e.id.as_str()).collect::>(), ["warp.arc"]);
+}
+
+/// A hidden or locked image with one Warp, embedded or linked, keeps its state, its link and its
+/// live Warp.
+#[test]
+fn a_hidden_or_locked_image_with_a_warp_keeps_its_state() {
+ // GUARDS: a linked image with a Warp was written as its drawn pieces alone, and a hidden image's
+ // Warp was compared with a look that drew nothing, so it came in as its drawn pieces.
+ for (what, make) in [("embedded", warped_photo as fn(&mut Document, Vec) -> Node), ("linked", linked_warped_photo)] {
+ for (shown, locked) in [(true, true), (false, false), (false, true)] {
+ let doc = photo_doc(|d| {
+ let mut n = make(d, vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))]);
+ (n.visible, n.locked) = (shown, locked);
+ n
+ });
+ let out = export_ai_editing(&doc, &opts()).unwrap();
+ assert!(out.warnings.is_empty(), "{what} {shown} {locked}: {:?}", out.warnings);
+ let back = read_back(&doc);
+ let art = back.layers[0].children().unwrap();
+ assert_eq!(art.len(), 1, "{what} {shown} {locked}: {:#?}", lines(&back));
+ assert_eq!(images_of(&back).len(), 1, "{what} {shown} {locked}");
+ assert_eq!(images_of(&back), images_of(&doc), "{what} {shown} {locked}");
+ assert_eq!(art[0].appearance.effects.iter().map(|e| e.id.as_str()).collect::>(), ["warp.arc"]);
+ }
+ }
+}
+
+/// A linked image with one Warp in an opacity mask's art keeps its live Warp; its copy on `%_` lines
+/// is embedded, as linked images in masks are, with that warning.
+#[test]
+fn a_linked_image_with_a_warp_in_a_mask_is_embedded_with_its_warp_live() {
+ // GUARDS: the placed file of a styled image written in an opacity mask's art.
+ let doc = photo_doc(|d| {
+ let art = linked_warped_photo(d, vec![fx("warp.arc", serde_json::json!({"bend": 40.0}))]);
+ let mut square =
+ Node::path(d.alloc_id(), shapes::rectangle(Rect::new(20.0, 10.0, 100.0, 90.0)), Appearance::basic(solid(Color::BLACK), Paint::None, 0.0));
+ square.extras_mut().mask = Some(Box::new(OpacityMask::new(art, false)));
+ square
+ });
+ let out = export_ai_editing(&doc, &opts()).unwrap();
+ assert_eq!(out.warnings, [writer::LINKS_EMBEDDED]);
+ let text = program_shown(&doc);
+ assert!(!text.contains("BeginPlace") && text.contains("1 (Anon 1) XW"), "{text}");
+ let back = read_back(&doc);
+ let mask = back.layers[0].children().unwrap()[0].extras().mask.as_ref().map(|m| m.art.clone()).expect("the mask");
+ let NodeKind::Image(im) = &mask.kind else { panic!("{:?}", mask.kind) };
+ assert!(im.link.is_none());
+ assert_eq!(mask.appearance.effects.iter().map(|e| e.id.as_str()).collect::>(), ["warp.arc"]);
+}
+
+/// An image with an opacity mask and an image with a Warp and another effect are written as their
+/// drawn art, with no art style, linked or embedded.
#[test]
fn other_warped_images_are_written_as_their_drawn_art() {
let arc = || fx("warp.arc", serde_json::json!({"bend": 40.0}));
- let linked = photo_doc(|d| {
- let mut n = warped_photo(d, vec![arc()]);
- if let NodeKind::Image(im) = &mut n.kind {
- im.link = Some(vectorcraft_doc::LinkInfo::new("/art/photo.png"));
- }
+ let linked_masked = photo_doc(|d| {
+ let mut n = linked_warped_photo(d, vec![arc()]);
+ let white =
+ Node::path(d.alloc_id(), shapes::rectangle(Rect::new(20.0, 10.0, 100.0, 90.0)), Appearance::basic(solid(Color::WHITE), Paint::None, 0.0));
+ n.extras_mut().mask = Some(Box::new(OpacityMask::new(white, false)));
n
});
+ let linked_stacked = photo_doc(|d| linked_warped_photo(d, vec![arc(), fx("stylize.feather", serde_json::json!({"radius": 2.0}))]));
let masked = photo_doc(|d| {
let mut n = warped_photo(d, vec![arc()]);
let white =
@@ -7374,7 +7507,7 @@ fn other_warped_images_are_written_as_their_drawn_art() {
n
});
let stacked = photo_doc(|d| warped_photo(d, vec![arc(), fx("stylize.feather", serde_json::json!({"radius": 2.0}))]));
- for (what, doc) in [("linked", linked), ("masked", masked), ("stacked", stacked)] {
+ for (what, doc) in [("linked masked", linked_masked), ("linked stacked", linked_stacked), ("masked", masked), ("stacked", stacked)] {
assert!(!program_shown(&doc).contains("1 (Anon"), "{what}");
let back = read_back(&doc);
let art = back.layers[0].children().unwrap();
diff --git a/crates/eps/src/ai_editing/writer.rs b/crates/eps/src/ai_editing/writer.rs
index 879eb9d9f5..765856753c 100644
--- a/crates/eps/src/ai_editing/writer.rs
+++ b/crates/eps/src/ai_editing/writer.rs
@@ -206,6 +206,9 @@ pub(super) struct Writer<'a> {
mask_level: Option<(u32, u32)>,
/// The image just written was written as a placed file.
placed: bool,
+ /// The image being written is the copy of a styled object on the layers, on `%_` lines: linked,
+ /// it is written as a placed file there ([`Self::styled_object`]).
+ styled_copy: bool,
/// The type just written was written as type, not as its outlines.
typed: bool,
/// The objects bound to variables so far, in the order they are written: each one's id, its XML
@@ -299,6 +302,7 @@ impl<'a> Writer<'a> {
hidden: 0,
mask_level: None,
placed: false,
+ styled_copy: false,
typed: false,
bound: vec![],
texts: texts::TextDoc::default(),
diff --git a/crates/eps/src/ai_editing/writer/images.rs b/crates/eps/src/ai_editing/writer/images.rs
index 36c21c14eb..355f5c2709 100644
--- a/crates/eps/src/ai_editing/writer/images.rs
+++ b/crates/eps/src/ai_editing/writer/images.rs
@@ -94,7 +94,8 @@ impl Samples {
impl Writer<'_> {
/// Image object `im` of transparency `xy`: embedded, a raster ([`Self::raster`]); linked, a
/// placed file with a preview ([`Self::placed_file`]), except in a part of the data written on
- /// `%_` lines and in a symbol's art. One whose pixels can't be read is its outline filled mid gray. Where the art is
+ /// `%_` lines (other than a styled object's copy on the layers, [`Self::styled_object`]) and in
+ /// a symbol's art. One whose pixels can't be read is its outline filled mid gray. Where the art is
/// cut to a clipping path, the image is in a clipping group of that path.
pub(super) fn image(&mut self, im: &ImageObject, xy: Xy) {
let doc = self.doc;
@@ -125,7 +126,7 @@ impl Writer<'_> {
let fit =
Affine::scale_non_uniform(f64::from(im.width.max(1)) / f64::from(samples.width), f64::from(im.height.max(1)) / f64::from(samples.height));
match &im.link {
- Some(link) if self.hidden == 0 && !self.in_symbol => {
+ Some(link) if (self.hidden == 0 || self.styled_copy) && !self.in_symbol => {
// Cut to a region, the placed file is in a clipping group.
self.placed = cut.is_none();
self.placed_file(link, &samples, (im.width.max(1), im.height.max(1)), im.xf);
diff --git a/crates/eps/src/ai_editing/writer/styles.rs b/crates/eps/src/ai_editing/writer/styles.rs
index 163d70381c..5a80e50a78 100644
--- a/crates/eps/src/ai_editing/writer/styles.rs
+++ b/crates/eps/src/ai_editing/writer/styles.rs
@@ -42,7 +42,7 @@ pub(in crate::ai_editing) fn style_links(doc: &Document) -> usize {
/// The objects of `doc`'s layers and of their opacity masks' art that may be written with an art
/// style ([`Writer::styled_object`]), by id (a mask's art root shown, as [`Writer::mask`] writes
/// it): paths and compound paths with points and groups, with live effects, linked to a graphic
-/// style or (paths and compound paths) with a brush on a stroke, and embedded images with one Warp
+/// style or (paths and compound paths) with a brush on a stroke, and images with one Warp
/// ([`warped_image`]), without Opacity & Mask Define
/// Knockout Shape (the reader takes an object's transparency
/// from its look, which doesn't carry it). A group's members are paths, compound paths or such
@@ -100,12 +100,11 @@ fn candidate(n: &Node) -> bool {
}
}
-/// Is `n` an embedded image without an opacity mask whose appearance is one Warp, with no fills or
-/// strokes that paint and no effects on them: an image the reader reads with its Warp live
-/// ([`crate::import::read_art_style`]'s `image_warp`)?
+/// Is `n` an image, embedded or linked, without an opacity mask whose appearance is one Warp, with
+/// no fills or strokes that paint and no effects on them: an image the reader reads with its Warp
+/// live ([`crate::import::read_art_style`]'s `image_warp`)?
fn warped_image(n: &Node) -> bool {
- let NodeKind::Image(im) = &n.kind else { return false };
- im.link.is_none()
+ matches!(n.kind, NodeKind::Image(_))
&& n.extras().mask.is_none()
&& matches!(n.appearance.effects.as_slice(), [e] if e.id.starts_with("warp."))
&& n.appearance.items.iter().all(|i| i.paint().is_none() && i.effects().is_empty())
@@ -725,7 +724,8 @@ impl Writer<'_> {
/// glows or feathers, the object's transparency
/// after the group's end, `0 0 Xd`, `6 () XW`); the object itself on `%_` lines (a path's or
/// compound path's points painted with nothing, or with its gradient fill when its style has
- /// one; a group with its members; an image with its pixels), its name and opacity mask; then
+ /// one; a group with its members; an image with its pixels, or on the layers a linked image as
+ /// a placed file with its path, folder and preview), its name and opacity mask; then
/// `1 (name) XW`.
/// The look's group and the object itself are counted first ([`Self::take_objects`]): without
/// room for them, nothing is written, and the object counts neither as written with its graphic
@@ -772,6 +772,9 @@ impl Writer<'_> {
// The object, on `%_` lines, in its own state (the look's members may have changed it).
self.state(orig.locked, !orig.visible);
let saved = std::mem::take(&mut self.body);
+ // On the layers, a linked image's copy is a placed file; in an opacity mask's art, it is
+ // embedded, as other linked images there are.
+ let on_layers = self.hidden == 0;
self.hidden += 1;
let fit = drawn_outline(n, orig).map(|p| p.bounding_box());
// A path's or an image's objects were kept for it above; it counts them itself.
@@ -781,7 +784,11 @@ impl Writer<'_> {
match &orig.kind {
NodeKind::Path { path, rule, .. } => self.styled_outline(orig, path, *rule, fit, xy),
NodeKind::Compound { rule, .. } => self.styled_outline(orig, &compound_path(orig), *rule, fit, xy),
- NodeKind::Image(im) => self.image(&self.doc.shown_image(orig, im), xy),
+ NodeKind::Image(im) => {
+ let was = std::mem::replace(&mut self.styled_copy, on_layers);
+ self.image(&self.doc.shown_image(orig, im), xy);
+ self.styled_copy = was;
+ }
NodeKind::Group { children, .. } => {
self.line("u");
for c in children {
diff --git a/crates/eps/src/import/ai/read.rs b/crates/eps/src/import/ai/read.rs
index b77bc32199..d31ada250c 100644
--- a/crates/eps/src/import/ai/read.rs
+++ b/crates/eps/src/import/ai/read.rs
@@ -8,7 +8,8 @@
//! next layers: a clipping group in each layer, read by [`masks`].
//! - Ruler guides: a straight `(N) *` guide path, directly on a layer, that runs across the whole
//! canvas (16,383 points around the template box's centre) or from edge to edge of one artboard
-//! is a ruler guide on that layer ([`Document::guides`]); other guide paths stay paths.
+//! is a ruler guide on that layer ([`Document::guides`]), shown or hidden, with the path's name,
+//! lock and visibility; other guide paths stay paths.
//! - Groups `u` … `U`, compound paths `*u` … `*U`, clipping groups `q` … `Q` (the path painted
//! with `W` clips); a group's transparency and name follow its end. A wraparound group `*w` …
//! `*W` (version 7 specification §15.4.2) is a group whose objects other than its type wrap
diff --git a/crates/eps/src/import/ai/read/art.rs b/crates/eps/src/import/ai/read/art.rs
index 7e0f91e993..3338968895 100644
--- a/crates/eps/src/import/ai/read/art.rs
+++ b/crates/eps/src/import/ai/read/art.rs
@@ -234,11 +234,10 @@ impl<'a> Reader<'a> {
}
let kind = NodeKind::Path { path, rule: gs.rule, live: None, clipping: clip, guide };
let mut node = self.new_node(kind, &gs);
- // A ruler guide is written as a straight `(N) *` guide path on its layer.
+ // A ruler guide is written as a straight `(N) *` guide path on its layer, shown or hidden.
if guide
&& op == "N"
&& !hidden
- && !gs.hidden
&& !clip
&& self.pattern.is_none()
&& matches!(self.frames.last(), Some(Frame { kind: Kind::Layer(_), .. }))
diff --git a/crates/eps/src/import/ai/read/document.rs b/crates/eps/src/import/ai/read/document.rs
index ded114bc67..a7451c8b99 100644
--- a/crates/eps/src/import/ai/read/document.rs
+++ b/crates/eps/src/import/ai/read/document.rs
@@ -1445,7 +1445,8 @@ fn ruler_guide(a: Point, b: Point, centre: Option<(f64, f64)>, boards: &[Rect])
}
/// Makes the children of the layers and sublayers of `nodes` that `pick` makes a guide of into
-/// ruler guide nodes with the same ids, moved to the top of their layer in their order.
+/// ruler guide nodes with the same ids, names, locks and visibility, moved to the top of their layer
+/// in their order.
fn take_ruler_guides(nodes: &mut [Arc], pick: &dyn Fn(NodeId) -> Option, depth: usize) {
if depth > MAX_DEPTH {
return;
@@ -1458,7 +1459,11 @@ fn take_ruler_guides(nodes: &mut [Arc], pick: &dyn Fn(NodeId) -> Option {
- guides.push(Arc::new(Node::new(k.id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard })));
+ let mut n = Node::new(k.id, NodeKind::RulerGuide { vertical: g.vertical, pos: g.pos, artboard: g.artboard });
+ n.name = k.name.clone();
+ n.locked = k.locked;
+ n.visible = k.visible;
+ guides.push(Arc::new(n));
false
}
None => true,
diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs
index c1a6aa0385..87ba9752ef 100644
--- a/crates/eps/src/import/ai/read/ops.rs
+++ b/crates/eps/src/import/ai/read/ops.rs
@@ -1141,7 +1141,8 @@ fn image_warps(object: &Node, st: &crate::import::ai::style::ArtStyle) -> bool {
/// Does image `object` with `appearance` (its style's Warp) draw as `look`, its drawn look, compared
/// over all that the warp draws ([`crate::import::ai::envelope::warped_alike`]) on `layer`, a layer
-/// id no other node has? Both are drawn shown, opaque and in the Normal mode. Unless the look is on
+/// id no other node has? Both are drawn shown, opaque and in the Normal mode; a hidden look is drawn
+/// with every object in it shown, as the hidden object's look holds them. Unless the look is on
/// `%_` lines itself (`hidden`), the copies in it that are on `%_` lines (`hidden_ids`) are left out.
#[allow(clippy::too_many_arguments)]
fn image_warp_alike(
@@ -1161,12 +1162,24 @@ fn image_warp_alike(
if !hidden {
strip(&mut plain, hidden_ids);
}
+ if !plain.visible {
+ show_members(&mut plain);
+ }
for n in [&mut live, &mut plain] {
(n.visible, n.opacity, n.blend) = (true, 1.0, BlendMode::Normal);
}
crate::import::ai::envelope::warped_alike(&live, &plain, layer, shared, page, spent)
}
+/// Show every object in container `n`, at any depth.
+fn show_members(n: &mut Node) {
+ for c in n.children_mut().into_iter().flatten() {
+ let c = Arc::make_mut(c);
+ c.visible = true;
+ show_members(c);
+ }
+}
+
/// What [`placed_paints`] keys a pattern's name with, apart from the gradients' names.
const PATTERN_KEY: &str = "\u{1}pattern ";
diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs
index f1e201824d..8a9bab7530 100644
--- a/crates/eps/src/import/ai/tests.rs
+++ b/crates/eps/src/import/ai/tests.rs
@@ -8832,7 +8832,8 @@ fn other_guide_paths_stay_guide_paths() {
guide_line(0.0, 30.0, 200.0, 30.0, "S"),
guide_line(-7885.0, -7796.0, 8498.0, 8587.0, "N"),
format!("u\n{}U\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "N")),
- format!("1 Xw\n{}0 Xw\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "N")),
+ format!("1 Xw\n{}0 Xw\n", guide_line(20.0, 40.0, 80.0, 40.0, "N")),
+ format!("1 A\n{}0 A\n", guide_line(-7885.0, 40.0, 8498.0, 40.0, "S")),
"-7885 40 m\n306 40 L\n8498 40 L\n(N) *\n".to_string(),
];
for body in cases {
@@ -8842,6 +8843,35 @@ fn other_guide_paths_stay_guide_paths() {
}
}
+/// A hidden (`1 Xw`), locked (`1 A`) or named ruler guide path is a ruler guide that keeps its own
+/// visibility, lock and name, across the canvas or an artboard, on a shown layer.
+#[test]
+fn hidden_locked_and_named_guide_paths_are_ruler_guides() {
+ let body = format!(
+ "1 Xw\n{}0 Xw\n1 A\n{}0 A\n{}%_/ArtDictionary :\n%_(Mark) /String (AIArtName) ,\n%_;\n%_\n1 Xw\n{}0 Xw\n",
+ guide_line(-7885.0, 40.0, 8498.0, 40.0, "N"),
+ guide_line(25.0, 8587.0, 25.0, -7796.0, "N"),
+ guide_line(-7885.0, 90.0, 8498.0, 90.0, "N"),
+ guide_line(0.0, 30.0, 200.0, 30.0, "N"),
+ );
+ let doc = read(canvas_stream(&layer(&body)).as_bytes()).unwrap().doc;
+ let top = &doc.layers[0];
+ assert!(top.visible);
+ let kids = top.children().unwrap();
+ assert_eq!(kids.len(), 4);
+ assert!(kids.iter().all(|c| c.is_ruler_guide()), "no guide path is left");
+ let got: Vec<_> = doc.ruler_guides().into_iter().map(|g| (g.vertical, g.pos, g.artboard, g.layer, g.hidden, g.locked, g.name)).collect();
+ let want = [
+ (false, 60.0, None, Some(top.id), true, false, None),
+ (true, 25.0, None, Some(top.id), false, true, None),
+ (false, 10.0, None, Some(top.id), false, false, Some("Mark".to_string())),
+ (false, 70.0, Some(1), Some(top.id), true, false, None),
+ ];
+ assert_eq!(got, want);
+ // The nodes themselves carry the state, so the Layers rows show it.
+ assert_eq!(kids.iter().map(|k| (k.visible, k.locked)).collect::>(), [(false, false), (true, true), (true, false), (false, false)]);
+}
+
/// The layer whose `Lb` has `ops` as its options (the first one in the file).
fn layer_with(ops: &str) -> Node {
let doc = read_ok(&format!("%AI5_BeginLayer\n{ops} Lb\n(L) Ln\nLB\n%AI5_EndLayer--\n")).doc;
diff --git a/crates/eps/src/legacy_ai/tests.rs b/crates/eps/src/legacy_ai/tests.rs
index 15d7d07152..4436c3a09f 100644
--- a/crates/eps/src/legacy_ai/tests.rs
+++ b/crates/eps/src/legacy_ai/tests.rs
@@ -2067,3 +2067,17 @@ fn colr_glyphs_of_outlined_type_are_vector_art() {
assert!(rasters(&text).is_empty(), "no images: {text}");
assert!(text.contains("1 0 0 Xa"), "the red layer: {text}");
}
+
+/// An instance stained blue by its fill writes its symbol's red art in blue, as the canvas draws it.
+#[test]
+fn a_stained_instance_is_written_in_its_stained_color() {
+ let mut doc = Document::new(200.0, 100.0);
+ let art = shape(&mut doc, shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 40.0)), red(), Paint::None);
+ doc.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Mark".into(), art: Arc::new(art) });
+ let mut n = Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: "Mark".into(), xf: Affine::translate((50.0, 30.0)) });
+ n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0).into();
+ let l = doc.layers[0].id;
+ doc.insert(Some(l), 0, n).unwrap();
+ let text = String::from_utf8(write(&doc).bytes).unwrap();
+ assert!(text.contains("0 0 1 Xa") && !text.contains("1 0 0 Xa"), "{text}");
+}
diff --git a/crates/eps/src/legacy_ai/writer.rs b/crates/eps/src/legacy_ai/writer.rs
index ad0ccbce43..eda25a6322 100644
--- a/crates/eps/src/legacy_ai/writer.rs
+++ b/crates/eps/src/legacy_ai/writer.rs
@@ -297,7 +297,7 @@ impl<'a> Writer<'a> {
self.warn(LIVE);
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
&& self.nest < MAX_NEST
- && let Some(mut art) = self.doc.place_symbol_art(symbol, &vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n), *xf)
+ && let Some(mut art) = self.doc.place_symbol_art(symbol, &vectorcraft_doc::symbol_overrides::instance_art(&sym.art, n), *xf)
{
art.visible = true;
self.nest += 1;
diff --git a/crates/eps/src/scene.rs b/crates/eps/src/scene.rs
index c20c5847a2..75a7437e1d 100644
--- a/crates/eps/src/scene.rs
+++ b/crates/eps/src/scene.rs
@@ -511,7 +511,7 @@ impl<'a> Scene<'a> {
NodeKind::SymbolInstance { symbol, xf } => {
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
&& self.nest < MAX_NEST
- && let Some(art) = self.doc.place_symbol_art(symbol, &vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n), *xf)
+ && let Some(art) = self.doc.place_symbol_art(symbol, &vectorcraft_doc::symbol_overrides::instance_art(&sym.art, n), *xf)
{
self.nest += 1;
self.node(&art, true);
diff --git a/crates/eps/src/tests.rs b/crates/eps/src/tests.rs
index c98cb9689a..2ab96d3555 100644
--- a/crates/eps/src/tests.rs
+++ b/crates/eps/src/tests.rs
@@ -1390,6 +1390,20 @@ fn an_instance_whose_art_reaches_the_region_is_written() {
assert!(page.contains("1 0 0 rg"), "{page}");
}
+/// An instance stained blue by its fill writes its symbol's red art in blue, as the canvas draws it.
+#[test]
+fn a_stained_instance_is_written_in_its_stained_color() {
+ let mut d = doc_with(|d| {
+ let mut n = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Mark".into(), xf: Affine::translate((50.0, 50.0)) });
+ n.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0).into();
+ vec![n]
+ });
+ let art = path(&mut d, shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 40.0)), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0);
+ d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Mark".into(), art: Arc::new(art) });
+ let page = page_of(&d, &opts(&d));
+ assert!(page.contains("0 0 1 rg") && !page.contains("1 0 0 rg"), "{page}");
+}
+
#[test]
fn an_instance_whose_brush_art_reaches_the_region_is_written() {
// The symbol's art: a line from (0, 40) to (0, 60) with a 1 pt red stroke painted by a 20 pt
diff --git a/crates/mcp/src/tests/fileio.rs b/crates/mcp/src/tests/fileio.rs
index 3a89588ecd..b9794c8263 100644
--- a/crates/mcp/src/tests/fileio.rs
+++ b/crates/mcp/src/tests/fileio.rs
@@ -284,7 +284,7 @@ fn save_file_warns_about_options_it_does_not_use() {
}
/// The notes on `options` name the `.ai` file the save writes, as the note on a top-level option
-/// does: a legacy `.ai` for `aiVersion: "7"` (beside `pdfCompatible: false` too), a
+/// does: a legacy `.ai` for `aiVersion: "7"` (beside `pdfCompatible: false` too, which it notes), a
/// non-PDF-compatible `.ai` for `pdfCompatible: false`, and a PDF-compatible `.ai` otherwise, also
/// when the save reuses the options the document was last saved with.
#[test]
@@ -301,8 +301,12 @@ fn save_file_notes_name_the_ai_file_it_writes() {
("pdf.ai", json!({"pdfCompatible": true, "preview": true}), "PDF-compatible .ai"),
] {
let r = text(&call_tool(&mut h, "save_file", &json!({"path": tmp(&dir, name), "version": 3, "options": options})));
- let expected =
- [format!("`version` was not used: {label} files don't read it"), format!("`options.preview` was not used: {label} files don't read it")];
+ // A legacy `.ai` doesn't read `pdfCompatible` either.
+ let unread = |k: &str| options.get(k).is_some() && (k != "pdfCompatible" || label.starts_with("legacy"));
+ let expected: Vec = std::iter::once("version".to_string())
+ .chain(["pdfCompatible", "preview"].into_iter().filter(|k| unread(k)).map(|k| format!("options.{k}")))
+ .map(|k| format!("`{k}` was not used: {label} files don't read it"))
+ .collect();
assert_eq!(notes(&r), expected, "{name}: {r}");
}
// A save given no option the file reads writes it as last saved: a legacy `.ai` again.
diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs
index e3b57166b7..f22b9dfd41 100644
--- a/crates/mcp/src/ui_catalog.rs
+++ b/crates/mcp/src/ui_catalog.rs
@@ -1465,7 +1465,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"ui.action.new",
"New Action",
"",
- "{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), which the panel does not show. Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
+ "{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), the color of its button in Button Mode (ui.action.buttonMode). Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
),
(
"ui.action.options",
@@ -1479,6 +1479,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{action: name, set?: name} play the Actions panel's action named action at the speed ui.action.playbackOptions sets; set names the set to look in (default: every set). Accelerated plays it as one undoable batch → what command.batch returns. Step by Step and Pause For play its first step now and the others one at a time, each its own undo step, until the last, ui.action.stop, or a step that comes due while a different document is active than after the step before → {results: [the first step's result], pending: the steps left}. An action with a stop, at any speed, plays up to the stop and waits until ui.action.continue or ui.action.stop; at Accelerated the steps between stops are one undoable batch each → {results, pending, atStop?: true when it already waits}. A name no action has, or one that more than one action in the sets looked in has, or an action played while another is still playing step by step or with pauses, is an error and nothing plays",
),
+ (
+ "ui.action.playAt",
+ "Play Action",
+ "",
+ "{set: index, action: index} play the Actions panel's action at action in set (0 is the top set) as ui.action.play plays it, and return what ui.action.play returns. A click on an action's button in Button Mode runs this. A missing index, or one that is not a whole number or is past the last set or action, is an error and nothing plays",
+ ),
+ (
+ "ui.action.buttonMode",
+ "Button Mode",
+ "",
+ "{enabled?: bool} with true, the Actions panel shows every action of every set, in set order and then action order, as a button with its name in its Action Options color (no color: an ordinary button), without set names, steps or checkmarks, and a click on a button plays its action (ui.action.playAt); with false, it shows its list again; left out or null, it switches to the other view. The panel menu's Button Mode runs this and shows a check mark while it is on. The view is kept with the UI preferences. Turning it on while an action records, or an enabled that is not true, false or null, is an error and the view stays → {enabled}",
+ ),
(
"ui.action.playbackOptions",
"Playback Options",
diff --git a/crates/metafile/src/scene.rs b/crates/metafile/src/scene.rs
index ecba61ce8e..6a6fd0b61f 100644
--- a/crates/metafile/src/scene.rs
+++ b/crates/metafile/src/scene.rs
@@ -217,7 +217,7 @@ impl<'a> Scene<'a> {
NodeKind::SymbolInstance { symbol, xf } => {
if let Some(sym) = self.doc.symbols.iter().find(|s| &s.name == symbol)
&& self.nest < MAX_NEST
- && let art = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n)
+ && let art = vectorcraft_doc::symbol_overrides::instance_art(&sym.art, n)
&& let Some(art) =
if self.clips { self.doc.place_symbol_art(symbol, &art, *xf) } else { self.doc.place_symbol_art_unclipped(symbol, &art, *xf) }
{
diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs
index 602db3d55f..ceb65d6fe2 100644
--- a/crates/metafile/src/tests.rs
+++ b/crates/metafile/src/tests.rs
@@ -399,6 +399,22 @@ fn an_instance_whose_art_reaches_the_artboard_is_written() {
assert!(k.contains(&39), "a brush fills the art: {k:?}");
}
+/// An instance stained blue by its fill writes its symbol's red art in blue, as the canvas draws it,
+/// in EMF (clipped) and in WMF (unclipped).
+#[test]
+fn a_stained_instance_is_written_in_its_stained_color() {
+ let mut inst = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: "Mark".into(), xf: Affine::translate((30.0, 30.0)) });
+ inst.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::None, 0.0).into();
+ let mut d = doc_with(100.0, 100.0, vec![inst]);
+ let art = path(shapes::rectangle(Rect::new(0.0, 0.0, 40.0, 40.0)), red(), Paint::None, 0.0);
+ d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Mark".into(), art: Arc::new(art) });
+ for clips in [true, false] {
+ let (ops, _) = crate::scene::Scene::new(&d, d.artboards[0].rect, clips).run();
+ let colors: Vec<[u8; 3]> = fills(&ops).into_iter().map(|(_, rgb)| rgb).collect();
+ assert_eq!(colors, [[0, 0, 255]], "clips: {clips}");
+ }
+}
+
/// A pattern `name` repeating `art` every point.
fn point_tiles(name: &str, art: Node) -> PatternDef {
let mut def = PatternDef::new(name, vec![Arc::new(art)]);
diff --git a/crates/pdf/src/export/nodes.rs b/crates/pdf/src/export/nodes.rs
index 8bafe017c6..097ae52bba 100644
--- a/crates/pdf/src/export/nodes.rs
+++ b/crates/pdf/src/export/nodes.rs
@@ -336,14 +336,15 @@ impl Exporter<'_> {
// instances multiply.
// Placing through 9-slice guides needs room for the most objects it can make
// (`NineSliceMap::most_placed`), and the objects it makes count.
- // A dynamic symbol's instance shows its own appearance changes.
+ // A dynamic symbol's instance shows its own appearance changes, and the instance's
+ // fill stains its art, as on the canvas.
let doc = self.doc;
if let Some(sym) = doc.symbols.iter().find(|x| &x.name == symbol) {
let changed;
- let art: &Node = if n.extras().symbol_overrides.is_empty() {
+ let art: &Node = if n.extras().symbol_overrides.is_empty() && vectorcraft_doc::symbol_overrides::stain(n).is_none() {
&sym.art
} else {
- changed = vectorcraft_doc::symbol_overrides::dynamic_art(&sym.art, n);
+ changed = vectorcraft_doc::symbol_overrides::instance_art(&sym.art, n);
&changed
};
let count = art.count() as u64;
diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs
index d63aaedb35..bee80238bd 100644
--- a/crates/pdf/src/tests.rs
+++ b/crates/pdf/src/tests.rs
@@ -271,6 +271,71 @@ fn a_scaled_nine_slice_instance_keeps_its_corners() {
assert!(close(boxes[2], Rect::new(105.0, 85.0, 135.0, 105.0), 0.01), "{boxes:?}");
}
+/// An instance of symbol `name` placed by `xf`, its fill red at `opacity` (shown when `visible`).
+fn stained_instance(name: &str, xf: Affine, opacity: f32, visible: bool) -> Node {
+ let mut n = Node::new(NodeId(0), NodeKind::SymbolInstance { symbol: name.into(), xf });
+ n.appearance = Appearance::basic(Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::None, 0.0).into();
+ n.appearance.items.retain(AppearanceItem::is_fill);
+ if let AppearanceItem::Fill(f) = &mut n.appearance.items[0] {
+ (f.opacity, f.visible) = (opacity, visible);
+ }
+ n
+}
+
+/// The fill colors of `d`'s leaves, in paint order.
+fn leaf_fills(d: &Document) -> Vec {
+ leaves(d).iter().filter_map(|n| n.appearance.fill_paint().color()).map(|c| c.to_hex()).collect()
+}
+
+/// A stained instance is written in the colors the canvas draws it in
+/// ([`vectorcraft_brush::instance_art`]): on its own, with a Transform effect that moves it, inside
+/// a group, after a dynamic symbol's own fill change, and placed through 9-slice guides. A hidden
+/// fill or one at 0% opacity leaves the art's colors as they are.
+#[test]
+fn a_stained_instance_keeps_its_stain() {
+ let blue = Color::rgb8(30, 90, 220);
+ let art = || Node::group(NodeId(0), vec![Arc::new(rect_node(Rect::new(0.0, 0.0, 100.0, 100.0), blue))]);
+ let canvas = |n: &Node| leaf_fills_of(&vectorcraft_brush::instance_art(&art(), n));
+ fn leaf_fills_of(n: &Node) -> Vec {
+ let mut d = doc(10.0, 10.0);
+ add(&mut d, n.clone());
+ leaf_fills(&d)
+ }
+ let write = |n: Node, nine_slice: bool| {
+ let mut d = doc(300.0, 200.0);
+ d.symbols.push(vectorcraft_doc::Symbol { dynamic: true, name: "Panel".into(), art: Arc::new(art()) });
+ if nine_slice {
+ d.set_symbol_nine_slice("Panel", Some(vectorcraft_doc::SymbolNineSlice::default()));
+ }
+ add(&mut d, n);
+ leaf_fills(&roundtrip(&d))
+ };
+ let half = stained_instance("Panel", Affine::translate((20.0, 20.0)), 0.5, true);
+ let want = canvas(&half);
+ assert_ne!(want, [blue.to_hex()], "the stain changes the color");
+ assert_eq!(write(half.clone(), false), want, "on its own");
+ let mut moved = half.clone();
+ moved.appearance.effects.push(vectorcraft_doc::Effect {
+ id: "distort.transform".into(),
+ params: serde_json::json!({"moveH": 30.0}),
+ visible: true,
+ });
+ assert_eq!(write(moved, false), want, "with a geometry effect");
+ assert_eq!(write(Node::group(NodeId(0), vec![Arc::new(half.clone())]), false), want, "in a group");
+ let mut scaled = half.clone();
+ scaled.kind = NodeKind::SymbolInstance { symbol: "Panel".into(), xf: Affine::translate((20.0, 20.0)) * Affine::scale_non_uniform(2.0, 1.5) };
+ assert_eq!(write(scaled, true), want, "through 9-slice guides");
+ let mut changed = half.clone();
+ changed.extras_mut().symbol_overrides =
+ vec![vectorcraft_doc::SymbolOverride { path: vec![0], fill: Some(Paint::solid(Color::rgb(0.0, 1.0, 0.0))), ..Default::default() }];
+ let changed_want = canvas(&changed);
+ assert_ne!(changed_want, want, "the part's own fill is stained");
+ assert_eq!(write(changed, false), changed_want, "after a dynamic symbol's change");
+ for (opacity, visible) in [(0.0, true), (0.5, false)] {
+ assert_eq!(write(stained_instance("Panel", Affine::IDENTITY, opacity, visible), false), [blue.to_hex()], "unstained");
+ }
+}
+
/// The placed art of all the 9-slice instances in one export holds at most `MAX_SYMBOL_NODES`
/// objects, opacity-mask art included, as on the canvas: of two instances whose art placed through
/// the guides holds about 9,009,000 objects each (a gradient square in all nine cells, each copy
diff --git a/crates/render/src/frame.rs b/crates/render/src/frame.rs
index e16f134aca..7d000aa64f 100644
--- a/crates/render/src/frame.rs
+++ b/crates/render/src/frame.rs
@@ -110,8 +110,8 @@ pub(crate) struct PlacedEntry {
pub(crate) art: std::sync::Weak,
/// The art placed, or `None` for art placed again in every frame ([`depends_on_document`]).
pub(crate) placed: Option>,
- /// The font generation ([`vectorcraft_text::FontDb::generation`]) the art was placed in, when
- /// it holds type: the art is placed again when the fonts change.
+ /// The layout inputs ([`vectorcraft_text::layout_inputs`]) the art was placed with, when it
+ /// holds type: the art is placed again when the fonts or the hyphenation exceptions change.
pub(crate) fonts: Option,
/// The 9-slice map the art was placed through ([`Renderer::sliced_art`]), or `None` for art
/// placed by the transform alone ([`Renderer::placed_art`]). An entry is used only by the
diff --git a/crates/render/src/fx/pixels.rs b/crates/render/src/fx/pixels.rs
index 72731f7330..cdf817f1e4 100644
--- a/crates/render/src/fx/pixels.rs
+++ b/crates/render/src/fx/pixels.rs
@@ -412,7 +412,7 @@ impl Renderer {
/// brush library it reads; for images and placed documents their pixels and files, the
/// layer's Dim Images and the options they are drawn with (with
/// [`crate::RenderOptions::kept_images_only`], also which pixels and undecodable notes the
- /// image store holds); for type the fonts, whether overprinting shows
+ /// image store holds); for type the fonts and the hyphenation exceptions, whether overprinting shows
/// ([`crate::proof::overprints`]) and Document Setup's substitution highlights; and the
/// plug-ins installed, with the names of the document's swatches and patterns and its pattern
/// definitions (a plug-in's painted pieces can name them), when an object in it has a plug-in
@@ -457,7 +457,7 @@ impl Renderer {
f.doc.setup.highlight_substituted_glyphs,
o.highlight_alternate_glyphs,
));
- (vectorcraft_text::FontDb::global().generation(), crate::proof::overprints(f.opts), highlights).hash(&mut h);
+ (vectorcraft_text::layout_inputs(), crate::proof::overprints(f.opts), highlights).hash(&mut h);
}
if reads.plugins || (doc.plugins && (reads.symbols || reads.patterns)) {
(effects::plugin_revision(), doc.names, doc.patterns).hash(&mut h);
diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs
index 141c3524c5..6a92d91e8f 100644
--- a/crates/render/src/lib.rs
+++ b/crates/render/src/lib.rs
@@ -65,8 +65,8 @@ pub struct Renderer {
/// The latest cached glyph geometry for each text object id. A move changes the node's Arc
/// but not its glyphs in text space, so the old geometry can still be used after comparison.
text_ids: PtrMap,
- /// The [`vectorcraft_text::FontDb::generation`] `texts` was laid out with: type lays out
- /// again when fonts are added or rescanned.
+ /// The [`vectorcraft_text::layout_inputs`] `texts` was laid out with: type lays out again
+ /// when fonts are added or rescanned and when the hyphenation exceptions change.
text_fonts: u64,
/// Clip regions per clipping path (see [`Self::clip_of`]).
clips: FrameCache,
diff --git a/crates/render/src/live.rs b/crates/render/src/live.rs
index 4d41e16af8..3777c98948 100644
--- a/crates/render/src/live.rs
+++ b/crates/render/src/live.rs
@@ -127,8 +127,9 @@ struct Walked {
}
impl Walked {
- /// The [`resources_key`] of the node walked: the walk's hash, the font generation when it found
- /// type or a placed file, the plug-in revision and the hash of the document's swatch and pattern
+ /// The [`resources_key`] of the node walked: the walk's hash, the layout inputs (fonts and
+ /// hyphenation exceptions, [`vectorcraft_text::layout_inputs`]) when it found type or a placed
+ /// file, the plug-in revision and the hash of the document's swatch and pattern
/// names (`names`) when it found a plug-in effect, and the color settings `cms`. Whether a
/// plug-in paints its pieces depends on the plug-ins installed, so the names count for every
/// plug-in effect.
@@ -136,7 +137,7 @@ impl Walked {
let mut h = DefaultHasher::new();
self.hash.hash(&mut h);
if self.text {
- vectorcraft_text::FontDb::global().generation().hash(&mut h);
+ vectorcraft_text::layout_inputs().hash(&mut h);
}
if self.plugins {
crate::effects::plugin_revision().hash(&mut h);
diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs
index 2372abdbf2..317a423681 100644
--- a/crates/render/src/pattern.rs
+++ b/crates/render/src/pattern.rs
@@ -39,7 +39,8 @@ type TileKey = (String, u64, Ink, bool);
/// What a tile was drawn from besides its pattern's own art, for art that shows more than itself:
/// the art of every pattern and symbol and the pixels of every image in the document, the
-/// document's other data (which holds its brushes), and the fonts. Editing a symbol leaves the art
+/// document's other data (which holds its brushes), and the fonts and hyphenation exceptions
+/// ([`vectorcraft_text::layout_inputs`]). Editing a symbol leaves the art
/// of a pattern that shows it as it was, so the art alone kept the tile from before (#1723). Held
/// weakly: a weak reference keeps the allocation, so no other value gets one of these addresses
/// while the tile is kept.
@@ -66,13 +67,13 @@ impl Deps {
art: Self::art(doc).map(Arc::downgrade).collect(),
pixels: Self::pixels(doc).map(Arc::downgrade).collect(),
unknown: vectorcraft_doc::Shared::downgrade(&doc.unknown),
- fonts: vectorcraft_text::FontDb::global().generation(),
+ fonts: vectorcraft_text::layout_inputs(),
}
}
/// Does `doc` hold what the tile was drawn from?
fn current(&self, doc: &Document) -> bool {
- self.fonts == vectorcraft_text::FontDb::global().generation()
+ self.fonts == vectorcraft_text::layout_inputs()
&& std::ptr::eq(self.unknown.as_ptr(), &*doc.unknown)
&& self.art.iter().map(Weak::as_ptr).eq(Self::art(doc).map(Arc::as_ptr))
&& self.pixels.iter().map(Weak::as_ptr).eq(Self::pixels(doc).map(Arc::as_ptr))
diff --git a/crates/render/src/renderer/text.rs b/crates/render/src/renderer/text.rs
index b4a0dd0d2e..945ce5303b 100644
--- a/crates/render/src/renderer/text.rs
+++ b/crates/render/src/renderer/text.rs
@@ -55,7 +55,7 @@ impl Renderer {
/// objects) hit for them. Other art is placed again in every frame, and so is art that holds
/// symbol instances, type with inline graphics, images, placed documents or raster effects
/// ([`depends_on_document`]), and art that the cache has no room for. Art that holds type is
- /// placed again when the fonts change.
+ /// placed again when the fonts or the hyphenation exceptions change.
pub(super) fn placed_art(&mut self, art: &Arc, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc, bool) {
let kept_kind = matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || vectorcraft_doc::live::is_live(art);
// An instance with appearance changes of its own isn't kept: the key doesn't tell them apart.
@@ -67,7 +67,7 @@ impl Renderer {
let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), scale_strokes, stain);
if let Some(e) = self.placed_arts.get(&key, self.stamp)
&& std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art))
- && e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
+ && e.fonts.is_none_or(|g| g == vectorcraft_text::layout_inputs())
&& e.slice.is_none()
{
return match &e.placed {
@@ -106,7 +106,7 @@ impl Renderer {
if keeps
&& let Some(e) = self.placed_arts.get(&key, self.stamp)
&& std::ptr::eq(e.art.as_ptr(), Arc::as_ptr(art))
- && e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation())
+ && e.fonts.is_none_or(|g| g == vectorcraft_text::layout_inputs())
&& e.slice.as_ref() == Some(map)
{
match e.placed.clone() {
@@ -134,7 +134,7 @@ impl Renderer {
fn keep_placed(&mut self, key: PlacedKey, art: &Arc, placed: &Arc, slice: Option) -> bool {
let kept = (!depends_on_document(art)).then(|| placed.clone());
let keeps = kept.is_some();
- let fonts = (keeps && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation());
+ let fonts = (keeps && holds_type(art)).then(vectorcraft_text::layout_inputs);
// The bytes count the key and the entry, the art's allocation that the weak reference
// keeps, and the kept art.
let bytes = size_of::<(PlacedKey, PlacedEntry, Node)>() + kept.as_deref().map_or(0, lru::node_bytes);
@@ -150,7 +150,7 @@ impl Renderer {
if !matches!(&a.kind, NodeKind::Text(own) if std::ptr::eq(&**own, t)) {
return (Arc::new(text_geom(t)), false);
}
- let fonts = vectorcraft_text::FontDb::global().generation();
+ let fonts = vectorcraft_text::layout_inputs();
if fonts != self.text_fonts {
self.texts.clear();
self.text_ids.clear();
diff --git a/crates/render/src/tests/fontchange.rs b/crates/render/src/tests/fontchange.rs
index bedffb6b4b..15389cd805 100644
--- a/crates/render/src/tests/fontchange.rs
+++ b/crates/render/src/tests/fontchange.rs
@@ -3,16 +3,16 @@
use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
-use vectorcraft_doc::text::{CharStyle, TextObject};
+use vectorcraft_doc::text::{CharStyle, TextKind, TextObject};
use vectorcraft_doc::{Node, NodeKind};
-use vectorcraft_geom::Point;
+use vectorcraft_geom::{PathData, Point};
use crate::*;
-/// Taken to add fonts, or to look up an installed one by name (`FontDb::face` loads it, which
-/// changes the fonts too). Renderers lay type out again when the fonts change, so tests of what
-/// they keep between frames hold it shared ([`fonts_steady`]) and the tests that change the fonts
-/// hold it alone ([`adding_fonts`]).
+/// Taken to add fonts, to look up an installed one by name (`FontDb::face` loads it, which
+/// changes the fonts too) or to change the hyphenation exceptions. Renderers lay type out again
+/// when the fonts or the exceptions change, so tests of what they keep between frames hold it
+/// shared ([`fonts_steady`]) and the tests that change them hold it alone ([`adding_fonts`]).
static FONTS: RwLock<()> = RwLock::new(());
/// No fonts are added by these tests until the guard is dropped.
@@ -29,14 +29,12 @@ pub(crate) fn adding_fonts() -> RwLockWriteGuard<'static, ()> {
/// What `step` returns from a run in which the fonts stayed the same. The fonts can change while
/// [`fonts_steady`] is held: the first lookup by family name catalogs the installed fonts, and
/// resolving a cataloged family, or a character no loaded font has, loads a face. `step` runs up
-/// to 100 times, until [`vectorcraft_text::FontDb::generation`] is the same after a run as before
-/// it.
+/// to 100 times, until [`vectorcraft_text::layout_inputs`] is the same after a run as before it.
pub(crate) fn with_fonts_unchanged(mut step: impl FnMut() -> T) -> T {
- let db = vectorcraft_text::FontDb::global();
for _ in 0..100 {
- let before = db.generation();
+ let before = vectorcraft_text::layout_inputs();
let out = step();
- if db.generation() == before {
+ if vectorcraft_text::layout_inputs() == before {
return out;
}
}
@@ -466,6 +464,75 @@ fn enveloped_type_under_effects_redraws_when_its_font_becomes_available() {
}
}
+/// Hyphenated area type redraws with its new line breaks when Preferences › Hyphenation ›
+/// Exceptions change: its glyph geometry, the pixels of its raster effects, the art drawn for
+/// geometry effects, the art kept for a symbol instance and an envelope's evaluation are made
+/// again.
+#[test]
+fn hyphenated_type_redraws_when_the_exceptions_change() {
+ let _adding = adding_fonts();
+ struct Clear;
+ impl Drop for Clear {
+ fn drop(&mut self) {
+ vectorcraft_text::set_hyphenation_exceptions("");
+ }
+ }
+ let _clear = Clear;
+ use vectorcraft_doc::Effect;
+ let moved = || Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 0.0}), visible: true };
+ const WORDS: &str = "Extraordinarily comprehensive typographical considerations notwithstanding everything.";
+ const NEVER: &str = "extraordinarily, comprehensive, typographical, considerations, notwithstanding";
+ let cases = ["the area type", "the type with a drop shadow", "the group with a Transform effect", "the symbol instance", "the envelope"];
+ for (i, what) in cases.into_iter().enumerate() {
+ vectorcraft_text::set_hyphenation_exceptions("");
+ let mut d = Document::new(300.0, 150.0);
+ let mut t = TextObject::point(Point::ZERO, WORDS, CharStyle { size: 14.0, ..Default::default() });
+ t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(10.0, 10.0, 130.0, 145.0).to_path(0.1)) };
+ t.xf = Affine::IDENTITY;
+ t.para.hyphenate = true;
+ let mut object = Node::new(d.alloc_id(), NodeKind::Text(Box::new(t)));
+ match i {
+ 0 => {}
+ 1 => {
+ let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100});
+ object.appearance.effects.push(Effect { id: "stylize.dropShadow".into(), params, visible: true });
+ }
+ 2 => {
+ object = Node::group(d.alloc_id(), vec![Arc::new(object)]);
+ object.appearance.effects.push(moved());
+ }
+ 3 => {
+ object.appearance.effects.push(moved());
+ let art = Node::group(d.alloc_id(), vec![Arc::new(object)]);
+ d.symbols.push(vectorcraft_doc::Symbol { dynamic: false, name: "Words".into(), art: Arc::new(art) });
+ object = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Words".into(), xf: Affine::IDENTITY });
+ }
+ _ => {
+ let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true, version: 0 };
+ let envelope = NodeKind::Envelope {
+ content: vec![Arc::new(object)],
+ kind: Box::new(kind),
+ fidelity: 50.0,
+ editing: false,
+ options: Default::default(),
+ frame: Box::new(Affine::IDENTITY),
+ };
+ object = Node::new(d.alloc_id(), envelope);
+ }
+ }
+ let l = d.layers[0].id;
+ d.insert(Some(l), 0, object).unwrap();
+ let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
+ let mut r = Renderer::new();
+ let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
+ vectorcraft_text::set_hyphenation_exceptions(NEVER);
+ let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
+ let fresh = Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
+ assert_ne!(before, fresh, "{what}: the words in the list are not broken");
+ assert!(after == fresh, "{what}: drawn as a new renderer draws it");
+ }
+}
+
/// Without an engine session, a renderer maps an envelope of type from its glyph outline bounds,
/// and measures the type again when its font becomes available.
#[test]
diff --git a/crates/render/src/tests/memory.rs b/crates/render/src/tests/memory.rs
index d286fa7e3c..1f7a49698c 100644
--- a/crates/render/src/tests/memory.rs
+++ b/crates/render/src/tests/memory.rs
@@ -320,13 +320,13 @@ fn kept_glyphs(r: &mut Renderer, d: &Document) -> Vec>> {
.collect()
}
-/// `f`'s result from a run during which the fonts did not change: other tests add fonts, and the
-/// renderer lays all type out again when the fonts change.
+/// `f`'s result from a run during which the fonts and the hyphenation exceptions did not change:
+/// other tests change them, and the renderer lays all type out again when they change.
fn fonts_unchanged(mut f: impl FnMut() -> T) -> T {
loop {
- let fonts = vectorcraft_text::FontDb::global().generation();
+ let inputs = vectorcraft_text::layout_inputs();
let t = f();
- if vectorcraft_text::FontDb::global().generation() == fonts {
+ if vectorcraft_text::layout_inputs() == inputs {
return t;
}
}
diff --git a/crates/render/src/tests/typeedges.rs b/crates/render/src/tests/typeedges.rs
index fae0207d0d..3295342ce8 100644
--- a/crates/render/src/tests/typeedges.rs
+++ b/crates/render/src/tests/typeedges.rs
@@ -859,7 +859,7 @@ fn type_outside_its_frame_is_drawn() {
fn a_pan_draws_the_type_from_the_kept_pixels() {
let _fonts = crate::tests_fontchange::fonts_steady();
let opts = RenderOptions::default();
- let fonts = || vectorcraft_text::FontDb::global().generation();
+ let fonts = vectorcraft_text::layout_inputs;
// How many type objects a renderer drew offscreen for `b` in `second` after drawing `a` in
// `first`. The second frame's pixels are a new renderer's, each channel within `near`. The
// frames are drawn again while the fonts change between them (other tests load fonts, and the
diff --git a/crates/svg/tests/it/symbols_layers.rs b/crates/svg/tests/it/symbols_layers.rs
index 8e83b6262e..005a03e0fa 100644
--- a/crates/svg/tests/it/symbols_layers.rs
+++ b/crates/svg/tests/it/symbols_layers.rs
@@ -272,6 +272,33 @@ fn instances_the_def_cant_stand_for_get_their_own_art() {
assert_similar(&render_artboard(&d), &resvg_render(&svg, 200, 120), 24.0, 0.002);
}
+/// An instance half stained red whose Transform effect moves it 60 pt right is written in its
+/// stained colors where the canvas draws it, on its own and inside a group.
+#[test]
+fn a_stained_instance_with_a_geometry_effect_keeps_its_stain() {
+ for grouped in [false, true] {
+ let mut d = symbol_doc(0.0, &[]);
+ let mut stained = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "My Mark".into(), xf: Affine::translate((40.0, 60.0)) });
+ stained.appearance = Appearance::basic(Paint::solid(Color::rgb8(255, 0, 0)), Paint::None, 0.0).into();
+ stained.appearance.items.retain(|i| i.is_fill());
+ if let vectorcraft_doc::AppearanceItem::Fill(f) = &mut stained.appearance.items[0] {
+ f.opacity = 0.5;
+ }
+ stained.appearance.effects.push(vectorcraft_doc::Effect {
+ id: "distort.transform".into(),
+ params: serde_json::json!({"moveH": 60.0}),
+ visible: true,
+ });
+ let n = if grouped { Node::group(d.alloc_id(), vec![Arc::new(stained)]) } else { stained };
+ let l = d.layers[0].id;
+ d.insert(Some(l), usize::MAX, n).unwrap();
+ let svg = export(&d, &ExportOptions::default());
+ let blue = Color::rgb8(30, 90, 220).to_hex().to_ascii_lowercase();
+ assert!(!svg.to_ascii_lowercase().contains(&blue), "the symbol's own blue is stained: {svg}");
+ assert_similar(&render_artboard(&d), &resvg_render(&svg, 200, 120), 24.0, 0.003);
+ }
+}
+
#[test]
fn pattern_paints_in_symbols_stay_on_the_page() {
let mut d = Document::new(200.0, 120.0);
diff --git a/crates/text/src/hyphen.rs b/crates/text/src/hyphen.rs
index e293346279..c940ed85a4 100644
--- a/crates/text/src/hyphen.rs
+++ b/crates/text/src/hyphen.rs
@@ -502,16 +502,16 @@ mod tests {
}
/// Process-wide store used by `prefs.set` / restore. Each change makes envelopes measure type
- /// again.
+ /// again, and caches of layouts lay type out again.
#[test]
fn set_hyphenation_exceptions_round_trips_the_source_string() {
- let inputs = crate::layout::envelope_ink_inputs();
+ let inputs = crate::layout_inputs();
set_hyphenation_exceptions("typography, hap-pen");
assert_eq!(hyphenation_exceptions(), "typography, hap-pen");
- let changed = crate::layout::envelope_ink_inputs();
- assert_ne!(changed, inputs, "the envelope inputs change with the exceptions");
+ let changed = crate::layout_inputs();
+ assert_ne!(changed, inputs, "the layout inputs change with the exceptions");
set_hyphenation_exceptions("");
assert!(hyphenation_exceptions().is_empty());
- assert_ne!(crate::layout::envelope_ink_inputs(), changed, "and again when they are cleared");
+ assert_ne!(crate::layout_inputs(), changed, "and again when they are cleared");
}
}
diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs
index 82a849a3dc..a7d7d1d3f3 100644
--- a/crates/text/src/layout.rs
+++ b/crates/text/src/layout.rs
@@ -46,9 +46,7 @@ pub(crate) use driver::{fit_passes, layout_faces_per_call};
pub(crate) use on_path::{layout_trying_each_start, path_arrangements};
use vertical::{TcyStyle, stands_upright, tate_chu_yoko, upright_cell, upright_centre, upright_extent, whole_character_widths};
-#[cfg(test)]
-pub(crate) use driver::envelope_ink_inputs;
-pub use driver::{envelope_ink, register_envelope_ink};
+pub use driver::{envelope_ink, layout_inputs, register_envelope_ink};
const EPS: f64 = 1e-6;
diff --git a/crates/text/src/layout/driver.rs b/crates/text/src/layout/driver.rs
index 9108cc171d..59c6ac38bf 100644
--- a/crates/text/src/layout/driver.rs
+++ b/crates/text/src/layout/driver.rs
@@ -191,12 +191,13 @@ pub fn envelope_ink(t: &TextObject, m: Affine, projection: Option<(vectorcraft_g
/// Make envelopes map type from [`envelope_ink`], measured again when the fonts or the hyphenation
/// exceptions change (the first registration stays, see `vectorcraft_doc::live::set_type_ink`).
pub fn register_envelope_ink() {
- vectorcraft_doc::live::set_type_ink(envelope_ink, envelope_ink_inputs);
+ vectorcraft_doc::live::set_type_ink(envelope_ink, layout_inputs);
}
-/// A number that changes whenever what [`envelope_ink`] reads besides the type changes: the
-/// installed fonts and the hyphenation exceptions.
-pub(crate) fn envelope_ink_inputs() -> u64 {
+/// A number that changes when the fonts ([`FontDb::generation`]) or the hyphenation exceptions
+/// (Preferences › Hyphenation › Exceptions) change. Caches of layouts made with the global font
+/// database, and of art made from them, key on it.
+pub fn layout_inputs() -> u64 {
FontDb::global().generation().rotate_left(32) ^ crate::hyphen::exceptions_generation()
}
diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs
index 2ff6b9ad5b..6ae85931ca 100644
--- a/crates/text/src/lib.rs
+++ b/crates/text/src/lib.rs
@@ -47,7 +47,7 @@ pub use fontdb::{
user_font_dirs,
};
pub use hyphen::{hyphenation_exceptions, set_hyphenation_exceptions};
-pub use layout::{envelope_ink, register_envelope_ink};
+pub use layout::{envelope_ink, layout_inputs, register_envelope_ink};
use std::sync::Arc;
pub use svg_glyph::{SvgGlyphReader, set_svg_glyph_reader};
diff --git a/crates/tools/src/addons/parametric_shape.rs b/crates/tools/src/addons/parametric_shape.rs
index d16cb63935..68a42a665f 100644
--- a/crates/tools/src/addons/parametric_shape.rs
+++ b/crates/tools/src/addons/parametric_shape.rs
@@ -4,7 +4,9 @@
//! dragging, ←/→ its second value or corner radius. A click selects a parametric shape and shows
//! its handles (centre, size, rotation, corner radius and one of the kind's own), whose drags run
//! `addon.pathtools.shape.set`; a double-click on the canvas places one with the last settings.
-//! Each gesture is one undo step.
+//! Each gesture is one undo step. A new shape's center (from a press or a double-click) and the
+//! point dragged out from it snap as a drawing tool's points do ([`DrawSnap`]), hovering before
+//! the press too.
//!
//! A parametric shape is an ordinary path that keeps its numbers in one entry of its object data
//! ([`KEY`]) with a fingerprint of the outline they drew. Moved, turned or scaled by any tool it
@@ -17,6 +19,7 @@ use vectorcraft_geom::{Affine, BezPath, PathData, Point, Vec2};
use vectorcraft_pathops::addons::pathtools::shapes::fingerprint;
pub use vectorcraft_pathops::addons::pathtools::shapes::{MAX_COUNT, MAX_SIZE, MIN_COUNT, MIN_SIZE, ParametricShape, ShapeKind};
+use crate::guides::DrawSnap;
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
/// The tool's id.
@@ -209,7 +212,7 @@ fn angle(c: Point, p: Point, shift: bool) -> f64 {
#[derive(Clone, Debug, PartialEq)]
enum Drag {
/// Drawing a shape out from its centre.
- Create { center: Point },
+ Create { center: Point, from: Point },
/// Dragging a handle of shape `id`, which was `shape` when the drag began at `from`.
Edit { id: NodeId, shape: ParametricShape, grip: Grip, from: Point },
}
@@ -221,11 +224,18 @@ pub struct ParametricShapeTool {
drag: Option,
/// Where the pointer last was during the drag, and the keys held.
last: (Point, Mods),
+ /// Smart Guides for a new shape's center and the point dragged out from it.
+ snap: DrawSnap,
}
impl Default for ParametricShapeTool {
fn default() -> Self {
- Self { next: ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0), drag: None, last: (Point::ZERO, Mods::default()) }
+ Self {
+ next: ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0),
+ drag: None,
+ last: (Point::ZERO, Mods::default()),
+ snap: DrawSnap::default(),
+ }
}
}
@@ -247,7 +257,7 @@ impl ParametricShapeTool {
fn shaped(&self, drag: &Drag, p: Point, mods: Mods) -> (&'static str, ParametricShape, Option) {
let fit = |v: f64| v.clamp(MIN_SIZE, MAX_SIZE);
match *drag {
- Drag::Create { center } => {
+ Drag::Create { center, .. } => {
let r = center.distance(p);
let aspect = self.next.width / self.next.height;
let s = ParametricShape {
@@ -288,6 +298,18 @@ impl ParametricShapeTool {
}
Some(Action::Preview(cmd.into(), p))
}
+
+ /// Show the Smart Guides of a new shape's center at `p`, the pointer hovering; none over a
+ /// handle of the selected shape or over a parametric shape, where a press does not draw.
+ fn hover(&mut self, cx: &ToolContext, p: Point) {
+ let tol = cx.point_tol();
+ let on_grip = selected(cx).is_some_and(|(_, s)| grips(cx, &s).iter().any(|(_, at)| at.distance(p) <= tol));
+ if on_grip || shape_at(cx, p).is_some() {
+ self.snap.clear();
+ } else {
+ self.snap.hover(cx, p, &[], None);
+ }
+ }
}
impl Tool for ParametricShapeTool {
@@ -300,6 +322,9 @@ impl Tool for ParametricShapeTool {
}
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec {
+ if ev.kind == PointerKind::Move && self.drag.is_none() {
+ self.hover(cx, ev.pos);
+ }
match ev.kind {
PointerKind::Move => vec![],
PointerKind::Down => {
@@ -309,15 +334,17 @@ impl Tool for ParametricShapeTool {
near.min_by(|a, b| a.1.distance(ev.pos).total_cmp(&b.1.distance(ev.pos))).map(|(g, _)| (id, s, g))
});
if let Some((id, shape, grip)) = grip {
+ self.snap.clear();
self.drag = Some(Drag::Edit { id, shape, grip, from: ev.pos });
self.last = (ev.pos, ev.mods);
return vec![Action::Begin(EDIT.into())];
}
if let Some(id) = shape_at(cx, ev.pos) {
+ self.snap.clear();
return if cx.selection.objects == [id] { vec![] } else { vec![Action::Exec("select.set".into(), json!({ "ids": [id.0] }))] };
}
- let (center, _) = crate::guides::snap_draw(cx, ev.pos, &[]);
- self.drag = Some(Drag::Create { center });
+ let center = self.snap.press(cx, ev.pos, &[], None);
+ self.drag = Some(Drag::Create { center, from: ev.pos });
self.last = (center, ev.mods);
vec![Action::Begin(CREATE.into())]
}
@@ -325,15 +352,19 @@ impl Tool for ParametricShapeTool {
if self.drag.is_none() {
return vec![];
}
- self.last = (ev.pos, ev.mods);
+ // The point a new shape is dragged out to snaps; a handle's drag does not.
+ let pos = if matches!(self.drag, Some(Drag::Create { .. })) { self.snap.drag(cx, ev.pos, None) } else { ev.pos };
+ self.last = (pos, ev.mods);
let Some(preview) = self.preview() else { return vec![] };
if ev.kind == PointerKind::Drag {
return vec![preview];
}
+ self.snap.clear();
let Some(drag) = self.drag.take() else { return vec![] };
match drag {
- // A click draws nothing (a double-click places a shape).
- Drag::Create { center } if center.distance(ev.pos) <= cx.tol(2.0) => vec![Action::Cancel],
+ // A click draws nothing (a double-click places a shape), even when its center
+ // snapped a few pixels from where it was pressed.
+ Drag::Create { from, .. } if from.distance(ev.pos) <= cx.tol(2.0) => vec![Action::Cancel],
Drag::Create { .. } => {
if let Action::Preview(_, p) = &preview
&& let Ok(s) = apply(self.next, p)
@@ -350,7 +381,8 @@ impl Tool for ParametricShapeTool {
if self.drag.is_some() || shape_at(cx, ev.pos).is_some() {
return vec![];
}
- let s = ParametricShape { center: ev.pos, ..self.next };
+ let s = ParametricShape { center: self.snap.press(cx, ev.pos, &[], None), ..self.next };
+ self.snap.clear();
vec![Action::Exec("addon.pathtools.shape.create".into(), to_json(&s))]
}
}
@@ -358,6 +390,7 @@ impl Tool for ParametricShapeTool {
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec {
if key == ToolKey::Escape && self.drag.take().is_some() {
+ self.snap.clear();
return vec![Action::Cancel];
}
if !matches!(self.drag, Some(Drag::Create { .. })) {
@@ -385,13 +418,16 @@ impl Tool for ParametricShapeTool {
}
fn overlays(&self, cx: &ToolContext) -> Vec {
- if let Some(Drag::Create { center }) = &self.drag {
- let (_, s, _) = self.shaped(&Drag::Create { center: *center }, self.last.0, self.last.1);
+ if let Some(drag @ Drag::Create { .. }) = &self.drag {
+ let (_, s, _) = self.shaped(drag, self.last.0, self.last.1);
let text = format!("{}\nAngle: {:.1}°", cx.size_label(s.width, s.height), s.rotation);
- return vec![Overlay::Measure { p: self.last.0 + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text }];
+ let mut out = self.snap.guides().to_vec();
+ out.push(Overlay::Measure { p: self.last.0 + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text });
+ return out;
}
- let Some((_, s)) = selected(cx) else { return vec![] };
- // The shape's box, then its handles.
+ // The hovering pointer's Smart Guides, then the selected shape's box and handles.
+ let mut out = self.snap.guides().to_vec();
+ let Some((_, s)) = selected(cx) else { return out };
let (rx, ry) = (s.width / 2.0, s.height / 2.0);
let mut bp = BezPath::new();
let f: Affine = s.frame();
@@ -400,7 +436,7 @@ impl Tool for ParametricShapeTool {
if i == 0 { bp.move_to(*p) } else { bp.line_to(*p) }
}
bp.close_path();
- let mut out = vec![Overlay::Path { path: bp, color: BLUE, width: 1.0, dashed: true }];
+ out.push(Overlay::Path { path: bp, color: BLUE, width: 1.0, dashed: true });
for (grip, p) in grips(cx, &s) {
if grip == Grip::Rotate {
out.push(Overlay::Line { a: f * Point::new(0.0, -ry), b: p, color: BLUE, dashed: false });
@@ -418,6 +454,11 @@ impl Tool for ParametricShapeTool {
Cursor::ParametricShape
}
+ fn deactivate(&mut self, _cx: &ToolContext) -> Vec {
+ self.snap.clear();
+ vec![]
+ }
+
/// The next shape's numbers (its size and rotation are a double-click's).
fn options(&self) -> Value {
let mut v = to_json(&self.next);
diff --git a/crates/tools/src/builder.rs b/crates/tools/src/builder.rs
index 5a8d44d400..f5be7ac4c5 100644
--- a/crates/tools/src/builder.rs
+++ b/crates/tools/src/builder.rs
@@ -695,6 +695,14 @@ impl Tool for ShapeBuilderTool {
self.finish_edit(cx)
}
+ /// The pointer hovers nothing for the tool: its highlight, the stroke a click would split and
+ /// the cursor swatch preview go, and a map that arrives afterwards highlights nothing.
+ fn hover_ended(&mut self, _cx: &ToolContext) {
+ self.hover = None;
+ self.stroke = None;
+ self.at = None;
+ }
+
fn poll(&mut self, cx: &ToolContext, wait: bool) -> Vec {
let mut acts = self.poll_merge(cx, wait);
if !acts.is_empty() || self.dragging {
diff --git a/crates/tools/src/builder/live_paint.rs b/crates/tools/src/builder/live_paint.rs
index 2ae2f082ee..e5f696a5a7 100644
--- a/crates/tools/src/builder/live_paint.rs
+++ b/crates/tools/src/builder/live_paint.rs
@@ -769,6 +769,11 @@ impl Tool for LivePaintSelectionTool {
vec![]
}
+ /// The pointer hovers nothing for the tool: its highlight goes.
+ fn hover_ended(&mut self, _cx: &ToolContext) {
+ self.hover = LpHover::None;
+ }
+
fn overlays(&self, cx: &ToolContext) -> Vec {
if let Some(r) = self.drag.and_then(|d| d.marquee(cx)) {
return vec![Overlay::Marquee(r)];
diff --git a/crates/tools/src/builder/tests.rs b/crates/tools/src/builder/tests.rs
index 01b64ee8c3..90e4832771 100644
--- a/crates/tools/src/builder/tests.rs
+++ b/crates/tools/src/builder/tests.rs
@@ -920,6 +920,43 @@ fn shape_builder_shift_drag_shows_no_scissors_or_stroke_highlight() {
assert_eq!(t.cursor(&cx, Point::new(75.0, 75.0), shift), Cursor::ShapeBuilder);
}
+/// When the pointer leaves the canvas, the Shape Builder's region or stroke highlight and its
+/// cursor swatch preview go.
+#[test]
+fn shape_builder_hover_ended_drops_the_highlight_and_the_swatch_preview() {
+ let (d, s) = corner_rects();
+ let p = paint();
+ let cx = cx(&d, &s, &p);
+ let mut t = ShapeBuilderTool::default();
+ for (key, v) in [("pickColorFrom", json!("swatches")), ("cursorSwatchPreview", json!(true)), ("clickStrokeSplitsPath", json!(true))] {
+ t.set_option(key, &v);
+ }
+ // Over a region, then over a stroke a click would split.
+ for (x, y) in [(25.0, 25.0), (-1.0, 25.0)] {
+ t.pointer(&cx, &ev(PointerKind::Move, x, y));
+ let ov = t.overlays(&cx);
+ assert!(ov.iter().any(|o| matches!(o, Overlay::Swatch { .. })) && ov.iter().any(|o| matches!(o, Overlay::Path { .. })), "{ov:?}");
+ t.hover_ended(&cx);
+ assert!(t.overlays(&cx).is_empty());
+ }
+}
+
+/// A map of a large selection that arrives after the pointer leaves the canvas highlights nothing
+/// under the point where the pointer was last seen.
+#[test]
+fn shape_builder_map_arriving_after_hover_ended_highlights_nothing() {
+ let (d, s) = square_row(40);
+ let p = paint();
+ let cx = ToolContext { revision: (1, 1), ..cx(&d, &s, &p) };
+ let mut t = ShapeBuilderTool::default();
+ t.pointer(&cx, &ev(PointerKind::Move, 15.0, 10.0));
+ assert!(t.waiting() && t.hover.is_none(), "the map is built on another thread");
+ t.hover_ended(&cx);
+ t.poll(&cx, true);
+ assert!(!t.waiting());
+ assert!(t.hover.is_none() && t.overlays(&cx).is_empty(), "{:?}", t.hover);
+}
+
#[test]
fn shape_builder_stroke_options_round_trip_and_pick_the_side_under_the_pointer() {
let (d, s) = corner_rects();
@@ -1593,6 +1630,21 @@ fn live_paint_selection_hover_builds_no_map_of_the_selection() {
assert!(!t.waiting());
}
+/// When the pointer leaves the canvas, the Live Paint Selection tool's highlight goes.
+#[test]
+fn live_paint_selection_hover_ended_drops_the_highlight() {
+ let (d, ..) = live_group_doc();
+ let s = Selection::default();
+ let p = paint();
+ let cx = cx(&d, &s, &p);
+ let mut t = LivePaintSelectionTool::default();
+ t.pointer(&cx, &ev(PointerKind::Move, 75.0, 25.0));
+ let ov = t.overlays(&cx);
+ assert!(ov.iter().any(|o| matches!(o, Overlay::Path { .. })), "{ov:?}");
+ t.hover_ended(&cx);
+ assert!(t.overlays(&cx).is_empty());
+}
+
/// A Live Paint Selection click pressed with `down`: the press does nothing, and the release does
/// what is returned.
fn lp_click(t: &mut LivePaintSelectionTool, cx: &ToolContext, down: PointerEvent) -> Vec {
diff --git a/crates/tools/src/cursors.toml b/crates/tools/src/cursors.toml
index 6f5b32e56a..674ecebf60 100644
--- a/crates/tools/src/cursors.toml
+++ b/crates/tools/src/cursors.toml
@@ -453,6 +453,7 @@ group = "Navigation"
shown_when = [
"The Rotate, Reflect, Scale and Shear tools with nothing selected.",
"Puppet Warp off the mesh, where no pin can go.",
+ "The Smooth tool, and the Pencil or Blob Brush where Alt gives the Smooth tool, when no path is selected that the Smooth tool can act on: a drag changes nothing.",
]
hotspot = "The centre."
designer = false
@@ -690,10 +691,10 @@ look = "Only a small dot at the centre, as the Blob Brush's: the tip outline dra
id = "Smooth"
group = "Drawing and editing tools"
shown_when = [
- "The Smooth tool: a drag along a selected path smooths it.",
- "The Pencil tool with Alt key toggles to Smooth Tool on: with Alt held before a stroke begins to draw, and through a drag that smooths.",
- "The Paintbrush tool with Alt held before a press while a path is selected that the Smooth tool can act on (an editable path, not a guide, with three or more anchors on one of its subpaths), and through a drag that smooths.",
- "The Blob Brush tool with Alt held before a press, or before the drag has moved from the press point, and through a drag that smooths.",
+ "The Smooth tool while a path is selected that it can act on (an editable path, not a guide, with three or more anchors on one of its subpaths), wherever the pointer is: a drag along the path smooths it. With no such path selected, Not Allowed shows.",
+ "The Pencil tool with Alt key toggles to Smooth Tool on and such a path selected: with Alt held before a stroke begins to draw, and through a drag that smooths.",
+ "The Paintbrush tool with Alt held before a press while such a path is selected, and through a drag that smooths.",
+ "The Blob Brush tool with such a path selected: with Alt held before a press, or before the drag has moved from the press point, and through a drag that smooths.",
]
hotspot = "The tip."
diff --git a/crates/tools/src/dimension.rs b/crates/tools/src/dimension.rs
index 14f7f85881..ff99091153 100644
--- a/crates/tools/src/dimension.rs
+++ b/crates/tools/src/dimension.rs
@@ -344,10 +344,19 @@ impl DimensionTool {
_ => Orientation::Aligned,
};
}
- d.place(p);
+ // A dimension dragged out from art starts with placeholder points. Read the art's points
+ // before placing the dimension at the pointer. Clear the offset first: an out-of-range
+ // offset from the last pointer position fails the read.
+ if !matches!(d.mode, DimensionMode::Radius | DimensionMode::Diameter) {
+ d.offset = 0.0;
+ }
if d.resolve(cx.doc).is_err() {
return vec![];
}
+ d.place(p);
+ if d.validate().is_err() {
+ return vec![];
+ }
let mut actions = vec![];
if !self.began {
actions.push(Action::Begin("Create Dimension".into()));
@@ -675,6 +684,53 @@ mod tests {
assert!(matches!(placed.last(), Some(Action::Commit)), "{placed:?}");
}
+ /// A pointer position too far away to place the dimension shows no preview, and the preview
+ /// comes back where the pointer returns.
+ #[test]
+ fn placement_recovers_after_a_position_out_of_range() {
+ let (d, _) = doc_with_rect();
+ let (s, p) = (Selection::default(), paint());
+ let c = cx(&d, &s, &p);
+ let mut t = DimensionTool::default();
+ for (x, y) in [(400.0, 400.0), (450.0, 400.0)] {
+ t.pointer(&c, &PointerEvent::new(PointerKind::Down, x, y));
+ t.pointer(&c, &PointerEvent::new(PointerKind::Up, x, y));
+ }
+ assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Move, 420.0, 3.0e6)).is_empty());
+ let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Move, 420.0, 450.0));
+ let Some(Action::Preview(_, params)) = acts.last() else { panic!("{acts:?}") };
+ assert_eq!(params["offset"], json!(50.0));
+ }
+
+ /// Dragging out from a segment previews the dimension at the pointer from the first drag on.
+ #[test]
+ fn the_first_preview_of_a_segment_drag_is_at_the_pointer() {
+ let (d, _) = doc_with_rect();
+ let (s, p) = (Selection::default(), paint());
+ let c = cx(&d, &s, &p);
+ let mut t = DimensionTool::default();
+ t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 100.0));
+ let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 150.0, 60.0));
+ let Some(Action::Preview(_, params)) = acts.last() else { panic!("{acts:?}") };
+ assert_eq!(params["offset"], json!(-40.0), "40 pt above the top edge");
+ }
+
+ /// Dragging out from a corner previews the angle at the pointer's distance from the corner
+ /// from the first drag on.
+ #[test]
+ fn the_first_preview_of_a_corner_drag_is_at_the_pointer() {
+ let (d, _) = doc_with_rect();
+ let (s, p) = (Selection::default(), paint());
+ let c = cx(&d, &s, &p);
+ let mut t = DimensionTool::default();
+ t.set_option("mode", &json!("angular"));
+ t.pointer(&c, &PointerEvent::new(PointerKind::Down, 100.0, 100.0));
+ let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 130.0, 140.0));
+ let Some(Action::Preview(_, params)) = acts.last() else { panic!("{acts:?}") };
+ assert_eq!(params["vertex"], json!([100.0, 100.0]));
+ assert_eq!(params["offset"], json!(50.0), "50 pt from the corner");
+ }
+
/// A Linear press on an anchor picks that anchor even when the pointer moves a few pixels
/// before the release: it does not drag out the dimension of a segment that ends there. Both
/// picks keep their links to the path, so the dimension measures between the two anchors.
diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs
index 9aed6f545b..61396e01d9 100644
--- a/crates/tools/src/draw2/gesture.rs
+++ b/crates/tools/src/draw2/gesture.rs
@@ -777,7 +777,8 @@ fn has_selected_smooth_path(cx: &ToolContext) -> bool {
cx.selection.objects.iter().filter_map(|id| cx.doc.node(*id)).any(|n| {
let mut found = false;
n.walk(&mut |child| {
- if matches!(&child.kind, NodeKind::Path { guide: false, path, .. } if path.control_bounds().is_some() && path.subpaths.iter().any(smoothable))
+ if !found
+ && matches!(&child.kind, NodeKind::Path { guide: false, path, .. } if path.control_bounds().is_some() && path.subpaths.iter().any(smoothable))
&& editable(child.id)
{
found = true;
@@ -787,6 +788,13 @@ fn has_selected_smooth_path(cx: &ToolContext) -> bool {
})
}
+/// The Smooth tool's cursor: Not Allowed when no selected path can be smoothed, since a drag then
+/// changes nothing. A selected path anywhere on the canvas is enough, so the cursor does not depend
+/// on where the pointer is.
+fn smooth_cursor(cx: &ToolContext) -> Cursor {
+ if has_selected_smooth_path(cx) { Cursor::Smooth } else { Cursor::NotAllowed }
+}
+
impl GestureTool {
pub fn new(id: &str) -> Self {
let id: &'static str = match id {
@@ -1173,7 +1181,7 @@ impl GestureTool {
let tol = cx.tol(self.edit_within);
let radius = cx.tol(SMOOTH_RADIUS);
if self.smoothing {
- if has_sel && pts.len() >= 2 {
+ if pts.len() >= 2 && has_selected_smooth_path(cx) {
return vec![Action::Exec(
"path.smoothRegion".into(),
json!({"points": points_json(pts), "radius": radius, "fidelity": cx.tol(self.smooth_fidelity)}),
@@ -1225,7 +1233,7 @@ impl GestureTool {
}),
)],
"knife" if pts.len() >= 2 => vec![Action::Exec("path.knife".into(), json!({"points": points_json(pts)}))],
- "smooth" if has_sel && pts.len() >= 2 => {
+ "smooth" if pts.len() >= 2 && has_selected_smooth_path(cx) => {
vec![Action::Exec(
"path.smoothRegion".into(),
json!({"points": points_json(pts), "radius": radius, "fidelity": cx.tol(self.fidelity)}),
@@ -1534,13 +1542,15 @@ impl Tool for GestureTool {
}
fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor {
// An active smoothing drag, or Alt before a press that would switch the Pencil, Paintbrush
- // or Blob Brush to Smooth. A Paintbrush stroke already drawing keeps drawing with Alt.
+ // or Blob Brush to Smooth. A Paintbrush stroke already drawing keeps drawing with Alt. With
+ // no selected path to smooth, the Pencil and the Blob Brush show Not Allowed, as the Smooth
+ // tool does.
if self.smoothing
|| (self.id == "pencil" && self.alt_smooth && m.alt && !self.began)
|| (!self.active && self.id == "paintbrush" && m.alt && has_selected_smooth_path(cx))
|| self.blob_alt_smooths(m)
{
- return Cursor::Smooth;
+ return smooth_cursor(cx);
}
// Before a press that would draw a straight line: over an end of a selected open path,
// the line continues that path from the end (`PointerKind::Down`).
@@ -1563,7 +1573,7 @@ impl Tool for GestureTool {
"blobBrush" => Cursor::BlobBrush,
"eraser" => Cursor::Eraser,
"knife" => Cursor::Knife,
- "smooth" => Cursor::Smooth,
+ "smooth" => smooth_cursor(cx),
"pathEraser" => Cursor::PathEraser,
"join" => Cursor::Join,
// Before a press that draws a new path the Pencil shows its star; over a selected path
diff --git a/crates/tools/src/draw2/gesture/tests.rs b/crates/tools/src/draw2/gesture/tests.rs
index 4703305d85..ece7a369bb 100644
--- a/crates/tools/src/draw2/gesture/tests.rs
+++ b/crates/tools/src/draw2/gesture/tests.rs
@@ -369,7 +369,7 @@ fn the_freehand_tools_want_every_sample() {
#[test]
fn alt_shows_the_closing_cursor_and_a_release_with_alt_closes() {
- let (d, _) = doc_with_rect();
+ let (d, rect) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
@@ -396,18 +396,26 @@ fn alt_shows_the_closing_cursor_and_a_release_with_alt_closes() {
}
// With Alt key toggles to Smooth Tool on, Alt lends the Pencil the Smooth tool instead, until
// a stroke has begun to draw: Alt then neither smooths nor closes, and the cursor stays the Pencil's.
+ // With nothing selected to smooth, the Smooth tool it lends shows Not Allowed.
let mut t = GestureTool::new("pencil");
t.set_option("altSmooth", &json!(true));
- assert_eq!(t.cursor(&cx, at, alt), Cursor::Smooth);
+ assert_eq!(t.cursor(&cx, at, alt), Cursor::NotAllowed);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 10.0, 5.0));
assert_eq!(t.cursor(&cx, at, alt), Cursor::Pencil, "a stroke that has begun");
let acts = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 20.0, 0.0).with_mods(alt));
assert!(matches!(&acts[0], Action::Preview(_, v) if v["closed"] == false), "{acts:?}");
- // A smoothing drag keeps the Smooth cursor when Alt is let go.
- t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0).with_mods(alt));
- t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 10.0, 5.0).with_mods(alt));
- assert_eq!(t.cursor(&cx, at, Mods::default()), Cursor::Smooth, "a smoothing drag");
+ // A smoothing drag keeps the lent cursor when Alt is let go: Smooth with a selected path, Not
+ // Allowed without one.
+ let mut selected = Selection::default();
+ selected.set([rect]);
+ for (sel, c) in [(&s, Cursor::NotAllowed), (&selected, Cursor::Smooth)] {
+ let cx = crate::testutil::cx(&d, sel, &p);
+ t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0).with_mods(alt));
+ t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 10.0, 5.0).with_mods(alt));
+ assert_eq!(t.cursor(&cx, at, Mods::default()), c, "a smoothing drag");
+ t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 20.0, 0.0).with_mods(alt));
+ }
}
#[test]
@@ -2531,6 +2539,117 @@ fn smooth_needs_selection_and_brackets_resize() {
assert_eq!(b.options()["size"], 11.0);
}
+/// The Smooth tool shows Not Allowed while no selected path can be smoothed: nothing selected, only
+/// type, a path of two anchors or a locked path. A selected path anywhere, alone or in a selected
+/// group, gives the Smooth cursor wherever the pointer is, and the cursor follows the selection while
+/// the tool stays chosen.
+#[test]
+fn smooth_shows_not_allowed_without_a_selected_path_to_smooth() {
+ use std::sync::Arc;
+ let (mut d, rect) = doc_with_rect();
+ let layer = d.layers[0].id;
+ let line_id = d.alloc_id();
+ let line = vectorcraft_geom::PathData::from_bezpath(&vectorcraft_geom::BezPath::from_vec(vec![
+ vectorcraft_geom::PathEl::MoveTo(Point::new(300.0, 300.0)),
+ vectorcraft_geom::PathEl::LineTo(Point::new(400.0, 300.0)),
+ ]));
+ d.insert(Some(layer), 1, vectorcraft_doc::Node::path(line_id, line, vectorcraft_doc::Appearance::default_art())).unwrap();
+ let square = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(300.0, 350.0, 350.0, 400.0));
+ let inner = vectorcraft_doc::Node::path(d.alloc_id(), square, vectorcraft_doc::Appearance::default_art());
+ let group = vectorcraft_doc::Node::group(d.alloc_id(), vec![Arc::new(inner)]);
+ let group_id = group.id;
+ d.insert(Some(layer), 2, group).unwrap();
+ let (text_doc, text) = doc_with_area_type();
+ let mut locked = d.clone();
+ locked.node_mut(rect).unwrap().locked = true;
+ let p = paint();
+ let sel = |ids: &[NodeId]| {
+ let mut s = Selection::default();
+ s.set(ids.iter().copied());
+ s
+ };
+ let t = GestureTool::new("smooth");
+ let far = Point::new(480.0, 20.0);
+ let cases: [(&vectorcraft_doc::Document, Selection, Cursor, &str); 7] = [
+ (&d, Selection::default(), Cursor::NotAllowed, "nothing selected"),
+ (&text_doc, sel(&[text]), Cursor::NotAllowed, "type"),
+ (&d, sel(&[line_id]), Cursor::NotAllowed, "a two-anchor path"),
+ (&locked, sel(&[rect]), Cursor::NotAllowed, "a locked path"),
+ (&d, sel(&[rect]), Cursor::Smooth, "a path away from the pointer"),
+ (&d, sel(&[line_id, rect]), Cursor::Smooth, "a path beside one it cannot smooth"),
+ (&d, sel(&[group_id]), Cursor::Smooth, "a group holding a path"),
+ ];
+ for (doc, s, c, what) in &cases {
+ assert_eq!(t.cursor(&cx(doc, s, &p), far, Mods::default()), *c, "{what}");
+ }
+ // The cursor changes as the selection does, with the same tool.
+ let (none, one) = (Selection::default(), sel(&[rect]));
+ for (s, c) in [(&none, Cursor::NotAllowed), (&one, Cursor::Smooth), (&none, Cursor::NotAllowed)] {
+ assert_eq!(t.cursor(&cx(&d, s, &p), Point::new(150.0, 150.0), Mods::default()), c);
+ }
+ // A drag with Not Allowed showing sends nothing.
+ let mut t = GestureTool::new("smooth");
+ let acts = drag(&mut t, &cx(&d, &none, &p), &[(50.0, 150.0), (150.0, 150.0), (250.0, 150.0)], Mods::default());
+ assert!(acts.iter().all(|a| a.is_empty()), "{acts:?}");
+}
+
+/// The Blob Brush's Alt shows Not Allowed with no selected path to smooth, and the Smooth cursor with
+/// one; the Paintbrush's Alt without one keeps the closing cursor, since its release closes the stroke.
+#[test]
+fn borrowed_smooth_shows_not_allowed_without_a_selected_path_to_smooth() {
+ let (d, rect) = doc_with_rect();
+ let none = Selection::default();
+ let mut one = Selection::default();
+ one.set([rect]);
+ let p = paint();
+ let alt = Mods { alt: true, ..Default::default() };
+ let at = Point::new(400.0, 400.0);
+ let blob = GestureTool::new("blobBrush");
+ assert_eq!(blob.cursor(&cx(&d, &none, &p), at, alt), Cursor::NotAllowed);
+ assert_eq!(blob.cursor(&cx(&d, &one, &p), at, alt), Cursor::Smooth);
+ assert_eq!(blob.cursor(&cx(&d, &none, &p), at, Mods::default()), Cursor::BlobBrush);
+ let mut pencil = GestureTool::new("pencil");
+ pencil.set_option("altSmooth", &json!(true));
+ assert_eq!(pencil.cursor(&cx(&d, &none, &p), at, alt), Cursor::NotAllowed);
+ assert_eq!(pencil.cursor(&cx(&d, &one, &p), at, alt), Cursor::Smooth);
+ let brush = GestureTool::new("paintbrush");
+ assert_eq!(brush.cursor(&cx(&d, &none, &p), at, alt), Cursor::PaintbrushClose);
+ assert_eq!(brush.cursor(&cx(&d, &one, &p), at, alt), Cursor::Smooth);
+}
+
+/// A Smooth release sends no command while the selection holds no path it can smooth, from the
+/// Smooth tool and from the Blob Brush and Pencil with Alt; with a path selected it smooths.
+#[test]
+fn smoothing_release_sends_nothing_without_a_selected_path_to_smooth() {
+ let (text_doc, text) = doc_with_area_type();
+ let (d, rect) = doc_with_rect();
+ let mut type_only = Selection::default();
+ type_only.set([text]);
+ let mut one = Selection::default();
+ one.set([rect]);
+ let p = paint();
+ let alt = Mods { alt: true, ..Default::default() };
+ let stroke = [(50.0, 150.0), (150.0, 150.0), (250.0, 160.0)];
+ let smooths = |acts: &[Vec]| acts.iter().flatten().any(|a| matches!(a, Action::Exec(c, _) if c == "path.smoothRegion"));
+ for (id, mods) in [("smooth", Mods::default()), ("blobBrush", alt), ("pencil", alt)] {
+ let tool = || {
+ let mut t = GestureTool::new(id);
+ if id == "pencil" {
+ t.set_option("altSmooth", &json!(true));
+ }
+ t
+ };
+ let mut c = cx(&text_doc, &type_only, &p);
+ c.smart_guides = false;
+ let acts = drag_held(&mut tool(), &c, &stroke, mods);
+ assert!(!smooths(&acts), "{id} with only type selected: {acts:?}");
+ let mut c = cx(&d, &one, &p);
+ c.smart_guides = false;
+ let acts = drag_held(&mut tool(), &c, &stroke, mods);
+ assert!(smooths(&acts), "{id} with a path selected: {acts:?}");
+ }
+}
+
#[test]
fn pencil_alt_toggles_to_smooth_when_enabled_and_closes_when_disabled() {
let (d, id) = doc_with_rect();
diff --git a/crates/tools/src/guides/tests.rs b/crates/tools/src/guides/tests.rs
index 1b6ddbaa12..602242d1cd 100644
--- a/crates/tools/src/guides/tests.rs
+++ b/crates/tools/src/guides/tests.rs
@@ -714,6 +714,81 @@ fn pen_click_near_a_glyph_anchor_lands_on_it() {
assert!(q.distance(anchor) > 1e-3, "{q:?} landed on the glyph anchor with Smart Guides off");
}
+/// Snap to Glyph: the Parametric Shape tool's center (a drag and a double-click) and the point
+/// dragged out from it, and the Slice tool's corners, land on a glyph's anchor point as a Pen
+/// click does, with the guides shown while hovering, and not with Snap to Glyph off.
+#[test]
+fn parametric_shape_and_slice_points_land_on_a_glyph_anchor() {
+ use crate::addons::parametric_shape::ParametricShapeTool;
+ use crate::slice::SliceTool;
+ use crate::{Action, PointerEvent, PointerKind, Tool};
+ let (d, t) = glyph_doc("H", 0.0);
+ let lay = layout_text(FontDb::global(), &t);
+ let g = lay.glyphs.iter().find(|g| !g.outline.elements().is_empty()).expect("a glyph with an outline");
+ // An anchor inside the glyph's bounds, which no glyph bounds line passes through.
+ let bb = g.outline.bounding_box();
+ let inside = |p: Point| p.x > bb.x0 + 1.0 && p.x < bb.x1 - 1.0 && p.y > bb.y0 + 1.0 && p.y < bb.y1 - 1.0;
+ let anchor = t.xf * g.outline.segments().map(|s| s.start()).find(|p| inside(*p)).expect("an anchor inside the glyph");
+ let (s, p) = (Selection::default(), paint());
+ let click = anchor + Vec2::new(1.5, -1.0);
+ let ev = |kind, q: Point| PointerEvent::new(kind, q.x, q.y);
+ let center = |v: &serde_json::Value| Point::new(v["center"][0].as_f64().unwrap(), v["center"][1].as_f64().unwrap());
+ // A press away from the type, for drags that end at the click.
+ let away = anchor + Vec2::new(-83.3, -61.7);
+ for glyphs in [true, false] {
+ let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &s, &p) };
+ let lands = |q: Point| q.distance(anchor) < 1e-9;
+ // A Parametric Shape drawn out from the click.
+ let mut tool = ParametricShapeTool::default();
+ tool.pointer(&c, &ev(PointerKind::Down, click));
+ let acts = tool.pointer(&c, &ev(PointerKind::Drag, click + Vec2::new(80.0, 60.0)));
+ let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") };
+ assert_eq!(lands(center(v)), glyphs, "drag, Snap to Glyph {glyphs}: {:?} vs {anchor:?}", center(v));
+ // A Parametric Shape dragged out to the click is sized and turned as one dragged out to
+ // the anchor itself.
+ let drawn_to = |q: Point| {
+ let mut tool = ParametricShapeTool::default();
+ tool.pointer(&c, &ev(PointerKind::Down, away));
+ tool.pointer(&c, &ev(PointerKind::Drag, q))
+ };
+ assert_eq!(drawn_to(click) == drawn_to(anchor), glyphs, "dragged point, Snap to Glyph {glyphs}");
+ // A click 3 px from the anchor draws nothing, though its center snaps onto the anchor.
+ let near = anchor + Vec2::new(2.5, -1.5);
+ let mut tool = ParametricShapeTool::default();
+ tool.pointer(&c, &ev(PointerKind::Down, near));
+ let acts = tool.pointer(&c, &ev(PointerKind::Drag, near));
+ let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") };
+ assert_eq!(lands(center(v)), glyphs, "click, Snap to Glyph {glyphs}: {:?} vs {anchor:?}", center(v));
+ assert_eq!(tool.pointer(&c, &ev(PointerKind::Up, near)), vec![Action::Cancel], "click, Snap to Glyph {glyphs}");
+ // One placed with a double-click.
+ let mut tool = ParametricShapeTool::default();
+ let acts = tool.pointer(&c, &ev(PointerKind::DoubleClick, click));
+ let Some(Action::Exec(_, v)) = acts.first() else { panic!("{acts:?}") };
+ assert_eq!(lands(center(v)), glyphs, "double-click, Snap to Glyph {glyphs}: {:?} vs {anchor:?}", center(v));
+ // The pointer hovering at the click shows the guides of the anchor it would land on.
+ let mut tool = ParametricShapeTool::default();
+ tool.pointer(&c, &ev(PointerKind::Move, click));
+ assert_eq!(!tool.overlays(&c).is_empty(), glyphs, "hover, Snap to Glyph {glyphs}");
+ // A slice dragged from the click down and to the right starts at its top-left corner.
+ let mut tool = SliceTool::default();
+ tool.pointer(&c, &ev(PointerKind::Move, click));
+ assert_eq!(!tool.overlays(&c).is_empty(), glyphs, "slice hover, Snap to Glyph {glyphs}");
+ tool.pointer(&c, &ev(PointerKind::Down, click));
+ let acts = tool.pointer(&c, &ev(PointerKind::Drag, click + Vec2::new(80.0, 60.0)));
+ let Some(Action::Preview(_, v)) = acts.get(1) else { panic!("{acts:?}") };
+ let corner = Point::new(v["x"].as_f64().unwrap(), v["y"].as_f64().unwrap());
+ assert_eq!(lands(corner), glyphs, "slice, Snap to Glyph {glyphs}: {corner:?} vs {anchor:?}");
+ // One dragged from up and to the left ends at its bottom-right corner.
+ let mut tool = SliceTool::default();
+ tool.pointer(&c, &ev(PointerKind::Down, away));
+ let acts = tool.pointer(&c, &ev(PointerKind::Drag, click));
+ let Some(Action::Preview(_, v)) = acts.get(1) else { panic!("{acts:?}") };
+ let f = |k: &str| v[k].as_f64().unwrap();
+ let end = Point::new(f("x") + f("width"), f("y") + f("height"));
+ assert_eq!(end.distance(anchor) < 1e-6, glyphs, "slice end, Snap to Glyph {glyphs}: {end:?} vs {anchor:?}");
+ }
+}
+
/// Snap to Glyph: a single line of type has an x-height guide; a paragraph shows glyph bounds only.
#[test]
fn drawn_points_line_up_with_the_x_height_of_a_single_line() {
diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs
index 3ee1d48256..1119bb0764 100644
--- a/crates/tools/src/lib.rs
+++ b/crates/tools/src/lib.rs
@@ -624,8 +624,16 @@ mod cursor_tests {
/// Use Precise Cursors turns it into the crosshair.
#[test]
fn drawing_tools_show_their_own_cursor() {
- let (d, _) = doc_with_rect();
+ let (d, rect) = doc_with_rect();
let away = Point::new(400.0, 400.0);
+ // The Smooth tool shows its own cursor only with a selected path (Not Allowed without one).
+ let mut selected = Selection::default();
+ selected.set([rect]);
+ let paint = paint();
+ let cursor_of = |id: &str| match id {
+ "smooth" => create(id).cursor(&cx(&d, &selected, &paint), away, Mods::default()),
+ _ => cursor_at(id, &d, away),
+ };
for (id, c) in [
("lasso", Cursor::Lasso),
(addons::enclosing_lasso::ID, Cursor::Lasso),
@@ -651,7 +659,7 @@ mod cursor_tests {
("symbolStyler", Cursor::SymbolStyler),
("mesh", Cursor::MeshAdd),
] {
- assert_eq!(cursor_at(id, &d, away), c, "{id}");
+ assert_eq!(cursor_of(id), c, "{id}");
assert_eq!(c.precise(), Cursor::Crosshair, "{id}");
// The Blob Brush's and the Eraser's dot is drawn: their tip outline goes round it. The
// Live Paint Bucket's bucket is drawn.
diff --git a/crates/tools/src/slice.rs b/crates/tools/src/slice.rs
index 0f804a8ef6..c4062123e8 100644
--- a/crates/tools/src/slice.rs
+++ b/crates/tools/src/slice.rs
@@ -14,7 +14,7 @@ use vectorcraft_doc::{NodeId, OrientedBox, SliceSource};
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
use crate::bbox::{Handle, hit_handle, move_delta, scale_for_drag};
-use crate::guides::{PointSnap, Targets};
+use crate::guides::{DrawSnap, PointSnap, Targets};
use crate::shape::drag_rect;
use crate::xform::{BLUE, polygon, rect_corners};
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, json_ids};
@@ -37,9 +37,26 @@ fn has_area(r: Rect) -> bool {
r.width() > 1e-6 && r.height() > 1e-6
}
-/// Snap `p` as the drawing tools do, then to the nearest ruler guide within reach (guides win).
-fn snap(cx: &ToolContext, p: Point) -> (Point, Vec) {
- let (mut q, ov) = crate::guides::snap_draw(cx, p, &[]);
+/// When the Slice tool snaps a point.
+#[derive(Clone, Copy, PartialEq)]
+enum Phase {
+ /// The pointer hovers before a press.
+ Hover,
+ /// The first corner is pressed: the targets are gathered afresh.
+ Press,
+ /// The other corner is dragged: the press's targets are kept.
+ Drag,
+}
+
+/// Snap `p` as the drawing tools do ([`DrawSnap`]), then to the nearest ruler guide within reach
+/// (guides win, and the Smart Guides are then not shown).
+fn snap(draw: &mut DrawSnap, cx: &ToolContext, p: Point, phase: Phase) -> (Point, Vec) {
+ let mut q = match phase {
+ Phase::Hover => draw.hover(cx, p, &[], None),
+ Phase::Press => draw.press(cx, p, &[], None),
+ Phase::Drag => draw.drag(cx, p, None),
+ };
+ let mut ov = draw.guides().to_vec();
let tol = cx.tol(5.0);
let guides = cx.doc.ruler_guides();
let nearest = |vertical: bool, v: f64| {
@@ -50,9 +67,11 @@ fn snap(cx: &ToolContext, p: Point) -> (Point, Vec) {
};
if let Some(g) = nearest(true, p.x) {
q.x = g.pos;
+ ov.clear();
}
if let Some(g) = nearest(false, p.y) {
q.y = g.pos;
+ ov.clear();
}
(q, ov)
}
@@ -64,6 +83,8 @@ pub struct SliceTool {
last: Point,
began: bool,
guides: Vec,
+ /// Smart Guides for the slice's corners, and for the pointer hovering before a press.
+ snap: DrawSnap,
}
impl Tool for SliceTool {
@@ -77,14 +98,20 @@ impl Tool for SliceTool {
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec {
match ev.kind {
+ PointerKind::Move => {
+ if self.start.is_none() {
+ self.guides = snap(&mut self.snap, cx, ev.pos, Phase::Hover).1;
+ }
+ vec![]
+ }
PointerKind::Down => {
- let (p, _) = snap(cx, ev.pos);
- (self.start, self.last, self.began) = (Some(p), p, false);
+ let (p, guides) = snap(&mut self.snap, cx, ev.pos, Phase::Press);
+ (self.start, self.last, self.began, self.guides) = (Some(p), p, false, guides);
vec![]
}
PointerKind::Drag => {
let Some(start) = self.start else { return vec![] };
- let (p, guides) = snap(cx, ev.pos);
+ let (p, guides) = snap(&mut self.snap, cx, ev.pos, Phase::Drag);
(self.last, self.guides) = (p, guides);
let mut out = vec![];
if !self.began {
@@ -102,6 +129,7 @@ impl Tool for SliceTool {
}
PointerKind::Up => {
self.guides.clear();
+ self.snap.clear();
let began = std::mem::take(&mut self.began);
if self.start.take().is_some() && began { vec![Action::Commit] } else { vec![] }
}
@@ -123,6 +151,8 @@ impl Tool for SliceTool {
}
o
}
+ // The hovering pointer's Smart Guides.
+ None => self.guides.clone(),
_ => vec![],
}
}
@@ -134,6 +164,7 @@ impl Tool for SliceTool {
fn deactivate(&mut self, _cx: &ToolContext) -> Vec {
self.start = None;
self.guides.clear();
+ self.snap.clear();
if std::mem::take(&mut self.began) { vec![Action::Cancel] } else { vec![] }
}
}
@@ -152,8 +183,8 @@ impl Drag {
}
}
-/// What a user slice's handle snaps to, gathered when it is pressed: what the Slice tool's drag
-/// snaps to ([`snap`]), the art through [`PointSnap`] and the ruler guides on their own.
+/// What a user slice's handle snaps to, gathered when it is pressed: the art's bounds through
+/// [`PointSnap`], as a bounding-box handle snaps, and the ruler guides on their own.
struct ResizeSnap {
art: PointSnap,
rulers: Targets,
@@ -164,10 +195,10 @@ impl ResizeSnap {
Self { art: PointSnap::scaling(cx, || Targets::collect(cx.doc, &[], None)), rulers: Targets::rulers(cx) }
}
- /// Snap the resize `a` ([`scale_for_drag`]) of the slice `rect` by `handle` as [`snap`] snaps
- /// a drawn slice's corner: the handle snaps as a bounding-box handle does
- /// ([`PointSnap::snap_scale`]), then a ruler guide within 5 px of where that leaves it takes
- /// what the handle moves on its axis. Returns the scale and the guides showing why.
+ /// Snap the resize `a` ([`scale_for_drag`]) of the slice `rect` by `handle`: the handle snaps
+ /// as a bounding-box handle does ([`PointSnap::snap_scale`]), then a ruler guide within 5 px of
+ /// where that leaves it takes what the handle moves on its axis, as [`snap`] does for a drawn
+ /// slice's corner. Returns the scale and the guides showing why.
fn scale(&self, cx: &ToolContext, rect: Rect, handle: Handle, a: Affine, m: Mods) -> (Affine, Vec) {
let bx = OrientedBox::aligned(rect);
let (a, ov) = self.art.snap_scale(cx, &bx, handle, a, m.shift, m.alt);
diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs
index bd0c229ddf..763325f90e 100644
--- a/crates/tools/src/text.rs
+++ b/crates/tools/src/text.rs
@@ -134,7 +134,7 @@ fn text_hash(s: &str) -> u64 {
h.finish()
}
-/// The edited text's layout, keyed by the font database's generation and the text.
+/// The edited text's layout, keyed by [`vectorcraft_text::layout_inputs`] and the text.
type CachedLayout = ((u64, TextObject), Arc);
/// Process-global Type-tool edit-session counter: the IME target's replacement epoch.
@@ -281,9 +281,9 @@ impl TypeTool {
}
/// Layout of `t`, cached by content (overlays run every frame) and by the fonts available (the
- /// web build adds fonts after startup).
+ /// web build adds fonts after startup) and the hyphenation exceptions.
fn layout(&self, t: &TextObject) -> Arc {
- let fonts = FontDb::global().generation();
+ let fonts = vectorcraft_text::layout_inputs();
let mut c = self.cache.lock().unwrap_or_else(|e| e.into_inner());
if let Some(((g, k), l)) = c.as_ref()
&& *g == fonts
diff --git a/crates/tools/src/text/ports.rs b/crates/tools/src/text/ports.rs
index 554a1cd611..6928fbe6f0 100644
--- a/crates/tools/src/text/ports.rs
+++ b/crates/tools/src/text/ports.rs
@@ -167,7 +167,8 @@ pub(crate) fn port_near(cx: &ToolContext, p: Point) -> Option<(NodeId, Port, f64
}
/// Which selected text overflows, for [`thread_ports`]: each text object's layout verdict kept
-/// with the object and the fonts it was made from, as overlays run every frame.
+/// with the object and the layout inputs it was made with (fonts and hyphenation exceptions,
+/// [`vectorcraft_text::layout_inputs`]), as overlays run every frame.
#[derive(Default)]
pub struct PortMarks {
seen: std::collections::HashMap,
@@ -178,7 +179,7 @@ pub struct PortMarks {
/// port whether text is hidden by the size of its frame, whatever tool is active. None when more
/// than [`PORTS_MAX`] of them are selected.
pub fn thread_ports(marks: &mut PortMarks, cx: &ToolContext) -> Vec {
- let fonts = FontDb::global().generation();
+ let fonts = vectorcraft_text::layout_inputs();
let mut o = Vec::new();
let mut kept = std::collections::HashMap::new();
if !ports_shown(cx.doc, &cx.selection.objects) {
diff --git a/crates/tools/src/text/tests.rs b/crates/tools/src/text/tests.rs
index a7f302db65..94063166bc 100644
--- a/crates/tools/src/text/tests.rs
+++ b/crates/tools/src/text/tests.rs
@@ -1538,6 +1538,36 @@ fn selected_type_shows_its_ports_in_each_state() {
assert!(thread_ports(&mut marks, &cx(&d, &sel, &p)).is_empty(), "only selected type shows them");
}
+/// The out port shows whether the text fits as the hyphenation exceptions lay it out now: when
+/// Preferences › Hyphenation › Exceptions change, the selected type is laid out again.
+#[test]
+fn the_out_port_follows_the_hyphenation_exceptions() {
+ struct Clear;
+ impl Drop for Clear {
+ fn drop(&mut self) {
+ vectorcraft_text::set_hyphenation_exceptions("");
+ }
+ }
+ let _clear = Clear;
+ const WORDS: &str = "Extraordinarily comprehensive typographical considerations notwithstanding everything.";
+ const NEVER: &str = "extraordinarily, comprehensive, typographical, considerations, notwithstanding";
+ let (mut d, id) = crate::testutil::doc_with_area_type();
+ // A 70 by 170 pt frame holds the hyphenated text, and the text without those breaks overflows
+ // it.
+ let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) else { panic!("not text") };
+ t.runs = vec![TextRun::new(WORDS, CharStyle { size: 14.0, ..Default::default() })];
+ t.para.hyphenate = true;
+ t.kind = TextKind::Area { frame: vectorcraft_geom::PathData::from_bezpath(&Rect::new(0.0, 0.0, 70.0, 170.0).to_path(0.1)) };
+ let (mut sel, p) = (Selection::default(), paint());
+ sel.set([id]);
+ let mut marks = PortMarks::default();
+ let red = |ov: &[Overlay]| ov.iter().filter(|o| matches!(o, Overlay::Path { color: [230, 30, 30], .. })).count();
+ vectorcraft_text::set_hyphenation_exceptions("");
+ assert_eq!(red(&thread_ports(&mut marks, &cx(&d, &sel, &p))), 0, "the hyphenated text fits");
+ vectorcraft_text::set_hyphenation_exceptions(NEVER);
+ assert_eq!(red(&thread_ports(&mut marks, &cx(&d, &sel, &p))), 1, "the text without those breaks does not");
+}
+
/// Past [`PORTS_MAX`] selected frames (select all of an opened PDF) no ports are drawn or clicked:
/// they would bury the canvas in boxes; up to it every frame has its two.
#[test]
diff --git a/crates/tools/src/touchtype.rs b/crates/tools/src/touchtype.rs
index 172487c197..018a65aefc 100644
--- a/crates/tools/src/touchtype.rs
+++ b/crates/tools/src/touchtype.rs
@@ -99,7 +99,7 @@ struct Drag {
began: bool,
}
-/// The type object's layout, keyed by the font database's generation and the object.
+/// The type object's layout, keyed by [`vectorcraft_text::layout_inputs`] and the object.
type CachedLayout = ((u64, TextObject), Arc);
#[derive(Default)]
@@ -196,9 +196,10 @@ fn char_box(t: &TextObject, lay: &TextLayout, gi: usize) -> Option {
}
impl TouchTypeTool {
- /// Layout of `t`, cached by content (overlays run every frame) and by the fonts available.
+ /// Layout of `t`, cached by content (overlays run every frame), by the fonts available and by
+ /// the hyphenation exceptions.
fn layout(&self, t: &TextObject) -> Arc {
- let fonts = FontDb::global().generation();
+ let fonts = vectorcraft_text::layout_inputs();
let mut c = self.cache.lock().unwrap_or_else(|e| e.into_inner());
if let Some(((g, k), l)) = c.as_ref()
&& *g == fonts
diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs
index 5888b6dfcb..412dbcb4ef 100644
--- a/crates/ui-egui/src/app.rs
+++ b/crates/ui-egui/src/app.rs
@@ -136,6 +136,7 @@ impl VectorcraftApp {
custom_titlebar: false,
caption_layout: Default::default(),
desktop_frame: false,
+ desktop_keeps_frame: false,
last_window_title: String::new(),
fullscreen_for_mode: false,
graphics_adapter: None,
diff --git a/crates/ui-egui/src/canvas/home.rs b/crates/ui-egui/src/canvas/home.rs
index cd2ac9c44a..bffab8f06c 100644
--- a/crates/ui-egui/src/canvas/home.rs
+++ b/crates/ui-egui/src/canvas/home.rs
@@ -22,7 +22,7 @@ pub(super) fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
});
}
-/// The Home screen's column: welcome, New / Open, Recent Files, presets and community links.
+/// The Home screen's column: welcome, New / Open, Recent Files, Learn, presets and community links.
fn home_content(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
// The welcome and the New / Open buttons sit centred, as PhotoCraft's Home screen does (#4684).
@@ -46,6 +46,7 @@ fn home_content(app: &mut VectorcraftApp, ui: &mut Ui) {
});
});
recent(app, ui);
+ learn(app, ui);
ui.add_space(28.0);
ui.label(egui::RichText::new(tl!("Quickly start a new file")).font(theme::semibold(14.0)).color(t.text));
ui.add_space(10.0);
@@ -68,6 +69,40 @@ fn home_content(app: &mut VectorcraftApp, ui: &mut Ui) {
});
}
+/// The id of the Home screen's Quick skill guide card.
+const LEARN_GUIDE_ID: &str = "home-quick-skill-guide";
+
+/// The Home screen's Learn section: the Quick skill guide card, which shows the Learn panel's list
+/// of skills, and the Learn panel's tutorials under their skills, each a button that shows the
+/// panel at its steps (`learn.show`). Neither opens, closes or switches a document, and the panel
+/// shows beside the Home screen.
+fn learn(app: &mut VectorcraftApp, ui: &mut Ui) {
+ use crate::panels::learn::{catalog, guide_card};
+ let t = Tokens::get(ui.ctx());
+ ui.add_space(28.0);
+ ui.label(egui::RichText::new(tl!("Learn")).font(theme::semibold(14.0)).color(t.text));
+ ui.add_space(10.0);
+ guide_card(app, ui, egui::Id::unique(LEARN_GUIDE_ID), t.panel);
+ let mut open = None;
+ for skill in catalog::SKILLS {
+ ui.add_space(10.0);
+ ui.label(egui::RichText::new(skill.title).size(12.0).color(t.text_dim));
+ ui.add_space(4.0);
+ ui.horizontal_wrapped(|ui| {
+ for tutorial in skill.tutorials {
+ if widgets::secondary_button(ui, tutorial.title).clicked() {
+ open = Some(tutorial.id);
+ }
+ }
+ });
+ }
+ if let Some(id) = open
+ && let Err(e) = app.run("learn.show", json!({ "tutorial": id }))
+ {
+ app.status(e);
+ }
+}
+
/// The Home screen's Recent Files (#663): the first of File › Open Recent Files, each its name and
/// folder; a click opens it. Nothing when there are none.
fn recent(app: &mut VectorcraftApp, ui: &mut Ui) {
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
index 1f3a01528c..d9d89e0bb9 100644
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -719,7 +719,8 @@ pub fn confirm(app: &mut VectorcraftApp) -> DialogResult {
/// After a dialog closes, and each frame: a Missing Fonts dialog that another dialog replaced waits
/// for its turn again, the search a Missing Fonts or Find Linked Files dialog started stops once
/// that dialog is gone, and with no dialog open the choice about a stopped plug-in that another
-/// dialog replaced opens again, else the next document's Missing Fonts dialog (one dialog at a
+/// dialog replaced opens again, else the questions about the next restored document's linked files
+/// ([`crate::recovery::ask_next`]), else the next document's Missing Fonts dialog (one dialog at a
/// time).
pub(crate) fn settle(app: &mut VectorcraftApp) {
crate::placing::settle_swatch_paste(app);
@@ -727,6 +728,7 @@ pub(crate) fn settle(app: &mut VectorcraftApp) {
missing_fonts::stop_when_closed(app);
find_links::stop_when_closed(app);
safe_mode::reopen(app);
+ crate::recovery::ask_next(app);
missing_fonts::open_next(app);
}
diff --git a/crates/ui-egui/src/dialogs/missing_links.rs b/crates/ui-egui/src/dialogs/missing_links.rs
index 70c2370f1b..46a8dca322 100644
--- a/crates/ui-egui/src/dialogs/missing_links.rs
+++ b/crates/ui-egui/src/dialogs/missing_links.rs
@@ -54,14 +54,7 @@ fn after_open_for(app: &mut VectorcraftApp, r: &Value, notes: Vec, web:
let rows = |k: &str| r[k].as_array().cloned().unwrap_or_default();
let (missing, modified) = (rows("missingLinks"), ids(&rows("modifiedLinks")));
let notice = if missing.is_empty() || web {
- let unreadable = (!missing.is_empty()).then(|| {
- crate::i18n::msg_n(
- missing.len() as u64,
- "{n} linked file can't be read here: its preview shows",
- "{n} linked files can't be read here: their previews show",
- &[],
- )
- });
+ let unreadable = (!missing.is_empty()).then(|| unreadable_status(missing.len()));
ask_update(app, modified).or(unreadable)
} else {
open(app, missing, modified, ¬es);
@@ -71,6 +64,40 @@ fn after_open_for(app: &mut VectorcraftApp, r: &Value, notes: Vec, web:
super::settle(app);
}
+/// The status bar's line for `n` linked files the web can't read.
+fn unreadable_status(n: usize) -> String {
+ crate::i18n::msg_n(
+ n as u64,
+ "{n} linked file can't be read here: its preview shows",
+ "{n} linked files can't be read here: their previews show",
+ &[],
+ )
+}
+
+/// What the linked files of a document that opened (`document.open`'s result `r`) need, without
+/// asking yet → the status bar's lines about them, the missing files to ask about and the ids of
+/// the modified images to offer for update ([`ask_next`]). The web reads no linked files, so the
+/// missing ones get a line there; without Update Links: Ask When Modified the modified ones get a
+/// line instead of the offer.
+pub(crate) fn questions(app: &VectorcraftApp, r: &Value) -> (Vec, Vec, Vec) {
+ questions_for(app, r, cfg!(target_arch = "wasm32"))
+}
+
+fn questions_for(app: &VectorcraftApp, r: &Value, web: bool) -> (Vec, Vec, Vec) {
+ let rows = |k: &str| r[k].as_array().cloned().unwrap_or_default();
+ let (mut missing, mut modified) = (rows("missingLinks"), ids(&rows("modifiedLinks")));
+ let mut lines = vec![];
+ if web && !missing.is_empty() {
+ lines.push(unreadable_status(missing.len()));
+ missing.clear();
+ }
+ if !modified.is_empty() && app.session.prefs.update_links != "askWhenModified" {
+ lines.push(changed_on_disk_status(modified.len()));
+ modified.clear();
+ }
+ (lines, missing, modified)
+}
+
fn open(app: &mut VectorcraftApp, missing: Vec, modified: Vec, notes: &[String]) {
let fields = json!({ "missing": missing, "modified": modified, "notes": notes, "applyToAll": false, "path": "" });
app.ui.dialog = Some(Dialog::new(KIND, fields));
@@ -85,7 +112,7 @@ pub(super) fn notes(d: &Dialog) -> Vec {
/// Ask about the missing files `rest`, one at a time, then offer the modified ones `modified` for
/// update. A line about the modified files is followed by the open's `notes`
/// ([`crate::io::show_opened`]).
-pub(super) fn ask_next(app: &mut VectorcraftApp, rest: Vec, modified: Vec, notes: Vec) {
+pub(crate) fn ask_next(app: &mut VectorcraftApp, rest: Vec, modified: Vec, notes: Vec) {
if rest.is_empty() {
app.ui.dialog = None;
if let Some(notice) = ask_update(app, modified) {
@@ -309,6 +336,23 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}
+ /// A restored document's linked files: the web gets a line about the missing ones, and
+ /// without Update Links: Ask When Modified the modified ones get a line; the rest are asked
+ /// about.
+ #[test]
+ fn the_questions_about_linked_files_wait_or_become_lines() {
+ let mut app = VectorcraftApp::new(Session::new(), Default::default());
+ let r = json!({ "missingLinks": [{"name": "a.png", "path": "/x/a.png", "ids": [1]}], "modifiedLinks": [{"ids": [2]}, {"ids": [3]}] });
+ app.session.prefs.update_links = "askWhenModified".into();
+ assert_eq!(questions_for(&app, &r, false), (vec![], r["missingLinks"].as_array().unwrap().clone(), vec![json!(2), json!(3)]));
+ let changed = "2 linked images changed on disk: Update Links shows the new versions".to_string();
+ app.session.prefs.update_links = "manually".into();
+ assert_eq!(questions_for(&app, &r, false), (vec![changed.clone()], r["missingLinks"].as_array().unwrap().clone(), vec![]));
+ let unreadable = "1 linked file can't be read here: its preview shows".to_string();
+ assert_eq!(questions_for(&app, &r, true), (vec![unreadable, changed], vec![], vec![]));
+ assert!(app.ui.dialog.is_none(), "nothing is asked yet");
+ }
+
/// The question about modified linked files uses the form the language's plural rule picks for
/// the count.
#[test]
diff --git a/crates/ui-egui/src/dialogs/save_options.rs b/crates/ui-egui/src/dialogs/save_options.rs
index 5dfb886044..974854a6c3 100644
--- a/crates/ui-egui/src/dialogs/save_options.rs
+++ b/crates/ui-egui/src/dialogs/save_options.rs
@@ -253,7 +253,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
}
// Version first; the legacy version takes only its own options.
let legacy = legacy_ai(d);
- let mut rows: Vec<&FormatOption> = shown(f).filter(|o| LEGACY_ONLY.contains(&o.name) == legacy || o.name == "aiVersion").collect();
+ let mut rows: Vec<&FormatOption> = shown(f).filter(|o| fileio::file_reads(f, legacy, o.name)).collect();
// EPS's PostScript level comes last, as in Export's EPS Options.
rows.sort_by_key(|o| (o.name != "aiVersion", f.id == "eps" && o.name == "level"));
for o in rows {
@@ -269,10 +269,6 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
false
}
-/// The `.ai` options only the legacy version takes (and the only ones it takes, besides the
-/// version).
-const LEGACY_ONLY: &[&str] = &["transparentImages", "hiddenObjects", "outlineText"];
-
/// Is a `.ai` file to be saved in the legacy version 7 (`aiVersion: "7"`)?
fn legacy_ai(d: &Dialog) -> bool {
d.str("format") == "ai" && d.str("aiVersion").trim() == "7"
@@ -615,9 +611,13 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult {
return Err("name the file".into());
}
let path = if d.bool("__pick") { with_extension(path.trim(), f) } else { path };
+ // Only the options the file reads: a `.ai` file's version takes its own (when the document's
+ // own file is a `.ai`, the save remembers the others as they were).
+ let legacy = legacy_ai(d);
let mut options: Map = f
.options
.iter()
+ .filter(|o| fileio::file_reads(f, legacy, o.name))
.filter_map(|o| d.fields.get(o.name).filter(|v| !v.is_null() && v.as_str() != Some("")).map(|v| (o.name.to_string(), v.clone())))
.collect();
if uncompressed(d) {
diff --git a/crates/ui-egui/src/dialogs/save_options/tests.rs b/crates/ui-egui/src/dialogs/save_options/tests.rs
index 7a1aeb3849..286550bac7 100644
--- a/crates/ui-egui/src/dialogs/save_options/tests.rs
+++ b/crates/ui-egui/src/dialogs/save_options/tests.rs
@@ -125,6 +125,33 @@ fn save_as_ai_version_7_writes_a_file_per_artboard() {
assert_eq!((st.save_options["hiddenObjects"].clone(), st.save_options["outlineText"].clone()), (json!("visible"), json!(true)));
}
+/// Save Options sends a `.ai` save only the options its version reads, so a person's save gets no
+/// note about an option the file doesn't read, and the other version's choices stay as the last
+/// save left them.
+#[test]
+fn ai_save_options_send_only_the_options_the_version_reads() {
+ let (mut app, _, _) = desktop("art.ai");
+ for (round, legacy) in [true, false, true].into_iter().enumerate() {
+ assert_eq!(app.run("file.saveAs", json!({})).unwrap()["pending"], KIND);
+ frame(&mut app, false);
+ match round {
+ 1 => assert!(dialog(&app).bool("outlineText"), "the legacy choice is shown as last saved"),
+ 2 => assert!(!dialog(&app).bool("editingData"), "the PDF-compatible choice is shown as last saved"),
+ _ => {}
+ }
+ let d = app.ui.dialog.as_mut().unwrap();
+ d.fields.insert("aiVersion".into(), json!(if legacy { "7" } else { "pdf" }));
+ let (key, value) = if legacy { ("outlineText", true) } else { ("editingData", false) };
+ d.fields.insert(key.into(), json!(value));
+ let d = d.clone();
+ app.ui.dialog = None;
+ let r = confirm(&mut app, &d).unwrap();
+ assert!(!r["warnings"].to_string().contains("was not used"), "round {round}: {r}");
+ }
+ let st = app.session.active().unwrap();
+ assert_eq!((st.save_options["outlineText"].clone(), st.save_options["editingData"].clone()), (json!(true), json!(false)));
+}
+
/// An Illustrator file opened, then saved as `.ai`: the options dialog stays open through the
/// Enter that closed the save panel, and only an Enter after it is let go confirms it.
#[test]
diff --git a/crates/ui-egui/src/dialogs/tests_screens.rs b/crates/ui-egui/src/dialogs/tests_screens.rs
index 7ae09dbdf6..1bd4f10afc 100644
--- a/crates/ui-egui/src/dialogs/tests_screens.rs
+++ b/crates/ui-egui/src/dialogs/tests_screens.rs
@@ -88,6 +88,39 @@ fn opens_on_defaults_and_draws_every_view() {
assert_eq!((spec(export_for_screens::KIND).ok_label.unwrap())(&app), "Export Artboard");
}
+/// The dialog's own request, with a row of every format its Format menu offers and every format's
+/// settings as it opens, gets no note about an option a file doesn't read.
+#[test]
+fn the_dialogs_own_rows_and_settings_get_no_unused_option_notes() {
+ let (mut app, _) = app(1, true);
+ open(&mut app);
+ let rows: Vec = [
+ json!({"format": "png"}),
+ json!({"format": "png8"}),
+ json!({"format": "jpg", "quality": 100}),
+ json!({"format": "webp"}),
+ json!({"format": "gif"}),
+ json!({"format": "tiff"}),
+ json!({"format": "svg"}),
+ json!({"format": "pdf"}),
+ json!({"format": "pdf", "marks": {"trim": true}, "suffix": "-print"}),
+ json!({"format": "eps"}),
+ json!({"format": "ai"}),
+ json!({"format": "vectorcraft"}),
+ ]
+ .into_iter()
+ .map(|mut r| {
+ r["scale"] = json!("1x");
+ r
+ })
+ .collect();
+ set(&mut app, "formats", json!(rows));
+ let p = export_for_screens::params(&app, app.ui.dialog.as_ref().unwrap()).unwrap();
+ let r = app.session.execute("document.exportForScreens", &p).unwrap();
+ let unused: Vec<&str> = r["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).filter(|w| w.contains("was not used")).collect();
+ assert!(unused.is_empty(), "{unused:?}");
+}
+
#[test]
fn checked_artboards_read_as_a_range() {
let (mut app, _) = app(5, false);
diff --git a/crates/ui-egui/src/font_menu.rs b/crates/ui-egui/src/font_menu.rs
index 554eb9229f..9b829114c2 100644
--- a/crates/ui-egui/src/font_menu.rs
+++ b/crates/ui-egui/src/font_menu.rs
@@ -1691,7 +1691,7 @@ pub(crate) fn end_stale_preview(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
/// What the font menus preview on and apply to: the Type tool's selected range while it edits
-/// text (the whole text when nothing is selected), else the selected text objects.
+/// text (the whole text when nothing is selected), else every frame of each selected story.
pub(crate) fn apply(app: &mut VectorcraftApp, ctx: &egui::Context, pick: FontPick) {
let previewing = crate::panels::pstate::(ctx, PREVIEWING);
match pick {
@@ -1791,7 +1791,11 @@ fn font_command(app: &VectorcraftApp, family: &str, style: Option<&str>) -> (&'s
}
("text.setRangeStyle", p)
}
- None => ("text.setStyle", p),
+ None => {
+ // With the Selection tool: every frame of each selected story.
+ crate::panels::character::target_selected_stories(app, &mut p);
+ ("text.setStyle", p)
+ }
}
}
diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv
index 1f469c69c9..4b5982cecd 100644
--- a/crates/ui-egui/src/i18n/bg.tsv
+++ b/crates/ui-egui/src/i18n/bg.tsv
@@ -4702,11 +4702,11 @@ axis Both И двете
@msg unknown effect `{id}` непознат ефект `{id}`
@msg unknown format непознат формат
@msg unknown format `{f}` (Save writes {_1}) непознат формат `{f}` (Запазването записва {_1})
-@msg unknown freeform mode `{m}` (points, lines) непознат режим на свободна форма `{m}` (точки, линии)
-@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) непознат вид градиент `{k}` (линеен, радиален, конусен, ромбовиден, свободна форма)
+@msg unknown freeform mode `{m}` (points, lines) непознат режим на свободна форма `{m}` (points, lines)
+@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) непознат вид градиент `{k}` (linear, radial, conic, diamond, freeform)
@msg unknown highlight `{h}` непознат акцент `{h}`
@msg unknown option `{path}.{k}` непозната опция `{path}.{k}`
-@msg unknown overprints `{o}` (preserve, simulate or discard) непознати надписи `{o}` (запазване, симулиране или отхвърляне)
+@msg unknown overprints `{o}` (preserve, simulate or discard) непознати надписи `{o}` (preserve, simulate или discard)
@msg unknown panel `{raw}` непознат панел `{raw}`
@msg unknown preference `{key}` непозната настройка `{key}`
@msg Preferences imported. Настройките са импортирани.
@@ -4724,7 +4724,7 @@ axis Both И двете
@msg `{key}` must be an object `{key}` трябва да бъде обект
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` се изпълнява самостоятелно, а не като стъпка от пакет
@msg unknown preset (one of {_1}) непозната предварителна настройка (една от {_1})
-@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) непозната предварителна настройка `{name}` (висока, средна, ниска или запазена: вижте flattener.presets.list)
+@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) непозната предварителна настройка `{name}` (high, medium, low или запазена: вижте flattener.presets.list)
@msg unknown preset `{name}` (see flattener.presets.list) непозната предварителна настройка `{name}` (вижте flattener.presets.list)
@msg unknown preset `{name}` (see webExport.presets.list) непозната предварителна настройка `{name}` (вижте webExport.presets.list)
@msg unknown side `{other}` (left|right|top|bottom) непозната страна `{other}` (left|right|top|bottom)
@@ -5027,6 +5027,7 @@ effect Soft Меко
VectorCraft Title Bar on Every Desktop Заглавна лента на VectorCraft на всеки работен плот
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Докато е зададена, променливата на средата VECTORCRAFT_TITLEBAR избира заглавната лента.
System Title Bar takes precedence over this option while it is on. Докато е включена, „Заглавна лента на системата“ има предимство пред тази опция.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Докато „Заглавна лента на VectorCraft на всеки работен плот“ е изключена, този работен плот изчертава заглавната лента на прозореца независимо дали тази опция е включена.
Value Axis Ос на стойностите
On Left Side От лявата страна
On Right Side От дясната страна
@@ -6057,6 +6058,8 @@ axis Y Axis Y ос
Action Options… Опции на действието…
Action Options Опции на действието
Playback Options… Опции за възпроизвеждане…
+ Button Mode Режим с бутони
+ No actions. Няма действия.
Insert Stop… Вмъкване на спиране…
Insert Stop Вмъкване на спиране
Record Stop Записване на спиране
diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv
index a23133933a..1576b72373 100644
--- a/crates/ui-egui/src/i18n/bs.tsv
+++ b/crates/ui-egui/src/i18n/bs.tsv
@@ -982,6 +982,7 @@
VectorCraft Title Bar on Every Desktop Naslovna traka VectorCrafta na svakoj radnoj površini
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Dok je varijabla okruženja VECTORCRAFT_TITLEBAR postavljena, ona bira naslovnu traku.
System Title Bar takes precedence over this option while it is on. Dok je uključena, Sistemska naslovna traka ima prednost pred ovom opcijom.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Dok je Naslovna traka VectorCrafta na svakoj radnoj površini isključena, ova radna površina crta naslovnu traku prozora bez obzira na to je li ova opcija uključena.
Exit Izlaz
Distort Izobliči
Diffuse Glow… Difuzni sjaj…
@@ -6099,6 +6100,8 @@ axis Y Axis Y osa
Action Options… Opcije akcije…
Action Options Opcije akcije
Playback Options… Opcije reprodukcije…
+ Button Mode Način dugmadi
+ No actions. Nema akcija.
Insert Stop… Umetni zaustavljanje…
Insert Stop Umetni zaustavljanje
Record Stop Snimi zaustavljanje
diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv
index 373eb0b79d..c098a7cb4b 100644
--- a/crates/ui-egui/src/i18n/ca.tsv
+++ b/crates/ui-egui/src/i18n/ca.tsv
@@ -4095,6 +4095,8 @@ digits Malayalam Malaiàlam
Action Options… Opcions de l’acció…
Action Options Opcions de l’acció
Playback Options… Opcions de reproducció…
+ Button Mode Mode de botons
+ No actions. No hi ha accions.
Insert Stop… Insereix una aturada…
Insert Stop Insereix una aturada
Record Stop Enregistra una aturada
@@ -4613,6 +4615,7 @@ digits Malayalam Malaiàlam
VectorCraft Title Bar on Every Desktop Barra de títol de VectorCraft a tots els escriptoris
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Mentre està definida, la variable d’entorn VECTORCRAFT_TITLEBAR determina la barra de títol.
System Title Bar takes precedence over this option while it is on. Mentre està activada, Barra de títol del sistema té prioritat sobre aquesta opció.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Mentre Barra de títol de VectorCraft a tots els escriptoris està desactivada, aquest escriptori dibuixa la barra de títol de la finestra tant si aquesta opció està activada com si no.
Tab Tabulació
Tab Stops Aturades de tabulació
Table Taula
diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv
index 7178be3143..c83c9945dc 100644
--- a/crates/ui-egui/src/i18n/cs.tsv
+++ b/crates/ui-egui/src/i18n/cs.tsv
@@ -1001,6 +1001,7 @@
VectorCraft Title Bar on Every Desktop Záhlaví okna VectorCraftu v každém desktopovém prostředí
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Dokud je proměnná prostředí VECTORCRAFT_TITLEBAR nastavena, záhlaví okna volí ona.
System Title Bar takes precedence over this option while it is on. Dokud je Systémové záhlaví okna zapnuté, má přednost před touto volbou.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Dokud je Záhlaví okna VectorCraftu v každém desktopovém prostředí vypnuté, toto desktopové prostředí kreslí záhlaví okna bez ohledu na to, zda je tato volba zapnutá.
Exit Ukončit
Distort Zkreslení
Diffuse Glow… Rozptýlená záře…
@@ -6600,6 +6601,8 @@ axis Y Axis osa Y
Action Options… Možnosti akce…
Action Options Možnosti akce
Playback Options… Možnosti přehrávání…
+ Button Mode Režim tlačítek
+ No actions. Žádné akce.
Insert Stop… Vložit zastavení…
Insert Stop Vložit zastavení
Record Stop Zaznamenat zastavení
@@ -7492,3 +7495,1816 @@ breakText Glyphs Glyfy
Grommets Průchodky
Grommet Inset Odstup průchodek od okraje
Diameter Průměr
+@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions 'caller', 'callee' a 'arguments' nelze použít u funkcí ve striktním režimu
+@msg A profile supports one Revolve effect. Edit the existing effect's options instead. Profil podporuje jeden efekt Rotační těleso. Upravte místo toho volby stávajícího efektu.
+@msg Add Points at Crossings: a path went missing Přidat body v křížení: jedna cesta se ztratila
+@msg Alignment must be one of the choices Zarovnání musí být jedna z nabízených voleb
+@msg Boolean.prototype.{name} was called on a value that is not a Boolean Boolean.prototype.{name} byla zavolána na hodnotě, která není Boolean
+@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB Obrazy CMYK, jejichž vzorky nešlo přečíst v CMYK, byly importovány v RGB
+@msg CSS options: {e} Volby CSS: {e}
+@msg CSV has a header but no data rows CSV má záhlaví, ale žádné řádky dat
+@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark CSV obsahuje bajty NUL: uložte ho jako UTF-8, nebo jako UTF-16 se značkou pořadí bajtů
+@msg CSV is empty CSV je prázdné
+@msg CSV is larger than {MAX_CSV_BYTES} bytes CSV je větší než {MAX_CSV_BYTES} bajtů
+@msg CSV is not UTF-16 CSV není v UTF-16
+@msg CSV is not UTF-8 CSV není v UTF-8
+@msg CSV would be larger than {MAX_CSV_BYTES} bytes CSV by bylo větší než {MAX_CSV_BYTES} bajtů
+@msg Cannot write to the location of “{name}”; choose where to save it Do umístění souboru „{name}“ nelze zapisovat; zvolte, kam ho uložit
+@msg Choose original artwork, not a dimension's generated paths Zvolte původní kresbu, ne cesty vytvořené kótou
+@msg Contour: select paths or shapes Obrys: vyberte cesty nebo tvary
+@msg Contour: the Offset Path effect is not available Obrys: efekt Odsadit cestu není dostupný
+@msg Create needs fills, strokes or both Vytvořit vyžaduje výplně, tahy nebo obojí
+@msg DCS files can only be placed linked: use File › Place with Link on Soubory DCS lze umístit jen jako propojené: použijte Soubor › Umístit se zapnutou volbou Propojení
+@msg DXF options: {e} Volby DXF: {e}
+@msg DXF scale must be a positive number up to {_1}, not {scale} Měřítko DXF musí být kladné číslo nejvýše {_1}, ne {scale}
+@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}`: {allowed}
+@msg Date.prototype method called on a value that is not a Date object Metoda Date.prototype byla zavolána na hodnotě, která není objekt Date
+@msg Dimension is locked or hidden Kóta je zamknutá nebo skrytá
+@msg Dimension mode does not match its source Režim kóty neodpovídá jejímu zdroji
+@msg Draw Inside needs exactly one selected path, compound path or text Kreslit dovnitř vyžaduje právě jednu vybranou cestu, složenou cestu nebo text
+@msg EPS Save writes all artwork: use document.export for selected-only EPS Uložení EPS zapisuje celou kresbu: pro EPS jen z výběru použijte document.export
+@msg EPS flattener: {e} Sloučení průhlednosti EPS: {e}
+@msg EPS options: {e} Volby EPS: {e}
+@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}`: {allowed}
+@msg Error.prototype.toString was called on a value that is not an object Error.prototype.toString byla zavolána na hodnotě, která není objekt
+@msg Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft Exportovat pro obrazovky zapisuje png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai nebo vectorcraft
+@msg Flare click coordinates must be finite numbers Souřadnice kliknutí pro Odlesk musí být konečná čísla
+@msg Function.prototype.toString was called on a value that is not a function Function.prototype.toString byla zavolána na hodnotě, která není funkce
+@msg HTML Text applies to object slices of type only Text HTML se týká jen řezů objektů s textem
+@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) Identifikovat objekty podle je nastaveno na XML ID: název musí být platné XML ID (začíná písmenem, podtržítkem nebo dvojtečkou; bez mezer)
+@msg Image Trace failed: internal error: its thread stopped (please report this bug) Vektorizace obrazu selhala: vnitřní chyba: její vlákno se zastavilo (nahlaste prosím tuto chybu)
+@msg Image Trace failed: {e} Vektorizace obrazu selhala: {e}
+@msg Image Trace finished, but its document was closed Vektorizace obrazu skončila, ale její dokument byl zavřen
+@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again Vektorizace obrazu skončila, ale obraz nebo jeho nastavení se mezitím změnily: vektorizujte ho znovu
+@msg Import SVG Filter reads SVG files, not SVGZ Importovat filtr SVG čte soubory SVG, ne SVGZ
+@msg Invalid dimension offset Neplatné odsazení kóty
+@msg Invalid point Neplatný bod
+@msg JSON.parse: {why} (at offset {at}) JSON.parse: {why} (na pozici {at})
+@msg JSON.stringify cannot write a structure that contains itself JSON.stringify nemůže zapsat strukturu, která obsahuje sama sebe
+@msg Join: a subpath went missing Spojit: jedna dílčí cesta se ztratila
+@msg Line Gap must be a number Mezera řádků musí být číslo
+@msg Lock Workspace is not available in VectorCraft Zamknout pracovní prostor není ve VectorCraft dostupné
+@msg Move Panel to New Window is not available: floating panels stay in the main window Přesunout panel do nového okna není dostupné: plovoucí panely zůstávají v hlavním okně
+@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString vyžaduje Namespace
+@msg New Document profiles need the preferences folder, which this run doesn't have Profily nového dokumentu vyžadují složku předvoleb, kterou toto spuštění nemá
+@msg Number.prototype.{name} was called on a value that is not a number Number.prototype.{name} byla zavolána na hodnotě, která není číslo
+@msg Object.create's prototype must be an object or null Prototyp pro Object.create musí být objekt nebo null
+@msg Object.{function} was given a value that is not an object Object.{function} dostala hodnotu, která není objekt
+@msg Offset Path: the path is too complex to offset Odsadit cestu: cesta je příliš složitá na odsazení
+@msg Offset Path: {e} Odsadit cestu: {e}
+@msg PDF content is incomplete or exceeds the reader's limits; no document was imported Obsah PDF je neúplný nebo překračuje limity čtení; žádný dokument nebyl importován
+@msg PDF flattener: {e} Sloučení průhlednosti PDF: {e}
+@msg PDF options: {e} Volby PDF: {e}
+@msg QName.prototype.toString needs a QName QName.prototype.toString vyžaduje QName
+@msg Recolor Artwork is no longer open Přebarvit kresbu už není otevřené
+@msg RegExp.prototype.{name} was called on a value that is not a RegExp object RegExp.prototype.{name} byla zavolána na hodnotě, která není objekt RegExp
+@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 SVG není v UTF-8, UTF-16 ani ISO 8859-1
+@msg Save writes one artboard: name one, or export several with document.export Uložit zapisuje jednu pracovní plochu: uveďte jednu, nebo jich několik exportujte pomocí document.export
+@msg Select a dimension first Nejprve vyberte kótu
+@msg Select dimensions to update Vyberte kóty k aktualizaci
+@msg Select only dimensions Vyberte jen kóty
+@msg Select two or more objects, or a group Vyberte dva nebo více objektů, nebo skupinu
+@msg Shaper needs 1–64 source paths Shaper vyžaduje 1–64 zdrojových cest
+@msg Stencil Bridges: the selected art is too large or too detailed to bridge Můstky šablony: vybraná kresba je příliš velká nebo příliš podrobná na přemostění
+@msg String.prototype.toString was called on a value that is not a string String.prototype.toString byla zavolána na hodnotě, která není řetězec
+@msg Switch x/y is for scatter graphs Prohodit x/y je určeno pro bodové grafy
+@msg Text options: {e} Volby textu: {e}
+@msg The {name} profile could not be read, so the new document has the default settings: {reason} Profil {name} nešlo přečíst, takže nový dokument má výchozí nastavení: {reason}
+@msg Trap: more than {_1} pairs of objects lie close enough to trap, or too many objects share a horizontal range; trap fewer objects at a time Přesah barev: více než {_1} dvojic objektů leží dost blízko pro přesah, nebo příliš mnoho objektů sdílí vodorovný rozsah; zpracujte najednou méně objektů
+@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off Rámeček a linie pro vybírání: mimo kresbu se nevejde žádná linie pro vybírání a rámeček je vypnutý
+@msg \ at end of pattern \ na konci vzoru
+@msg `alignOn` cannot be empty `alignOn` nesmí být prázdné
+@msg `alignOn` must be a string `alignOn` musí být řetězec
+@msg `align` is start, center or end `align` je start, center nebo end
+@msg `align` must be a string `align` musí být řetězec
+@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` musí být pole dvojic [dílčí cesta, kotevní bod]
+@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` musí uvádět indexy kotevních bodů (0, 1, 2…), ne {k}
+@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` musí uvádět indexy kotevních bodů (0, 1, 2…), ne {v}
+@msg `answer` for this {kind} dialog must be one of: {_1} `answer` pro tento dialog {kind} musí být jedna z hodnot: {_1}
+@msg `answers` has more than {MAX_ANSWERS} answers `answers` má více než {MAX_ANSWERS} odpovědí
+@msg `answers` is larger than 1 MiB `answers` je větší než 1 MiB
+@msg `answers` must be a list of answers `answers` musí být seznam odpovědí
+@msg `artboard` must be the index of an artboard of the document `artboard` musí být index pracovní plochy dokumentu
+@msg `artboardsAt` must be [x, y] `artboardsAt` musí být [x, y]
+@msg `artboards` must be a list of artboard indices `artboards` musí být seznam indexů pracovních ploch
+@msg `artboards` names an artboard the document does not have `artboards` uvádí pracovní plochu, kterou dokument nemá
+@msg `artboards` names an artboard twice `artboards` uvádí některou pracovní plochu dvakrát
+@msg `asset` is the asset id (see assets.list) `asset` je ID prvku (viz assets.list)
+@msg `at` must be [x, y] (got {at}) `at` musí být [x, y] (zadáno {at})
+@msg `at` must be a byte offset `at` musí být bajtový posun
+@msg `at` must be a point [x, y] `at` musí být bod [x, y]
+@msg `autoLeading` must be a number `autoLeading` musí být číslo
+@msg `background` must be "", "matte" or #rrggbb `background` musí být "", "matte" nebo #rrggbb
+@msg `beside` needs `side` `beside` vyžaduje `side`
+@msg `burasagari` must be "none", "standard" or "forced" `burasagari` musí být "none", "standard" nebo "forced"
+@msg `center` must be [x, y] `center` musí být [x, y]
+@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign` musí být "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" nebo "icfBottom"
+@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection` musí být "default", "leftToRight" nebo "rightToLeft"
+@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke` vyžaduje `stroke: false`, `ids: []` a žádné `stop`
+@msg `clear` must be a list of type object ids, not {v} `clear` musí být seznam ID textových objektů, ne {v}
+@msg `clear` must list type objects; {_1} is not one `clear` musí uvádět textové objekty; {_1} jím není
+@msg `clear` must not list `id` `clear` nesmí uvádět `id`
+@msg `code` must be a whole number `code` musí být celé číslo
+@msg `colorMode` must be one of rgb, cmyk `colorMode` musí být jedna z hodnot rgb, cmyk
+@msg `color` needs a `rule` to make the theme from `color` vyžaduje `rule`, podle kterého se motiv vytvoří
+@msg `colors` does not apply to the {name} reduction `colors` se netýká redukce {name}
+@msg `colors` is empty `colors` je prázdné
+@msg `colors` must be a whole number from 2 to 256 `colors` musí být celé číslo od 2 do 256
+@msg `colors` must be at least 9 for the uniform reduction `colors` musí být pro rovnoměrnou redukci nejméně 9
+@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors` musí být mezi 1 a {MAX_THEME_COLORS} (zadáno {n})
+@msg `columnType` is vertical, uniform, repeating or sliding `columnType` je vertical, uniform, repeating nebo sliding
+@msg `columns` must be a whole number 1..50 `columns` musí být celé číslo 1..50
+@msg `commands` must not be empty `commands` nesmí být prázdné
+@msg `contents` goes with `to` `contents` patří k `to`
+@msg `corners` must be sharp, round or square, not `{other}` `corners` musí být sharp, round nebo square, ne `{other}`
+@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners` musí uvádět indexy kotevních bodů (0, 1, 2…), ne {v}
+@msg `count` must be a whole number from 0 to {MAX_COUNT} `count` musí být celé číslo od 0 do {MAX_COUNT}
+@msg `count` must be a whole number from 1 to 100 `count` musí být celé číslo od 1 do 100
+@msg `dataBase64` is larger than the module limit `dataBase64` je větší než limit modulu
+@msg `dataBase64` is not valid base64 `dataBase64` není platný base64
+@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly` nelze kombinovat s `ids`, `reverse` ani `append`
+@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth` a `childLimit` omezují souhrn: předejte `summary: true`
+@msg `design` is a design name, or null for plain columns `design` je název návrhu, nebo null pro obyčejné sloupce
+@msg `design` is a design name, or null for the default marker `design` je název návrhu, nebo null pro výchozí značku
+@msg `destinationFolder` is required when destination is `folder` `destinationFolder` je povinné, když je cíl `folder`
+@msg `destination` must be `none`, `saveAndClose` or `folder` `destination` musí být `none`, `saveAndClose` nebo `folder`
+@msg `diameter` must be from 0.1 to 2500 pt `diameter` musí být od 0.1 do 2500 pt
+@msg `digits` must be one of {_1} `digits` musí být jedna z hodnot {_1}
+@msg `dimImages` is a percentage or false `dimImages` je procento nebo false
+@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction` musí být "auto", "leftToRight" nebo "rightToLeft"
+@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount` musí být číslo od 0 do 100
+@msg `each` is the value one design stands for, more than 0 `each` je hodnota, kterou představuje jeden návrh, větší než 0
+@msg `end` must be "start" or "end" `end` musí být "start" nebo "end"
+@msg `ends` applies to joining exactly two paths `ends` se týká spojení právě dvou cest
+@msg `event` must be one of {_1} `event` musí být jedna z hodnot {_1}
+@msg `every` must be "all", "odd" or "even", not {v} `every` musí být "all", "odd" nebo "even", ne {v}
+@msg `expand` must be true or false `expand` musí být true nebo false
+@msg `fidelity` must be from {lo} to {hi} pt `fidelity` musí být od {lo} do {hi} pt
+@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` musí být 1 až 1000 cest k souborům písem, které našlo poslední hledání text.findFontFiles
+@msg `focal` and `radius` cannot be used together `focal` a `radius` nelze použít společně
+@msg `focal` must be an array of [subpath, anchor] pairs `focal` musí být pole dvojic [dílčí cesta, kotevní bod]
+@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` uvádí [{si}, {ai}], což není kotevní bod cesty
+@msg `folder` must be an absolute folder path `folder` musí být absolutní cesta ke složce
+@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` musí být objekt se značkami os a hodnotami, například {"wght"}
+@msg `format` must be a string `format` musí být řetězec
+@msg `format` must be one of {_1} `format` musí být jedna z hodnot {_1}
+@msg `fractions` is chop or scale `fractions` je chop nebo scale
+@msg `from` and `to` must differ `from` a `to` se musí lišit
+@msg `from` has an empty frame `from` má prázdný rámec
+@msg `from` must be a non-negative integer `from` musí být nezáporné celé číslo
+@msg `from` must be area type or type on a path `from` musí být text v oblasti nebo text na cestě
+@msg `gid` must be a glyph id, not {v} `gid` musí být ID glyfu, ne {v}
+@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate` patří jednomu znaku, ne stylu znaků (nastavuje ho text.setGlyphAlternate)
+@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate` přijímá jen null, které volbu glyfu odebere (volbu nastavuje text.setGlyphAlternate)
+@msg `group` must be one of {_1} `group` musí být jedna z hodnot {_1}
+@msg `group` must be the Objects on Path group of `member` and `target` `group` musí být skupina Objekty na cestě, do které patří `member` a `target`
+@msg `guide` must be a guide index (guide.list) `guide` musí být index vodítka (guide.list)
+@msg `hAlign` must be default, left, center or right `hAlign` musí být default, left, center nebo right
+@msg `id` is an object id, not {v} `id` je ID objektu, ne {v}
+@msg `id` must be a node id `id` musí být ID uzlu
+@msg `id` must be an object id, not {v} `id` musí být ID objektu, ne {v}
+@msg `ids` must name Objects on Path groups or objects in them `ids` musí uvádět skupiny Objekty na cestě nebo objekty v nich
+@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids` přijímá nejvýše {MAX_INSPECT_IDS} ID
+@msg `imageMap` must be none, rectangle or polygon `imageMap` musí být none, rectangle nebo polygon
+@msg `includeArtboardFill` must be true or false `includeArtboardFill` musí být true nebo false
+@msg `includeSubdirs` must be true or false `includeSubdirs` musí být true nebo false
+@msg `index` must be a point index `index` musí být index bodu
+@msg `index` must be a whole number or null `index` musí být celé číslo nebo null
+@msg `indices` must be a non-empty array of item indices `indices` musí být neprázdné pole indexů položek
+@msg `item` needs `index` `item` vyžaduje `index`
+@msg `items` is a list of {kind, item} `items` je seznam {kind, item}
+@msg `items` is empty `items` je prázdné
+@msg `items` must be a list of {id, points} `items` musí být seznam {id, points}
+@msg `items` must be a list of {{id, anchors?}}, not {v} `items` musí být seznam {{id, anchors?}}, ne {v}
+@msg `items` must be an array of {id, anchors} `items` musí být pole {id, anchors}
+@msg `items` must be an array of {{id, corners?}}, not {v} `items` musí být pole {{id, corners?}}, ne {v}
+@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida` musí být "none", "short", "medium", "long" nebo "stylistic"
+@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore` patří prvnímu znaku řádku, ne stylu znaků (nastavuje ho text.setRangeStyle)
+@msg `kind` is one of {_1} `kind` je jedna z hodnot {_1}
+@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind` musí být "round", "invertedRound" nebo "chamfer", ne {v}
+@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind` musí být `text`, `visibility`, `image` nebo `graph`
+@msg `kind` must be a string `kind` musí být řetězec
+@msg `kind` must be image, noImage or htmlText `kind` musí být image, noImage nebo htmlText
+@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType` musí být "pushInFirst", "pushOutFirst" nebo "pushOutOnly"
+@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku` musí být "none", "hard" nebo "soft"
+@msg `label` must be a non-empty name of at most 200 characters `label` musí být neprázdný název o nejvýše 200 znacích
+@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language` musí být název nebo značka jazyka, například "Turkish" nebo "tr"
+@msg `leader` must be a string `leader` musí být řetězec
+@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel` musí být "romanBaseline" nebo "emBoxTop"
+@msg `leading` must be from 0.1 to 5000, or null for Auto `leading` musí být od 0.1 do 5000, nebo null pro Automaticky
+@msg `length` must be a number of at most 1000000 pt either way `length` musí být číslo nejvýše 1000000 pt v obou směrech
+@msg `line` must be true or false `line` musí být true nebo false
+@msg `max` must be 1 to {MAX_SUGGESTIONS} `max` musí být 1 až {MAX_SUGGESTIONS}
+@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle` ({min_angle}°) nemůže být větší než `maxAngle` ({max_angle}°)
+@msg `mode` must be "handles", "straight" or "smooth" `mode` musí být "handles", "straight" nebo "smooth"
+@msg `mode` must be "straight", "bezier" or "arc" `mode` musí být "straight", "bezier" nebo "arc"
+@msg `mode` must be one of {_1} `mode` musí být jedna z hodnot {_1}
+@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi` musí být "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" nebo "lineEndHalf"
+@msg `name` must be a name that is not empty `name` musí být název, který není prázdný
+@msg `name` must not be empty `name` nesmí být prázdné
+@msg `names` is a list of preset names `names` je seznam názvů přednastavení
+@msg `names` lists preset names `names` uvádí názvy přednastavení
+@msg `names` must be a list of names `names` musí být seznam názvů
+@msg `names` must be a list of swatch names `names` musí být seznam názvů položek vzorníku
+@msg `names` must be an array of style names `names` musí být pole názvů stylů
+@msg `new` is a design name, or true `new` je název návrhu, nebo true
+@msg `origin` must be [x, y] in points `origin` musí být [x, y] v bodech
+@msg `path` must be a folder `path` musí být složka
+@msg `path` must be a path object `path` musí být objekt cesty
+@msg `picks` must be an array (got {v}) `picks` musí být pole (zadáno {v})
+@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks` musí obsahovat 1 až {MAX_PICKS} voleb (zadáno {_1})
+@msg `pivot` must be a whole number from 0 to 8 `pivot` musí být celé číslo od 0 do 8
+@msg `points` must be a list of [x, y] points `points` musí být seznam bodů [x, y]
+@msg `points` must be a list of point indexes `points` musí být seznam indexů bodů
+@msg `points` must be an array of point indices `points` musí být pole indexů bodů
+@msg `points` must be point indexes `points` musí být indexy bodů
+@msg `points` must hold from 2 to 100000 points `points` musí obsahovat 2 až 100000 bodů
+@msg `port` must be "in" or "out" `port` musí být "in" nebo "out"
+@msg `position` is above or below `position` je above nebo below
+@msg `preset` must be a string `preset` musí být řetězec
+@msg `radius` must be 0 or more `radius` musí být 0 nebo více
+@msg `ratio` must be from 0.1 to 0.5 `ratio` musí být od 0.1 do 0.5
+@msg `rect` must be [x, y, width, height] `rect` musí být [x, y, width, height]
+@msg `rect` must be [x, y, width, height] with a positive size `rect` musí být [x, y, width, height] s kladnou velikostí
+@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect` musí být [x0, y0, x1, y1] (zadáno {r})
+@msg `relation` must be overlap, inside or outside `relation` musí být overlap, inside nebo outside
+@msg `replace` must be a string `replace` musí být řetězec
+@msg `reversed` must be true or false `reversed` musí být true nebo false
+@msg `rotate` must be a finite number `rotate` musí být konečné číslo
+@msg `rows` must be lists of numbers or nulls, not {r} `rows` musí být seznamy čísel nebo hodnot null, ne {r}
+@msg `rule` must be nonZero or evenOdd `rule` musí být nonZero nebo evenOdd
+@msg `scale` must be a number from 0.001 to 1000 `scale` musí být číslo od 0.001 do 1000
+@msg `scale` must be a number in (0, 100] `scale` musí být číslo v rozsahu (0, 100]
+@msg `scope.{key}` must be a non-empty array `scope.{key}` musí být neprázdné pole
+@msg `scope` must be "document", {artboards} or {layers} `scope` musí být "document", {artboards} nebo {layers}
+@msg `scope` must be "selection" or "document", not {other} `scope` musí být "selection" nebo "document", ne {other}
+@msg `script` must be a string `script` musí být řetězec
+@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes` je neprázdný seznam indexů řad
+@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes` vyžaduje `type` nebo `valueAxis`
+@msg `shift` must be a number of points `shift` musí být počet bodů
+@msg `showCenter` must be true or false `showCenter` musí být true nebo false
+@msg `side` must be "left", "right", "top" or "bottom" `side` musí být "left", "right", "top" nebo "bottom"
+@msg `side` places the artboard: leave out x, y, width and height `side` umísťuje pracovní plochu: vynechte x, y, width a height
+@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification` musí být "fullJustify", "alignLeft", "alignCenter" nebo "alignRight"
+@msg `size` is a percentage of the base text's size, 1 to 100 `size` je procento velikosti základního textu, 1 až 100
+@msg `source` and `target` are the same object `source` a `target` jsou tentýž objekt
+@msg `source` is the only path in series group `target` `source` je jediná cesta ve skupině řady `target`
+@msg `source` must be `folder` or `datasets` `source` musí být `folder` nebo `datasets`
+@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` vytvoří napříč rámečkem více než {MAX_COUNT} linií
+@msg `spacing` must be a number `spacing` musí být číslo
+@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing` musí být počet bodů, 0 nebo více, nebo "auto"
+@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing` musí být více než 0 a nejvýše {MAX_DISTANCE} pt
+@msg `start` must be a finite number `start` musí být konečné číslo
+@msg `starts` must be an array of anchor indices (or nulls) `starts` musí být pole indexů kotevních bodů (nebo hodnot null)
+@msg `status` must be linked, embedded, missing or modified, or a list of them `status` musí být linked, embedded, missing nebo modified, nebo jejich seznam
+@msg `stop` must be a stop index `stop` musí být index zarážky
+@msg `story` styles the whole story: give no `start` or `end` `story` nastavuje styl celého článku: nezadávejte `start` ani `end`
+@msg `story` takes the whole story: give no `start` or `end` `story` zahrnuje celý článek: nezadávejte `start` ani `end`
+@msg `strokeMode` applies to strokes (stroke: true) `strokeMode` platí pro tahy (stroke: true)
+@msg `style` must be roman or japanese `style` musí být roman nebo japanese
+@msg `subpaths` must be two integer indices `subpaths` musí být dva celočíselné indexy
+@msg `symbol` must be a symbol name `symbol` musí být název symbolu
+@msg `target` must be "object" or "contents", not {v} `target` musí být "object" nebo "contents", ne {v}
+@msg `target` must be an object of the same Objects on Path group as `member` `target` musí být objekt ze stejné skupiny Objekty na cestě jako `member`
+@msg `tint` applies to global and spot colours `tint` platí pro globální a přímé barvy
+@msg `tint` goes with a global or spot `swatch` `tint` patří ke globální nebo přímé barvě v `swatch`
+@msg `tint` must be a number (%) `tint` musí být číslo (%)
+@msg `tint` needs a global or spot colour `tint` vyžaduje globální nebo přímou barvu
+@msg `to` is already threaded `to` je už zřetězené
+@msg `to` is the design's new name `to` je nový název návrhu
+@msg `to` must be [x, y] `to` musí být [x, y]
+@msg `to` must be a closed path, area type or type on a path other than `from` `to` musí být uzavřená cesta, text v oblasti nebo text na cestě jiný než `from`
+@msg `to` must be a whole number `to` musí být celé číslo
+@msg `to` takes the rows of one object's stack, not aligned rows `to` přijímá řádky zásobníku jednoho objektu, ne zarovnané řádky
+@msg `tolerance` must be from 0 to 100 pt `tolerance` musí být od 0 do 100 pt
+@msg `useLab` must be true or false `useLab` musí být true nebo false
+@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign` musí být default, top, middle, baseline nebo bottom
+@msg `values` must be a list `values` musí být seznam
+@msg `values` must be an object `values` musí být objekt
+@msg `values` must name hex, rgb, cmyk, lab or gray `values` musí uvádět hex, rgb, cmyk, lab nebo gray
+@msg `values` names a kind twice `values` uvádí některý druh dvakrát
+@msg `version` must be 0 or {_1} `version` musí být 0 nebo {_1}
+@msg `version` must be 0 or {newest} `version` musí být 0 nebo {newest}
+@msg `warichuAlign` must be one of {_1} `warichuAlign` musí být jedna z hodnot {_1}
+@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines` musí být celé číslo od {_1} do {_2}
+@msg `weight` must be from 0.01 to 1000 pt `weight` musí být od 0.01 do 1000 pt
+@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights` musí být seznam {_1} čísel, jedno na řádek (zadáno {v})
+@msg `xml` must be a string `xml` musí být řetězec
+@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it položka {n} v `{KEY}` dává odstavci na bajtu {start} objektu {_1} jiná odsazení než některá položka před ní
+@msg `{KEY}` entry {n} must be an object položka {n} v `{KEY}` musí být objekt
+@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` položka {n} v `{KEY}` nenastavuje `leftIndent` ani `firstLineIndent`
+@msg `{KEY}` entry {n}: `id` must be an object id položka {n} v `{KEY}`: `id` musí být ID objektu
+@msg `{KEY}` entry {n}: `start` must be a byte offset položka {n} v `{KEY}`: `start` musí být bajtový posun
+@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt položka {n} v `{KEY}`: `{k}` musí být číslo od -{MAX_PARA} do {MAX_PARA} pt
+@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} položka {n} v `{KEY}`: žádný odstavec objektu {_1} nezačíná na bajtu {start}
+@msg `{KEY}` entry {n}: unknown key `{k}` položka {n} v `{KEY}`: neznámý klíč `{k}`
+@msg `{KEY}` entry {n}: {_1} is not a type object položka {n} v `{KEY}`: {_1} není textový objekt
+@msg `{KEY}` is empty `{KEY}` je prázdné
+@msg `{KEY}` must be a list `{KEY}` musí být seznam
+@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}` přijímá nejvýše {MAX_PARAGRAPH_INDENTS} položek (zadáno {_1})
+@msg `{KEY}` takes no `{other}` beside it `{KEY}` vedle sebe nepřijímá `{other}`
+@msg `{_1}` can't be edited or removed `{_1}` nelze upravit ani odebrat
+@msg `{_1}` is built in and always listed `{_1}` je vestavěné a je uvedeno vždy
+@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}` není soubor písma, který našlo poslední hledání text.findFontFiles
+@msg `{_1}` would be overwritten by the package: choose a different folder or name `{_1}` by balíček přepsal: zvolte jinou složku nebo název
+@msg `{base}` is larger than {_1} MB `{base}` je větší než {_1} MB
+@msg `{base}` is not in the DCS file's folder `{base}` není ve složce souboru DCS
+@msg `{file}` has no art to place `{file}` nemá žádnou kresbu k umístění
+@msg `{file}` is over {_1} MB `{file}` má více než {_1} MB
+@msg `{f}` belongs to a VectorCraft that is running `{f}` patří běžící aplikaci VectorCraft
+@msg `{f}` is the copy of a document open now `{f}` je kopie právě otevřeného dokumentu
+@msg `{id}` is no writable format (see document.formats) `{id}` není formát, který lze zapisovat (viz document.formats)
+@msg `{id}` isn't a loaded library `{id}` není načtená knihovna
+@msg `{key}` has no colours `{key}` neobsahuje žádné barvy
+@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` obsahuje více než {MAX_EDIT_WORDS} slov
+@msg `{key}` is a design name `{key}` je název návrhu
+@msg `{key}` is a design name or a list of them `{key}` je název návrhu nebo jejich seznam
+@msg `{key}` is longer than {MAX_NAME} characters `{key}` je delší než {MAX_NAME} znaků
+@msg `{key}` is out of range `{key}` je mimo rozsah
+@msg `{key}` must be 0 or more `{key}` musí být 0 nebo více
+@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` musí být [min, desired, max] nebo {{min?, desired?, max?}}, v procentech
+@msg `{key}` must be [x, y] numbers `{key}` musí být čísla [x, y]
+@msg `{key}` must be a list of words `{key}` musí být seznam slov
+@msg `{key}` must be a list, not {v} `{key}` musí být seznam, ne {v}
+@msg `{key}` must be a non-negative integer `{key}` musí být nezáporné celé číslo
+@msg `{key}` must be a number or null, not {v} `{key}` musí být číslo nebo null, ne {v}
+@msg `{key}` must be a number {_1}..{_2} `{key}` musí být číslo {_1}..{_2}
+@msg `{key}` must be a whole number of 0 or more `{key}` musí být celé číslo 0 nebo větší
+@msg `{key}` must be a whole number of at least 0, not {n} `{key}` musí být celé číslo nejméně 0, ne {n}
+@msg `{key}` must be an appearance item index (paint order) or null `{key}` musí být index položky vzhledu (pořadí vykreslení) nebo null
+@msg `{key}` must be an array of asset ids (see assets.list) `{key}` musí být pole ID prvků (viz assets.list)
+@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}` musí být od 0 do 180 stupňů, ne {a}
+@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}` musí být od 0 do {MAX_DISTANCE} pt
+@msg `{key}` must be from {_1} to {shift_max} `{key}` musí být od {_1} do {shift_max}
+@msg `{key}` must be from {lo} to {hi}, not {v} `{key}` musí být od {lo} do {hi}, ne {v}
+@msg `{key}` must be one of auto, always, never `{key}` musí být jedna z hodnot auto, always, never
+@msg `{key}` must be one of miter, round, bevel `{key}` musí být jedna z hodnot miter, round, bevel
+@msg `{key}` must be one word of at most {_1} characters `{key}` musí být jedno slovo o nejvýše {_1} znacích
+@msg `{key}` must hold single words of at most {_1} characters `{key}` musí obsahovat jednotlivá slova o nejvýše {_1} znacích
+@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}` musí uvádět objekt připojený k cestě skupiny Objekty na cestě
+@msg `{key}` names an object that is hidden or locked `{key}` uvádí objekt, který je skrytý nebo zamknutý
+@msg `{key}`: not a colour: {v} `{key}`: není barva: {v}
+@msg `{k}.family` is required `{k}.family` je povinné
+@msg `{k}` is a number 0..1 `{k}` je číslo 0..1
+@msg `{k}` is true or false `{k}` je true nebo false
+@msg `{k}` isn't an export setting: {_1} `{k}` není nastavení exportu: {_1}
+@msg `{k}` must be a byte offset `{k}` musí být bajtový posun
+@msg `{k}` must be a number `{k}` musí být číslo
+@msg `{k}` must be a number of points `{k}` musí být počet bodů
+@msg `{k}` must be a string `{k}` musí být řetězec
+@msg `{k}` must be a whole number `{k}` musí být celé číslo
+@msg `{k}` must be from {lo} to {hi} for {key} `{k}` musí být pro {key} od {lo} do {hi}
+@msg `{k}` must be {what}, not {v} `{k}` musí být {what}, ne {v}
+@msg `{l}` isn't a global or spot colour `{l}` není globální ani přímá barva
+@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}` není bitmapa: použijte clipboard.importSvg, importPdf nebo importText
+@msg `{name}` can't be deleted `{name}` nelze odstranit
+@msg `{name}` can't be duplicated `{name}` nelze duplikovat
+@msg `{name}` can't be edited `{name}` nelze upravit
+@msg `{name}` can't be edited this way `{name}` nelze tímto způsobem upravit
+@msg `{name}` can't be renamed `{name}` nelze přejmenovat
+@msg `{name}` can't name a folder `{name}` nemůže být názvem složky
+@msg `{name}` has no art to place `{name}` nemá žádnou kresbu k umístění
+@msg `{name}` has no text to place `{name}` nemá žádný text k umístění
+@msg `{name}` in the Word document is not UTF-8 or UTF-16 text `{name}` v dokumentu Word není text v UTF-8 ani UTF-16
+@msg `{name}` in the Word document is not well-formed XML: {e} `{name}` v dokumentu Word není správně utvořené XML: {e}
+@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep `{name}` v dokumentu Word vnořuje prvky hlouběji než {MAX_NESTING} úrovní
+@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}` je vestavěné přednastavení jen pro čtení: uložte ho pod jiným názvem
+@msg `{name}` is a built-in preset: save under another name `{name}` je vestavěné přednastavení: uložte ho pod jiným názvem
+@msg `{name}` is a built-in profile and stays `{name}` je vestavěný profil a zůstává
+@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` je samostatný soubor, který lze najít jen vedle cesty souboru DCS
+@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}` je tón barvy `{base}`: upravte `{base}` (položce vzorníku s tónem lze dát jen nový název)
+@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}` je pracovní barevný prostor: nejprve zvolte jiný pomocí edit.colorSettings
+@msg `{name}` is built in and stays `{name}` je vestavěné a zůstává
+@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}` přijímá nátěr svého druhu (barvu, přechod nebo vzorek)
+@msg `{name}`: {_1}: {e} `{name}`: {_1}: {e}
+@msg `{n}` can't be deleted `{n}` nelze odstranit
+@msg `{n}` can't be merged `{n}` nelze sloučit
+@msg `{n}` can't be moved `{n}` nelze přesunout
+@msg `{n}` is not a colour swatch `{n}` není barevná položka vzorníku
+@msg `{n}` isn't a solid colour `{n}` není plná barva
+@msg `{n}`: colour groups hold solid colours only `{n}`: skupiny barev obsahují jen plné barvy
+@msg `{other}` doesn't trace an image `{other}` nevektorizuje obraz
+@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}`: odkazy kroku by zkopírovaly více než {_1} MB hodnot
+@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}` není značka osy: značka má čtyři znaky, například "wght"
+@msg `{to}` is empty or already used `{to}` je prázdné nebo už použité
+@msg `{u}` is no Unicode character `{u}` není znak Unicode
+@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` je delší než {MAX_TEXT} bajtů
+@msg `{what}` must be text `{what}` musí být text
+@msg `{word}` isn't in the custom dictionary `{word}` není ve vlastním slovníku
+@msg a CMap has too many mappings CMap má příliš mnoho mapování
+@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard výkres DXF má jeden počátek: předejte useArtboards: true pro jeden výkres na každou pracovní plochu
+@msg a PDF presets file with over {_1} presets soubor přednastavení PDF s více než {_1} přednastaveními
+@msg a \u escape needs four hexadecimal digits sekvence \u vyžaduje čtyři šestnáctkové číslice
+@msg a batch preview cannot switch documents náhled dávky nemůže přepínat dokumenty
+@msg a brush named `{nn}` exists štětec s názvem `{nn}` už existuje
+@msg a comma in an XPath expression must separate function arguments čárka ve výrazu XPath musí oddělovat argumenty funkce
+@msg a container of the listed objects is gone kontejner uvedených objektů už neexistuje
+@msg a content stream can't be read proud obsahu nelze přečíst
+@msg a content stream isn't a stream proud obsahu není proud
+@msg a design holds at most {_1} objects návrh obsahuje nejvýše {_1} objektů
+@msg a design needs a name návrh potřebuje název
+@msg a dictionary grew too large slovník se příliš zvětšil
+@msg a document holds at most {_1} SVG filters dokument obsahuje nejvýše {_1} filtrů SVG
+@msg a document keeps at most {_1} graph designs dokument uchovává nejvýše {_1} návrhů grafu
+@msg a document keeps at most {_1} saved selections dokument uchovává nejvýše {_1} uložených výběrů
+@msg a document keeps at most {_1} views dokument uchovává nejvýše {_1} zobrazení
+@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files dokument odkazuje na nejvýše {MAX_SVG_SCRIPTS} souborů JavaScript
+@msg a document with unsaved changes has no recovery copy dokument s neuloženými změnami nemá záložní kopii
+@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes řád filtru feConvolveMatrix je {x}×{y}, což přeteče při násobení velikosti jádra, které provádí usvg
+@msg a font's data is too large data písma jsou příliš velká
+@msg a font's subroutines are too large podprogramy písma jsou příliš velké
+@msg a function declaration without a name deklarace funkce bez názvu
+@msg a gradient preset is already called {n} některé přednastavení přechodu se už jmenuje {n}
+@msg a graph has at most {_1} series, so it transposes with at most that many categories graf má nejvýše {_1} řad, takže ho lze transponovat nejvýše s tolika kategoriemi
+@msg a host value has too many entries hodnota hostitele má příliš mnoho položek
+@msg a host value is nested too deep hodnota hostitele je vnořena příliš hluboko
+@msg a layer can't be merged into its own sublayer vrstvu nelze sloučit do její vlastní podvrstvy
+@msg a layer's form box isn't an array rámeček formuláře vrstvy není pole
+@msg a layer's form can't be read formulář vrstvy nelze přečíst
+@msg a layer's form has no box formulář vrstvy nemá rámeček
+@msg a layer's form has no dictionary formulář vrstvy nemá slovník
+@msg a legend draws at most {MAX_CHIPS} swatches legenda vykreslí nejvýše {MAX_CHIPS} barevných políček
+@msg a name names one asset: give multiple: false, or one object název určuje jeden prvek: zadejte multiple: false, nebo jeden objekt
+@msg a namespace declaration uses a reserved prefix or URI deklarace jmenného prostoru používá vyhrazenou předponu nebo URI
+@msg a namespace qualifier is undefined kvalifikátor jmenného prostoru není definován
+@msg a namespace with an empty URI cannot have a prefix jmenný prostor s prázdným URI nemůže mít předponu
+@msg a number has no digits číslo nemá žádné číslice
+@msg a number's exponent has no digits exponent čísla nemá žádné číslice
+@msg a number's fraction has no digits zlomková část čísla nemá žádné číslice
+@msg a page can't be read stránku nelze přečíst
+@msg a page object is used by something else objekt stránky používá něco jiného
+@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) miniatura stránky musí být z obrazových bodů RGB, 1 až {THUMBNAIL_SIZE} na stranu (zadáno {w} × {h})
+@msg a pattern layer doesn't end what it starts vrstva vzorku neukončuje to, co začíná
+@msg a pattern layer's colors hold something else barvy vrstvy vzorku obsahují něco jiného
+@msg a pattern named `{n}` exists vzorek s názvem `{n}` už existuje
+@msg a plug-in with id {_1} is already installed zásuvný modul s ID {_1} je už nainstalován
+@msg a preset named `{n}` exists přednastavení s názvem `{n}` už existuje
+@msg a print presets file with over {_1} presets soubor přednastavení tisku s více než {_1} přednastaveními
+@msg a profile is already called {n} některý profil se už jmenuje {n}
+@msg a property descriptor cannot have both a value or writable and a get or set popisovač vlastnosti nemůže mít zároveň value nebo writable a get nebo set
+@msg a property descriptor must be an object popisovač vlastnosti musí být objekt
+@msg a property's {name} must be a function or undefined {name} vlastnosti musí být funkce nebo undefined
+@msg a row can't go into itself řádek nelze vložit do sebe samého
+@msg a series goes on the left or the right value axis řada patří na levou nebo pravou osu hodnot
+@msg a start must be an anchor index ≥ 0 or null začátek musí být index kotevního bodu ≥ 0 nebo null
+@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied některý proud v něm by se dekódoval na více než 256 MB, nebo nelze použít jeho parametry dekódování
+@msg a string holds a control character řetězec obsahuje řídicí znak
+@msg a string holds an escape sequence JSON does not have řetězec obsahuje řídicí sekvenci, kterou JSON nezná
+@msg a string is not closed řetězec není uzavřen
+@msg a style named `{name}` already exists styl s názvem `{name}` už existuje
+@msg a style named `{to}` already exists styl s názvem `{to}` už existuje
+@msg a swatch needs a colour, gradient or pattern položka vzorníku potřebuje barvu, přechod nebo vzorek
+@msg a symbol named `{n}` already exists symbol s názvem `{n}` už existuje
+@msg a theme has 1 to {THEME_SIZE} colours motiv má 1 až {THEME_SIZE} barev
+@msg a theme is already called {name} (replace: "{name}" overwrites it) některý motiv se už jmenuje {name} (replace: "{name}" ho přepíše)
+@msg a toolbar name has at most {MAX_NAME} characters název panelu nástrojů má nejvýše {MAX_NAME} znaků
+@msg a view named `{_1}` exists zobrazení s názvem `{_1}` už existuje
+@msg a view named `{name}` exists zobrazení s názvem `{name}` už existuje
+@msg a workspace keeps at most {MAX} toolbars pracovní prostor uchovává nejvýše {MAX} panelů nástrojů
+@msg a {_1} series can't go in a {_2} graph řadu typu {_1} nelze vložit do grafu typu {_2}
+@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v}: "pdf" (kompatibilní s PDF, výchozí) nebo "7" (starší .ai, verze 7)
+@msg already editing an opacity mask maska krytí se už upravuje
+@msg already editing another object's opacity mask už se upravuje maska krytí jiného objektu
+@msg already in pattern editing mode režim úprav vzorku je už aktivní
+@msg an SVG filter is named by its id: give `xml` with `name` filtr SVG je pojmenován svým ID: zadejte `xml` spolu s `name`
+@msg an XML element has two attributes with the same name prvek XML má dva atributy se stejným názvem
+@msg an XML value cannot be inserted into itself or its descendants hodnotu XML nelze vložit do sebe samé ani do jejích potomků
+@msg an XML value's children cannot be assigned by position potomky hodnoty XML nelze přiřazovat podle pozice
+@msg an XML value's children cannot be deleted by position potomky hodnoty XML nelze odstraňovat podle pozice
+@msg an XMLList converts to XML only when it has one item XMLList se převede na XML, jen když má jednu položku
+@msg an XPath node test is missing chybí test uzlu XPath
+@msg an XPath operand is missing chybí operand XPath
+@msg an XPath operator is missing chybí operátor XPath
+@msg an XPath predicate or step cannot follow here predikát ani krok XPath zde nemůže následovat
+@msg an XPath predicate or step follows a value that is not a node-set predikát nebo krok XPath následuje za hodnotou, která není množina uzlů
+@msg an XPath step is missing chybí krok XPath
+@msg an artboard is listed twice některá pracovní plocha je uvedena dvakrát
+@msg an empty artboard has no width or height to scale to prázdná pracovní plocha nemá šířku ani výšku, na kterou by šlo měnit velikost
+@msg an image couldn't be decoded and is missing from the picture obraz nešlo dekódovat a v obrázku chybí
+@msg an image is too large for DXF and was left out obraz je pro DXF příliš velký a byl vynechán
+@msg an image of {_1} MB, at most {_2} MB obraz o velikosti {_1} MB, nejvýše {_2} MB
+@msg an image of {declared_w} × {declared_h} pixels is too large to draw obraz o rozměrech {declared_w} × {declared_h} obrazových bodů je příliš velký na vykreslení
+@msg an image of {width} × {height} pixels is too large to draw obraz o rozměrech {width} × {height} obrazových bodů je příliš velký na vykreslení
+@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) obraz o rozměrech {w} × {h} obrazových bodů je příliš velký na převod (nejvýše {_1} obrazových bodů)
+@msg an index must be a whole number index musí být celé číslo
+@msg an inline graphic has no glyphs vložená grafika v textu nemá žádné glyfy
+@msg an invisible text form can't be read formulář neviditelného textu nelze přečíst
+@msg an object is missing chybí objekt
+@msg an object's header is missing chybí záhlaví objektu
+@msg an overprinting form can't be read formulář s přetiskem nelze přečíst
+@msg an unread object has no readable bounds nepřečtený objekt nemá čitelné hranice
+@msg an unread object holds a frame of threaded type nepřečtený objekt obsahuje rámec zřetězeného textu
+@msg an unread object is missing from its layer nepřečtený objekt chybí ve své vrstvě
+@msg anchor `smooth` must be true or false `smooth` kotevního bodu musí být true nebo false
+@msg anchor `{key}` must be [x, y] `{key}` kotevního bodu musí být [x, y]
+@msg anchor `{key}` must be a number `{key}` kotevního bodu musí být číslo
+@msg angle and roundness must be finite úhel a zaoblení musí být konečné
+@msg angles must be a list as long as pins (degrees or null) angles musí být seznam stejně dlouhý jako pins (stupně nebo null)
+@msg appearance must be flat|center|edge appearance musí být flat|center|edge
+@msg apply is "fill" or "stroke", not `{other}` apply je "fill" nebo "stroke", ne `{other}`
+@msg apply was called on a value that is not a function apply byla zavolána na hodnotě, která není funkce
+@msg area must be a closed polygon of 3 to 100000 [x, y] points area musí být uzavřený mnohoúhelník o 3 až 100000 bodech [x, y]
+@msg area type in a thread cannot use Dynamic Text: {frames} text v oblasti ve zřetězeném textu nemůže používat dynamický text: {frames}
+@msg area type needs a path that encloses an area text v oblasti vyžaduje cestu, která uzavírá plochu
+@msg area type {_1} is named twice text v oblasti {_1} je uveden dvakrát
+@msg array literal too long literál pole je příliš dlouhý
+@msg arrowAlign must be extend|tip, got {o} arrowAlign musí být extend|tip, zadáno {o}
+@msg arrowScale must be [start, end] arrowScale musí být [start, end]
+@msg artboard is a 0-based index artboard je index počítaný od 0
+@msg artboard must be a 0-based index artboard musí být index počítaný od 0
+@msg artboard {i} doesn't print pracovní plocha {i} se netiskne
+@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout musí být {layout?, columns?, spacing?, rightToLeft?}
+@msg artboardLayout.columns must be 1 or more artboardLayout.columns musí být 1 nebo více
+@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout musí být gridByRow, gridByColumn, row nebo column
+@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft musí být true nebo false
+@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing musí být 0–{MAX_SPACING} pt
+@msg artboards must be a non-empty list artboards musí být neprázdný seznam
+@msg artboards must be a number artboards musí být číslo
+@msg asset `{_1}` has no art to export prvek `{_1}` nemá žádnou kresbu k exportu
+@msg assignment to a constant binding přiřazení do konstantní vazby
+@msg at least one row that isn't excluded needs a weight above 0 alespoň jeden nevyloučený řádek potřebuje váhu větší než 0
+@msg at most {MAX_KEYWORDS} keywords nejvýše {MAX_KEYWORDS} klíčových slov
+@msg at most {MAX_MATCHES} matches are kept uchovává se nejvýše {MAX_MATCHES} shod
+@msg at most {MAX_POINTS} points nejvýše {MAX_POINTS} bodů
+@msg at most {MAX_QUEUE} files nejvýše {MAX_QUEUE} souborů
+@msg at must be [x, y] at musí být [x, y]
+@msg at/rect lie outside the canvas at/rect leží mimo plátno
+@msg attribute must be size|leading|tracking|kerning|baselineShift attribute musí být size|leading|tracking|kerning|baselineShift
+@msg attrs must be an object attrs musí být objekt
+@msg automation path rejected: the folder's name isn't valid text cesta automatizace byla odmítnuta: název složky není platný text
+@msg automation path rejected: {_1} isn't valid text cesta automatizace byla odmítnuta: {_1} není platný text
+@msg automation path rejected: {why}: {path} cesta automatizace byla odmítnuta: {why}: {path}
+@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis musí být "vertical", "horizontal" nebo úhel ve stupních, ne `{given}`
+@msg backgroundContents must be transparent or white backgroundContents musí být transparent nebo white
+@msg bad AES key chybný klíč AES
+@msg bad `runs`: {e} chybné `runs`: {e}
+@msg bad arrowScale chybné arrowScale
+@msg bad base64 chybný base64
+@msg bad color {c} chybná barva {c}
+@msg bad colour chybná barva
+@msg bad colour in `colors` chybná barva v `colors`
+@msg bad colour {v} chybná barva {v}
+@msg bad colour {v} in groupColors chybná barva {v} v groupColors
+@msg bad cross-reference entry chybná položka křížových odkazů
+@msg bad cross-reference table chybná tabulka křížových odkazů
+@msg bad current colour {v} chybná aktuální barva {v}
+@msg bad dataBase64 chybné dataBase64
+@msg bad edit chybná úprava
+@msg bad fill {v} chybná výplň {v}
+@msg bad fit: {e} chybné přizpůsobení: {e}
+@msg bad model {other} (keep, new or document) chybný model {other} (keep, new nebo document)
+@msg bad new colour {v} chybná nová barva {v}
+@msg bad object chybný objekt
+@msg bad object number chybné číslo objektu
+@msg bad offset chybný posun
+@msg bad path data: {e} chybná data cesty: {e}
+@msg bad point {v}: expected [x, y] chybný bod {v}: očekáváno [x, y]
+@msg bad point {v}: expected [x, y] or [x, y, pressure] chybný bod {v}: očekáváno [x, y] nebo [x, y, pressure]
+@msg bad reference chybný odkaz
+@msg bad trailer size chybná velikost v zakončení
+@msg bad {k} colour chybná barva {k}
+@msg batches cannot nest dávky nelze vnořovat
+@msg bind something to a variable first nejprve něco svažte s proměnnou
+@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed musí být 0–{MAX_BLEED} pt: číslo, [top, bottom, left, right] nebo {{top, bottom, left, right}}
+@msg blend must be a mode name blend musí být název režimu
+@msg both sides of an XPath | must select nodes obě strany operátoru | v XPath musí vybírat uzly
+@msg brush must be a name or null brush musí být název nebo null
+@msg by must be a number by musí být číslo
+@msg can't add page thumbnails: {why} miniatury stránek nelze přidat: {why}
+@msg can't add the editing data for other apps: {why} data pro úpravy v jiných aplikacích nelze přidat: {why}
+@msg can't edit the written PDF: {why} zapsané PDF nelze upravit: {why}
+@msg can't encrypt the PDF: {why} PDF nelze zašifrovat: {why}
+@msg can't linearise the PDF: {why} PDF nelze linearizovat: {why}
+@msg can't open `{_1}`: {_2} `{_1}` nelze otevřít: {_2}
+@msg can't open `{_1}`: {e} `{_1}` nelze otevřít: {e}
+@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) `{name}` nelze otevřít: nejde o formát, který VectorCraft čte (viz document.formats)
+@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) `{name}` nelze umístit: nejde o formát, který VectorCraft čte (viz document.formats)
+@msg can't read back the written PDF: {e} zapsané PDF nelze znovu načíst: {e}
+@msg can't read this Word document: {why} tento dokument Word nelze přečíst: {why}
+@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp obrázky .{e} nelze zapisovat: použijte .png, .jpg, .gif, .webp, .tif nebo .bmp
+@msg can't write PDF layers, overprint or invisible text: {why} vrstvy PDF, přetisk ani neviditelný text nelze zapsat: {why}
+@msg can't write the file's copyright: {why} údaj o autorských právech souboru nelze zapsat: {why}
+@msg can't write the file's modification date: {why} datum změny souboru nelze zapsat: {why}
+@msg can't write the pages' user units: {why} uživatelské jednotky stránek nelze zapsat: {why}
+@msg canceled zrušeno
+@msg cannot convert the object to a primitive value objekt nelze převést na primitivní hodnotu
+@msg cannot convert undefined or null to an object undefined ani null nelze převést na objekt
+@msg cannot distort a zero-size or non-finite bounding box ohraničovací rámeček s nulovou nebo nekonečnou velikostí nelze deformovat
+@msg cannot move an object into itself objekt nelze přesunout do sebe samého
+@msg case must be upper|lower|title|sentence case musí být upper|lower|title|sentence
+@msg category `{c}`: one of {_1} kategorie `{c}`: jedna z hodnot {_1}
+@msg center must be [x, y] center musí být [x, y]
+@msg character dimensions cannot be represented with the source text transform rozměry znaků nelze vyjádřit transformací zdrojového textu
+@msg character dimensions cannot be represented with the target text transform rozměry znaků nelze vyjádřit transformací cílového textu
+@msg choose at least one artboard zvolte alespoň jednu pracovní plochu
+@msg choose at least one status zvolte alespoň jeden stav
+@msg clip must be true or false clip musí být true nebo false
+@msg clippingMask must be true or false clippingMask musí být true nebo false
+@msg closed must be a boolean closed musí být logická hodnota
+@msg collapsed must be true or false collapsed musí být true nebo false
+@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile: žádný profil RGB ani CMYK se nejmenuje „{profile}“
+@msg colorMode must be rgb or cmyk, not `{m}` colorMode musí být rgb nebo cmyk, ne `{m}`
+@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel musí být {own} (režim dokumentu), grayscale nebo bitmap
+@msg colour groups can't go into colour groups skupiny barev nelze vkládat do skupin barev
+@msg colour, mode and spot apply to solid-colour swatches only colour, mode a spot platí jen pro položky vzorníku s plnou barvou
+@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read sloupec {column}: `{cell}` je sloupec `%` pro soubory dat grafu, které se nečtou
+@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable sloupec {column}: `{cell}` uvádí `{name}`, což je proměnná typu {_1}
+@msg column {column}: `{cell}` names no variable sloupec {column}: `{cell}` neuvádí žádnou proměnnou
+@msg column {column}: the variable name is longer than {MAX_NAME} characters sloupec {column}: název proměnné je delší než {MAX_NAME} znaků
+@msg column: true floats the whole column and takes no onto, above or below column: true uvolní celý sloupec jako plovoucí a nepřijímá onto, above ani below
+@msg command must be a shape.* command command musí být příkaz shape.*
+@msg commands[{i}]: missing `command` commands[{i}]: chybí `command`
+@msg composer must be "singleLine" or "everyLine" composer musí být "singleLine" nebo "everyLine"
+@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes složené tvary přijímají cesty, složené cesty, skupiny, složené tvary, text, prolnutí a obálky
+@msg compound {_1} must contain exactly one open path to join složený objekt {_1} musí ke spojení obsahovat právě jednu otevřenou cestu
+@msg connect needs a path other than the one extended connect vyžaduje jinou cestu než tu prodlužovanou
+@msg connect needs id connect vyžaduje id
+@msg connect path has no open end cesta v connect nemá žádný otevřený konec
+@msg continuation must be an array of path chunks continuation musí být pole úseků cesty
+@msg continuation requires points continuation vyžaduje points
+@msg copy needs path: the file to copy into the document's folder copy vyžaduje path: soubor, který se má zkopírovat do složky dokumentu
+@msg copy takes images only: relink placed documents where their files are copy přijímá jen obrázky: umístěné dokumenty znovu propojte tam, kde jsou jejich soubory
+@msg copying a file isn't available on the web kopírování souboru není na webu dostupné
+@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus musí být unknown, copyrighted nebo publicDomain
+@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl musí být adresa URL (bez mezer, nejvýše 2048 znaků)
+@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners musí být 4 body [x, y] (TL, TR, BR, BL)
+@msg counterPer `{c}`: item or file counterPer `{c}`: item nebo file
+@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}`: rámečky art, trim, bleed a media mají jen stránky PDF; použijte "crop" nebo "bounding"
+@msg crop rectangle doesn't overlap the image ořezový obdélník se nepřekrývá s obrazem
+@msg cross-reference and object streams aren't supported proudy křížových odkazů a proudy objektů nejsou podporovány
+@msg damaged compressed data: {e} poškozená komprimovaná data: {e}
+@msg dash lengths must be numbers, got {v} délky čárek musí být čísla, zadáno {v}
+@msg dash must be a list of lengths or null, got {v} dash musí být seznam délek nebo null, zadáno {v}
+@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) klíč dat `{k}`: písmena, číslice, -, _ a . (ne name ani vc-…)
+@msg data must be an object of key: value, not {v} data musí být objekt ve tvaru klíč: hodnota, ne {v}
+@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare sada dat `{name}` obsahuje hodnotu pro `{var}`, kterou knihovna nedeklaruje
+@msg data set `{name}` holds two values for `{var}` sada dat `{name}` obsahuje dvě hodnoty pro `{var}`
+@msg data set `{name}` is declared twice sada dat `{name}` je deklarována dvakrát
+@msg data takes at most {MAX_DATA} keys at once data přijímá najednou nejvýše {MAX_DATA} klíčů
+@msg dataset `{name}` already exists sada dat `{name}` už existuje
+@msg dataset `{to}` already exists sada dat `{to}` už existuje
+@msg date `{d}`: YYYY-MM-DD datum `{d}`: YYYY-MM-DD
+@msg decoded data is too large dekódovaná data jsou příliš velká
+@msg define a variable first nejprve definujte proměnnou
+@msg detect must be true or false detect musí být true nebo false
+@msg direct-select anchor points of a path vyberte kotevní body cesty přímým výběrem
+@msg distance must be > 0 distance musí být > 0
+@msg dx and dy give the arrow's direction dx a dy udávají směr šipky
+@msg dx and dy must be numbers dx a dy musí být čísla
+@msg dxf fitTo must be [width, height], positive dxf fitTo musí být [width, height], kladné
+@msg dxf must be an object of DXF import options dxf musí být objekt s volbami importu DXF
+@msg dxf scale must be a positive number up to {_1}, not {s} dxf scale musí být kladné číslo nejvýše {_1}, ne {s}
+@msg dxf unit `{u}`: a unit such as mm, cm, in or pt dxf unit `{u}`: jednotka jako mm, cm, in nebo pt
+@msg dxf: {e} dxf: {e}
+@msg each format is an object {format, scale?, suffix?} každý formát je objekt {format, scale?, suffix?}
+@msg each item needs an id každá položka potřebuje id
+@msg each match needs `kind` každá shoda potřebuje `kind`
+@msg each match needs a `name` každá shoda potřebuje `name`
+@msg each of `fonts` is {family, style?}; 1 to 1000 of them každá položka `fonts` je {family, style?}; 1 až 1000 položek
+@msg each of `items` is {kind, item} každá položka `items` je {kind, item}
+@msg each of `items` is {{id, corners?}} with an object id, not {it} každá položka `items` je {{id, corners?}} s ID objektu, ne {it}
+@msg each path chunk must contain 1 to 4096 points každý úsek cesty musí obsahovat 1 až 4096 bodů
+@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) každá volba je {{at: [x, y]}} nebo {{rect: [x0, y0, x1, y1]}} (zadáno {v})
+@msg each row needs `from` každý řádek potřebuje `from`
+@msg each stop needs a position každá zarážka potřebuje pozici
+@msg each subpath needs an anchors array každá dílčí cesta potřebuje pole anchors
+@msg each weight must be a number of 0 or more (got {v}) každá váha musí být číslo 0 nebo větší (zadáno {v})
+@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType musí být easeIn, easeOut, easeInOut nebo easeOutIn
+@msg editingData must be true or false editingData musí být true nebo false
+@msg editingData must be true, false or "always" editingData musí být true, false nebo "always"
+@msg edits overlap úpravy se překrývají
+@msg effects of objects whose appearances differ can't be moved together efekty objektů s odlišným vzhledem nelze přesouvat společně
+@msg empty header in column {_1} prázdné záhlaví ve sloupci {_1}
+@msg encoding `{_1}`: utf8 or utf16 kódování `{_1}`: utf8 nebo utf16
+@msg ends must be two of "first" and "last" ends musí být dvě z hodnot "first" a "last"
+@msg every numbered name for the result is taken každý číslovaný název pro výsledek je už obsazen
+@msg expanded SVG styles exceed the import size limit rozvinuté styly SVG překračují limit velikosti importu
+@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes soubor přednastavení exportu je větší než {MAX_PRESET_FILE_BYTES} bajtů
+@msg exportText must be editable or appearance exportText musí být editable nebo appearance
+@msg exportType must be movieClip or graphic, not `{s}` exportType musí být movieClip nebo graphic, ne `{s}`
+@msg extend needs id extend vyžaduje id
+@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges uvádí nejvýše {MAX_FEATURE_CHANGES} značek
+@msg featureChanges must be a list of tags featureChanges musí být seznam značek
+@msg features must be a list of tags features musí být seznam značek
+@msg fidelity must be 0..100 fidelity musí být 0..100
+@msg files must be {name, dataBase64} files musí být {name, dataBase64}
+@msg finally table {t} out of range tabulka finally {t} je mimo rozsah
+@msg fit must be none|autoHeight|shrinkText|block, got {id} fit musí být none|autoHeight|shrinkText|block, zadáno {id}
+@msg flags cannot be given with a RegExp object příznaky nelze zadat spolu s objektem RegExp
+@msg flattener: {e} sloučení průhlednosti: {e}
+@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}`: high, medium, low nebo uložené přednastavení (viz flattener.presets.list)
+@msg flipProfile must be along|across, got {o} flipProfile musí být along|across, zadáno {o}
+@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}`: none, rows, format, item nebo itemFormat
+@msg folders deeper than {MAX_BATCH_DEPTH} levels složky hlubší než {MAX_BATCH_DEPTH} úrovní
+@msg folders deeper than {MAX_DEPTH} levels složky hlubší než {MAX_DEPTH} úrovní
+@msg font family `{family}` is not available rodina písma `{family}` není dostupná
+@msg font family `{tf}` is not available rodina písma `{tf}` není dostupná
+@msg format `{f}`: pdf or postscript formát `{f}`: pdf nebo postscript
+@msg format `{f}`: text or json formát `{f}`: text nebo json
+@msg format is `{format}` formát je `{format}`
+@msg formats must be an array of rows, not {v} formats musí být pole řádků, ne {v}
+@msg frequency must be between 1 and 300 lines per inch frequency musí být mezi 1 a 300 linkami na palec
+@msg from and to must be finite from a to musí být konečné
+@msg from must be [x0, y0, x1, y1] from musí být [x0, y0, x1, y1]
+@msg function declaration index out of range index deklarace funkce je mimo rozsah
+@msg give `before` or `after`, not both zadejte `before` nebo `after`, ne obojí
+@msg give `char`, `text`, `code` or `unicode` zadejte `char`, `text`, `code` nebo `unicode`
+@msg give `colors`, or `color` and `rule` zadejte `colors`, nebo `color` a `rule`
+@msg give `count` or `spacing`, not both zadejte `count` nebo `spacing`, ne obojí
+@msg give `event`, such as onclick zadejte `event`, například onclick
+@msg give `file`, the JavaScript file's path or URL zadejte `file`, cestu nebo adresu URL souboru JavaScript
+@msg give `fill` and/or `stroke` zadejte `fill` a/nebo `stroke`
+@msg give `fill`, `stroke` or both zadejte `fill`, `stroke` nebo obojí
+@msg give `folder` zadejte `folder`
+@msg give `fonts`, or open a document zadejte `fonts`, nebo otevřete dokument
+@msg give `from` or `indices`, not both zadejte `from` nebo `indices`, ne obojí
+@msg give `id` (no text is being edited) zadejte `id` (žádný text se právě neupravuje)
+@msg give `id` and `anchors`, or `items`, not both zadejte `id` a `anchors`, nebo `items`, ne obojí
+@msg give `id` or `guide`, not both zadejte `id` nebo `guide`, ne obojí
+@msg give `length` or `to` zadejte `length` nebo `to`
+@msg give `matches`: [{kind, name, target}] zadejte `matches`: [{kind, name, target}]
+@msg give `name` (or edit a pattern) zadejte `name` (nebo upravujte vzorek)
+@msg give `name` or `names` zadejte `name` nebo `names`
+@msg give `name`, an SVG filter's name zadejte `name`, název filtru SVG
+@msg give `path` (a .wasm file) or `dataBase64` (the module) zadejte `path` (soubor .wasm) nebo `dataBase64` (modul)
+@msg give `path`, `data` or `dataBase64` zadejte `path`, `data` nebo `dataBase64`
+@msg give `path`, `data`, `dataBase64` or `csv` zadejte `path`, `data`, `dataBase64` nebo `csv`
+@msg give `path`, `xml` or `dataBase64` zadejte `path`, `xml` nebo `dataBase64`
+@msg give `path`, an SVG file, or `svg`, SVG text zadejte `path`, soubor SVG, nebo `svg`, text SVG
+@msg give `script`, the JavaScript the event runs ("" removes the event) zadejte `script`, JavaScript, který událost spustí ("" událost odebere)
+@msg give `text` (the reading) or `remove` zadejte `text` (čtení) nebo `remove`
+@msg give `x` and `y` (and a `tolerance` of 0 or more) zadejte `x` a `y` (a `tolerance` 0 nebo více)
+@msg give `xml`, one SVG element zadejte `xml`, jeden prvek SVG
+@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments zadejte režim nebo úpravy r/g/b, c/m/y/k, gray či tint
+@msg give a name, or paint the active proxy with a saved preset zadejte název, nebo aktivní výplň či tah nastavte uloženým přednastavením
+@msg give a name, or select a stroke with a saved profile zadejte název, nebo vyberte tah s uloženým profilem
+@msg give a pattern name or select an object painted with a pattern zadejte název vzorku nebo vyberte objekt vybarvený vzorkem
+@msg give at least one of clip, invert, disabled, linked zadejte alespoň jedno z clip, invert, disabled, linked
+@msg give color, none, swatch or gradient zadejte color, none, swatch nebo gradient
+@msg give dataBase64 zadejte dataBase64
+@msg give either at (a click) or area (a path drawn around the overlaps) zadejte buď at (kliknutí), nebo area (cestu nakreslenou kolem překryvů)
+@msg give exactly one of `at`, `rect` and `points` zadejte právě jedno z `at`, `rect` a `points`
+@msg give exactly one of `path`, `data` and `dataBase64` zadejte právě jedno z `path`, `data` a `dataBase64`
+@msg give formats or a preset, not both zadejte formats nebo přednastavení, ne obojí
+@msg give index or indexes, not both zadejte index nebo indexes, ne obojí
+@msg give newName or delete zadejte newName nebo delete
+@msg give offset or by zadejte offset nebo by
+@msg give one element zadejte jeden prvek
+@msg give one of `to`, `rect` or `at` zadejte jedno z `to`, `rect` nebo `at`
+@msg give one of assets, layers, selection or fullDocument zadejte jedno z assets, layers, selection nebo fullDocument
+@msg give only one of onto, above, below, icon and a column place zadejte jen jedno z onto, above, below, icon a umístění ve sloupci
+@msg give opacity or blend zadejte opacity nebo blend
+@msg give origin: [x, y], or reset: true zadejte origin: [x, y], nebo reset: true
+@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note zadejte overprintFill, overprintStroke, showCenter, imageMap, url nebo note
+@msg give path (a file) or folder zadejte path (soubor) nebo folder
+@msg give path, or name and dataBase64 zadejte path, nebo name a dataBase64
+@msg give paths or files zadejte paths nebo files
+@msg give rows, rowHeight, columns or columnWidth zadejte rows, rowHeight, columns nebo columnWidth
+@msg give svg, or dataBase64 of an SVG or SVGZ file zadejte svg, nebo dataBase64 souboru SVG či SVGZ
+@msg give text zadejte text
+@msg give the asset a name pojmenujte prvek
+@msg give the preset a `name` zadejte pro přednastavení `name`
+@msg give the preset a name pojmenujte přednastavení
+@msg give the preset a name (1–64 characters) pojmenujte přednastavení (1–64 znaků)
+@msg give the settings to change, e.g. {"formats"}]} zadejte nastavení, která se mají změnit, např. {"formats"}]}
+@msg give the style a name pojmenujte styl
+@msg give two or more style `names` zadejte v `names` dva nebo více stylů
+@msg give two or more swatches zadejte dvě nebo více položek vzorníku
+@msg give x, y, or a handle's length or angle zadejte x, y, nebo délku či úhel úchytu
+@msg give {key, value} or {values} zadejte {key, value} nebo {values}
+@msg global must be true or false global musí být true nebo false
+@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) glyf {_1} se pro {ch} se stylem znaku nevykresluje (například Verzálky, Kapitálky nebo číselná soustava)
+@msg glyph {g} is not an alternate of {ch} in {_1} {_2} glyf {g} není alternativou znaku {ch} v písmu {_1} {_2}
+@msg gradient must be "objects" or "mesh" gradient musí být "objects" nebo "mesh"
+@msg grays must be "profile" or "black", not `{g}` grays musí být "profile" nebo "black", ne `{g}`
+@msg gridColors must be a preset name or two colours gridColors musí být název přednastavení nebo dvě barvy
+@msg gridSize must be small, medium or large gridSize musí být small, medium nebo large
+@msg guide {g} is not selected vodítko {g} není vybráno
+@msg handle must be 0..3 handle musí být 0..3
+@msg handler at {_1} expects {_2} values but its range starts with {d} obslužná rutina na pozici {_1} očekává {_2} hodnot, ale její rozsah začíná s {d}
+@msg handler depth too large hloubka obslužné rutiny je příliš velká
+@msg handler range {_1}..{_2} out of range rozsah obslužné rutiny {_1}..{_2} je mimo rozsah
+@msg hiddenLayers must be true or false hiddenLayers musí být true nebo false
+@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v}: "layer" (výchozí), "omit" nebo "visible"
+@msg highlight a layer to merge into zvýrazněte vrstvu, do které se má sloučit
+@msg highlight two or more rows in one layer or group zvýrazněte dva nebo více řádků v jedné vrstvě nebo skupině
+@msg highlight two or more rows to merge zvýrazněte dva nebo více řádků ke sloučení
+@msg hvcC hvcC
+@msg icon docks a panel in the icon column: use window.panel.dock icon ukotví panel ve sloupci ikon: použijte window.panel.dock
+@msg id must be a dimension id id musí být ID kóty
+@msg image has no bounds obraz nemá hranice
+@msg image {_1} is linked already obraz {_1} je už propojený
+@msg imageMap `{m}`: none, client or server imageMap `{m}`: none, client nebo server
+@msg images in a spot ink whose samples couldn't be read were imported in RGB obrazy v přímé barvě, jejichž vzorky nešlo přečíst, byly importovány v RGB
+@msg images past the most pixels one file may hold were left out obrazy nad největší počet obrazových bodů, který smí jeden soubor obsahovat, byly vynechány
+@msg images too large to read were left out obrazy příliš velké na přečtení byly vynechány
+@msg index out of range index je mimo rozsah
+@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes smí obsahovat nejvýše {MAX_GUIDE_INDEXES} položek
+@msg indexes must be an array of guide indexes indexes musí být pole indexů vodítek
+@msg instances must be 1..1000 instances musí být 1..1000
+@msg instruction out of range instrukce je mimo rozsah
+@msg instruction {pc} ({op}) pops below the stack instrukce {pc} ({op}) odebírá pod dno zásobníku
+@msg instruction {pc} reached with stack depths {seen} and {d} instrukce {pc} byla dosažena s hloubkami zásobníku {seen} a {d}
+@msg invalid `color` neplatné `color`
+@msg invalid anchor index {v}: expected a non-negative integer neplatný index kotevního bodu {v}: očekáváno nezáporné celé číslo
+@msg invalid anchor reference {v}: expected [subpath, anchor] neplatný odkaz na kotevní bod {v}: očekáváno [dílčí cesta, kotevní bod]
+@msg invalid angle neplatný úhel
+@msg invalid array length neplatná délka pole
+@msg invalid brush: {e} neplatný štětec: {e}
+@msg invalid color neplatná barva
+@msg invalid colour {c} neplatná barva {c}
+@msg invalid delta neplatný rozdíl
+@msg invalid group neplatná skupina
+@msg invalid object id {v}: expected a non-negative integer neplatné ID objektu {v}: očekáváno nezáporné celé číslo
+@msg invalid point neplatný bod
+@msg invalid regular expression flag '{_1}' neplatný příznak regulárního výrazu '{_1}'
+@msg invalid regular expression: {m} neplatný regulární výraz: {m}
+@msg invalid size neplatná velikost
+@msg invalid t neplatné t
+@msg invalid time value neplatná hodnota času
+@msg invalid tolerance neplatná tolerance
+@msg is a confirm's answer, whose `value` must be true or false je odpověď na potvrzení, jejíž `value` musí být true nebo false
+@msg is a prompt's answer, whose `value` must be a string or null je odpověď na výzvu, jejíž `value` musí být řetězec nebo null
+@msg is an alert's answer, whose `value` must be "ok" je odpověď na upozornění, jejíž `value` musí být "ok"
+@msg is longer than 16 KiB je delší než 16 KiB
+@msg it draws its image through a clipping path vykresluje svůj obraz přes ořezovou cestu
+@msg it draws no image nevykresluje žádný obraz
+@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes je větší než {MAX_DICTIONARY_FILE_BYTES} bajtů
+@msg it isn't a file není to soubor
+@msg it names {_1} plates, and at most {MAX_PLATES} are read uvádí {_1} výtažků a čte se jich nejvýše {MAX_PLATES}
+@msg its art is too long or too detailed for a head jeho kresba je na hrot příliš dlouhá nebo příliš podrobná
+@msg its art paints nothing jeho kresba nic nevykresluje
+@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) všechny jeho pracovní plochy leží mimo plátno (více než {_1} pt od počátku)
+@msg its clipping masks are nested too deeply jeho ořezové masky jsou vnořeny příliš hluboko
+@msg its image is too large to read jeho obraz je příliš velký na přečtení
+@msg its metadata has no description jeho metadata nemají popis
+@msg its metadata has no direct length jeho metadata nemají přímou délku
+@msg its plates are too large: more than {MAX_SAMPLES} pixels together jeho výtažky jsou příliš velké: dohromady více než {MAX_SAMPLES} obrazových bodů
+@msg its plates are too large: {_1} plates of {w} × {h} pixels jeho výtažky jsou příliš velké: {_1} výtažků o {w} × {h} obrazových bodech
+@msg its plates can't be kept together jeho výtažky nelze udržet pohromadě
+@msg its shapes could not be combined into one jeho tvary nešlo spojit do jednoho
+@msg join two different paths spojte dvě různé cesty
+@msg jump to {pc}, past the end skok na {pc}, za konec
+@msg justify must be auto|left|center|right|justifyAll justify musí být auto|left|center|right|justifyAll
+@msg kbps must be a positive number, not {kbps} kbps musí být kladné číslo, ne {kbps}
+@msg keywords must be a list of words or a comma-separated string keywords musí být seznam slov nebo řetězec oddělený čárkami
+@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords smí mít nejvýše {MAX_KEYWORD} znaků, bez řídicích znaků
+@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind musí být 1, 2 nebo 3, nebo zadejte `name` přednastavení (viz perspective.presets.list)
+@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind musí být graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle nebo paraStyle
+@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout musí být "on", "off", "neutral" nebo logická hodnota, ne {v}
+@msg layer `{name}` has no art to export vrstva `{name}` nemá žádnou kresbu k exportu
+@msg layer must name a layer or sublayer layer musí uvádět vrstvu nebo podvrstvu
+@msg layers can't be intertwined: select the objects in them vrstvy nelze proplést: vyberte objekty v nich
+@msg layers go only at the top level or in layers, objects in layers and groups vrstvy mohou být jen na nejvyšší úrovni nebo ve vrstvách, objekty ve vrstvách a skupinách
+@msg layers must be true or false layers musí být true nebo false
+@msg layout must be gridByRow, gridByColumn, row or column layout musí být gridByRow, gridByColumn, row nebo column
+@msg leading must be a number or "auto" leading musí být číslo nebo "auto"
+@msg left/right must be non-negative widths left/right musí být nezáporné šířky
+@msg legacyNameEncoding must be one of {_1} legacyNameEncoding musí být jedna z hodnot {_1}
+@msg level `{t}`: 2 or 3 úroveň `{t}`: 2 nebo 3
+@msg line {line}: more than {MAX_CSV_COLS} columns řádek {line}: více než {MAX_CSV_COLS} sloupců
+@msg line {line}: unclosed quote řádek {line}: neuzavřená uvozovka
+@msg line {line}: value for `{var}` must match its variable kind řádek {line}: hodnota pro `{var}` musí odpovídat druhu její proměnné
+@msg lineEndings `{other}`: lf or crlf lineEndings `{other}`: lf nebo crlf
+@msg loading profiles from disk isn't available in the browser načítání profilů z disku není v prohlížeči dostupné
+@msg localize: the first argument must be a localization object or a string localize: první argument musí být lokalizační objekt nebo řetězec
+@msg margin: a {m} pt margin leaves no imageable area on the paper okraj: okraj {m} pt nenechá na papíře žádnou tisknutelnou plochu
+@msg matrix must be finite and invertible, depth finite matrix musí být konečná a invertovatelná, depth konečná
+@msg matte: {e} matte: {e}
+@msg member is for order front, forward, backward or back member se používá s order front, forward, backward nebo back
+@msg missing `artboards` chybí `artboards`
+@msg missing `at` [x, y] chybí `at` [x, y]
+@msg missing `attrs` chybí `attrs`
+@msg missing `color` or `opacity` chybí `color` nebo `opacity`
+@msg missing `colors` chybí `colors`
+@msg missing `commands` chybí `commands`
+@msg missing `destinationFolder` chybí `destinationFolder`
+@msg missing `effect` id chybí ID `effect`
+@msg missing `family` chybí `family`
+@msg missing `find` chybí `find`
+@msg missing `gid` (a glyph id, or null for the default glyph) chybí `gid` (ID glyfu, nebo null pro výchozí glyf)
+@msg missing `group` chybí `group`
+@msg missing `id` chybí `id`
+@msg missing `id`: give one, or select one type object chybí `id`: zadejte ho, nebo vyberte jeden textový objekt
+@msg missing `index` (a point index, or null to clear) chybí `index` (index bodu, nebo null pro vymazání)
+@msg missing `index` (a stop index, or null to clear) chybí `index` (index zarážky, nebo null pro vymazání)
+@msg missing `index` (an item index or null) chybí `index` (index položky nebo null)
+@msg missing `item` chybí `item`
+@msg missing `library` chybí `library`
+@msg missing `library` (see graphicStyle.libraries) chybí `library` (viz graphicStyle.libraries)
+@msg missing `library` (see swatch.library.list) chybí `library` (viz swatch.library.list)
+@msg missing `map` (rows [{from, to}] or {current}) chybí `map` (řádky [{from, to}] nebo {current})
+@msg missing `matrix` [a,b,c,d,e,f] chybí `matrix` [a,b,c,d,e,f]
+@msg missing `name` (see gradient.presets.list) chybí `name` (viz gradient.presets.list)
+@msg missing `offset` (a distance in points) chybí `offset` (vzdálenost v bodech)
+@msg missing `path` chybí `path`
+@msg missing `path` (a path object id) chybí `path` (ID objektu cesty)
+@msg missing `picks` chybí `picks`
+@msg missing `rule` chybí `rule`
+@msg missing `source` id chybí ID `source`
+@msg missing `start` (the byte offset of a character) chybí `start` (bajtový posun znaku)
+@msg missing `status`: linked, embedded, missing or modified chybí `status`: linked, embedded, missing nebo modified
+@msg missing `target` id chybí ID `target`
+@msg missing `text` chybí `text`
+@msg missing `to` chybí `to`
+@msg missing `to` (an index) chybí `to` (index)
+@msg missing `to`: a non-negative integer chybí `to`: nezáporné celé číslo
+@msg missing `type` chybí `type`
+@msg missing `value` chybí `value`
+@msg missing `word` chybí `word`
+@msg missing `{key}` chybí `{key}`
+@msg missing `{k}` chybí `{k}`
+@msg missing anchor chybí kotevní bod
+@msg missing anchors chybí kotevní body
+@msg missing anchors [[subpath, anchor]…] chybí kotevní body [[dílčí cesta, kotevní bod]…]
+@msg missing array `{key}` chybí pole `{key}`
+@msg missing case chybí case
+@msg missing color chybí barva
+@msg missing commands chybí příkazy
+@msg missing destination folder chybí cílová složka
+@msg missing from chybí from
+@msg missing group chybí skupina
+@msg missing id or ids chybí id nebo ids
+@msg missing ids chybí ids
+@msg missing indexes chybí indexes
+@msg missing matrix [a,b,c,d,e,f] chybí matrix [a,b,c,d,e,f]
+@msg missing name chybí název
+@msg missing op chybí op
+@msg missing or invalid `color` chybějící nebo neplatné `color`
+@msg missing or unknown `tool` chybějící nebo neznámé `tool`
+@msg missing point chybí bod
+@msg missing point [x,y] chybí bod [x,y]
+@msg missing point `{key}` ([x, y]) chybí bod `{key}` ([x, y])
+@msg missing points chybí body
+@msg missing points [[x,y]…] chybí body [[x,y]…]
+@msg missing segment chybí segment
+@msg missing stops chybí zarážky
+@msg missing subpaths chybí dílčí cesty
+@msg missing target chybí cíl
+@msg missing to chybí to
+@msg missing x, y chybí x, y
+@msg mode is set, add or toggle, not `{other}` mode je set, add nebo toggle, ne `{other}`
+@msg mode must be "cmyk" or "rgb" mode musí být "cmyk" nebo "rgb"
+@msg mode must be area|onPath, got {o} mode musí být area|onPath, zadáno {o}
+@msg more than 10000 linked files: package dependencies in smaller parts více než 10000 propojených souborů: zabalte závislosti po menších částech
+@msg more than 256 commands více než 256 příkazů
+@msg more than {MAX_BATCH_FILES} data sets více než {MAX_BATCH_FILES} sad dat
+@msg more than {MAX_BATCH_FILES} files více než {MAX_BATCH_FILES} souborů
+@msg more than {MAX_CSV_ROWS} data rows více než {MAX_CSV_ROWS} řádků dat
+@msg more than {MAX_FILES} images více než {MAX_FILES} obrázků
+@msg more than {MAX_IMPORT} presets více než {MAX_IMPORT} přednastavení
+@msg moved must be [[x,y]…] moved musí být [[x,y]…]
+@msg must be an object with `kind` and `value` musí být objekt s `kind` a `value`
+@msg must have `kind` alert, confirm or prompt musí mít `kind` alert, confirm nebo prompt
+@msg must have exactly `kind` and `value` musí mít právě `kind` a `value`
+@msg name (a toolbar's name) is required name (název panelu nástrojů) je povinné
+@msg name (a toolbar's name) must be a string name (název panelu nástrojů) musí být řetězec
+@msg name must be a toolbar's name name musí být název panelu nástrojů
+@msg names is empty names je prázdné
+@msg names must be an array of symbol names names musí být pole názvů symbolů
+@msg need `anchor` or `segment` + `t` je potřeba `anchor` nebo `segment` + `t`
+@msg need at least one anchor je potřeba alespoň jeden kotevní bod
+@msg need at least one point je potřeba alespoň jeden bod
+@msg need at least two points jsou potřeba alespoň dva body
+@msg needs a closing quote after the name vyžaduje za názvem uzavírací uvozovku
+@msg needs a name vyžaduje název
+@msg needs a name of at most 64 bytes vyžaduje název o nejvýše 64 bajtech
+@msg needs a name of letters, digits and _, or a quoted name vyžaduje název z písmen, číslic a _, nebo název v uvozovkách
+@msg needs a name without control characters vyžaduje název bez řídicích znaků
+@msg needs points vyžaduje body
+@msg nesting too deep příliš hluboké vnoření
+@msg nineSlice must be true or false nineSlice musí být true nebo false
+@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides musí být [left, top, right, bottom], každá hodnota -16..16, přičemž left + right < 1 a top + bottom < 1
+@msg no HP-GL format žádný formát HP-GL
+@msg no New Document profile called `{shown}` ({_1}) žádný profil nového dokumentu se nejmenuje `{shown}` ({_1})
+@msg no PDF preset named `{n}` přednastavení PDF s názvem `{n}` neexistuje
+@msg no SVG filter at index {k} na indexu {k} není žádný filtr SVG
+@msg no action set is current žádná sada akcí není aktuální
+@msg no active data set žádná aktivní sada dat
+@msg no appearance item {index} položka vzhledu {index} neexistuje
+@msg no arrowheads file here zde není žádný soubor šipek
+@msg no artboard {_1} pracovní plocha {_1} neexistuje
+@msg no artboard {artboard} (0-based; the document has {_1}) pracovní plocha {artboard} neexistuje (počítáno od 0; dokument jich má {_1})
+@msg no asset {id} prvek {id} neexistuje
+@msg no asset {id} (see assets.list) prvek {id} neexistuje (viz assets.list)
+@msg no assets to export žádné prvky k exportu
+@msg no assets yet: collect art for export first (assets.add) zatím žádné prvky: nejprve shromážděte kresbu pro export (assets.add)
+@msg no brush named `{n}` štětec s názvem `{n}` neexistuje
+@msg no character at byte {start} na bajtu {start} není žádný znak
+@msg no character has code {code} žádný znak nemá kód {code}
+@msg no colour žádná barva
+@msg no colour group `{group}` skupina barev `{group}` neexistuje
+@msg no colour group `{name}` skupina barev `{name}` neexistuje
+@msg no custom arrowhead {name} (see stroke.arrowheads.list) vlastní šipka {name} neexistuje (viz stroke.arrowheads.list)
+@msg no data sets žádné sady dat
+@msg no dataset named `{from}` sada dat s názvem `{from}` neexistuje
+@msg no dataset named `{name}` sada dat s názvem `{name}` neexistuje
+@msg no datasets žádné sady dat
+@msg no design named `{name}` návrh s názvem `{name}` neexistuje
+@msg no design named `{n}` návrh s názvem `{n}` neexistuje
+@msg no design named `{old}` návrh s názvem `{old}` neexistuje
+@msg no encoder for {_1} yet pro {_1} zatím není žádný kodér
+@msg no export preset `{name}` (see document.exportPresets.list) přednastavení exportu `{name}` neexistuje (viz document.exportPresets.list)
+@msg no file to open žádný soubor k otevření
+@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder žádná složka: předejte `path` nebo nastavte Předvolby › Zásuvné moduly a odkládací disky › Další složka zásuvných modulů
+@msg no fonts to look for: they are all available žádná písma k hledání: všechna jsou dostupná
+@msg no formats to export žádné formáty k exportu
+@msg no gradient preset `{name}` (see gradient.presets.list) přednastavení přechodu `{name}` neexistuje (viz gradient.presets.list)
+@msg no graphic style `{n}` in `{_1}` grafický styl `{n}` v `{_1}` neexistuje
+@msg no graphic style library `{key}` (see graphicStyle.libraries) knihovna grafických stylů `{key}` neexistuje (viz graphicStyle.libraries)
+@msg no guide selected není vybráno žádné vodítko
+@msg no guide selected (or give an index) není vybráno žádné vodítko (nebo zadejte index)
+@msg no guide {i} vodítko {i} neexistuje
+@msg no images in the folder ve složce nejsou žádné obrázky
+@msg no instructions žádné instrukce
+@msg no item at index {_1} na indexu {_1} není žádná položka
+@msg no item at index {from} na indexu {from} není žádná položka
+@msg no item at one of the indices {idx} na některém z indexů {idx} není žádná položka
+@msg no layer žádná vrstva
+@msg no layer to rename žádná vrstva k přejmenování
+@msg no layer {id} (see layer.list) vrstva {id} neexistuje (viz layer.list)
+@msg no layers to export žádné vrstvy k exportu
+@msg no library `{key}` knihovna `{key}` neexistuje
+@msg no metafile format žádný formát metasouboru
+@msg no object {_1} objekt {_1} neexistuje
+@msg no objects žádné objekty
+@msg no parent layer žádná nadřazená vrstva
+@msg no perspective grid preset named `{b}` (see perspective.presets.list) přednastavení perspektivní mřížky s názvem `{b}` neexistuje (viz perspective.presets.list)
+@msg no perspective grid preset named `{n}` přednastavení perspektivní mřížky s názvem `{n}` neexistuje
+@msg no point selected (give `index`) není vybrán žádný bod (zadejte `index`)
+@msg no preset called `{from}` (see file.newPresets) přednastavení s názvem `{from}` neexistuje (viz file.newPresets)
+@msg no preset called `{name}` (see file.newPresets) přednastavení s názvem `{name}` neexistuje (viz file.newPresets)
+@msg no preset named `{n}` přednastavení s názvem `{n}` neexistuje
+@msg no print preset named `{b}` (see print.presets.list) přednastavení tisku s názvem `{b}` neexistuje (viz print.presets.list)
+@msg no print preset named `{name}` (presets: {_1}) přednastavení tisku s názvem `{name}` neexistuje (přednastavení: {_1})
+@msg no print preset named `{n}` přednastavení tisku s názvem `{n}` neexistuje
+@msg no profile named `{name}` profil s názvem `{name}` neexistuje
+@msg no random numbers ({e}) náhodná čísla nejsou k dispozici ({e})
+@msg no readable files in the folder ve složce nejsou žádné čitelné soubory
+@msg no recovery copy `{f}` (see file.recovery.list) záložní kopie `{f}` neexistuje (viz file.recovery.list)
+@msg no rows žádné řádky
+@msg no saved PDF preset named `{name}` uložené přednastavení PDF s názvem `{name}` neexistuje
+@msg no saved export preset `{name}` uložené přednastavení exportu `{name}` neexistuje
+@msg no saved gradient preset called {name} uložené přednastavení přechodu s názvem {name} neexistuje
+@msg no saved gradient presets žádná uložená přednastavení přechodu
+@msg no saved preset called `{name}` uložené přednastavení s názvem `{name}` neexistuje
+@msg no saved presets to export (name [Default] in `names` to export it) žádná uložená přednastavení k exportu (chcete-li exportovat [Default], uveďte ho v `names`)
+@msg no saved presets to export (name built-in ones in `names`) žádná uložená přednastavení k exportu (vestavěná uveďte v `names`)
+@msg no saved print preset named `{name}` uložené přednastavení tisku s názvem `{name}` neexistuje
+@msg no saved profile called {name} uložený profil s názvem {name} neexistuje
+@msg no saved selection `{name}` uložený výběr `{name}` neexistuje
+@msg no saved theme called {name} (see colorTheme.list) uložený motiv s názvem {name} neexistuje (viz colorTheme.list)
+@msg no saved views žádná uložená zobrazení
+@msg no selected or given object has a visible 3D effect žádný vybraný ani zadaný objekt nemá viditelný 3D efekt
+@msg no series {i} řada {i} neexistuje
+@msg no slice {n} in the image (see slice.list) řez {n} v obrazu neexistuje (viz slice.list)
+@msg no source art žádná zdrojová kresba
+@msg no stop {i} (the gradient has {n}) zarážka {i} neexistuje (přechod jich má {n})
+@msg no style `{name}` styl `{name}` neexistuje
+@msg no such artboard {v} taková pracovní plocha neexistuje: {v}
+@msg no such assets (see assets.list) takové prvky neexistují (viz assets.list)
+@msg no such frame anchor, or the frame would reach past the canvas takový kotevní bod rámce neexistuje, nebo by rámec přesáhl plátno
+@msg no such objects takové objekty neexistují
+@msg no such path anchor, or the path would reach past the canvas takový kotevní bod cesty neexistuje, nebo by cesta přesáhla plátno
+@msg no such rows takové řádky neexistují
+@msg no swatch `{keep}` položka vzorníku `{keep}` neexistuje
+@msg no swatch `{n}` položka vzorníku `{n}` neexistuje
+@msg no swatch `{n}` (swatches and colour groups move separately) položka vzorníku `{n}` neexistuje (položky vzorníku a skupiny barev se přesouvají zvlášť)
+@msg no swatch library `{key}` (see swatch.library.list) knihovna vzorníku `{key}` neexistuje (viz swatch.library.list)
+@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) knihovna vzorníku `{key}` neexistuje (viz swatch.library.list; "document": vzorník dokumentu)
+@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) knihovna vzorníku `{key}` neexistuje (viz swatch.library.list; "{DOCUMENT_SWATCHES}": vzorník dokumentu)
+@msg no swatch named `{n}` položka vzorníku s názvem `{n}` neexistuje
+@msg no swatch or colour group `{n}` položka vzorníku ani skupina barev `{n}` neexistuje
+@msg no swatch or colour group `{n}` in `{_1}` položka vzorníku ani skupina barev `{n}` v `{_1}` neexistuje
+@msg no symbol named `{_1}` symbol s názvem `{_1}` neexistuje
+@msg no symbol named `{name}` symbol s názvem `{name}` neexistuje
+@msg no text objects selected nejsou vybrány žádné textové objekty
+@msg no toolbar named `{_1}` panel nástrojů s názvem `{_1}` neexistuje
+@msg no toolbar named `{name}` panel nástrojů s názvem `{name}` neexistuje
+@msg no tracing preset `{preset}` (see imageTrace.presets) přednastavení vektorizace `{preset}` neexistuje (viz imageTrace.presets)
+@msg no user library folder here (save with a path, or without one to get the data) zde není žádná složka uživatelských knihoven (uložte s cestou, nebo bez ní, chcete-li získat data)
+@msg no variable named `{from}` proměnná s názvem `{from}` neexistuje
+@msg no variable named `{name}` proměnná s názvem `{name}` neexistuje
+@msg no variable named `{var}` proměnná s názvem `{var}` neexistuje
+@msg no variable named `{v}` proměnná s názvem `{v}` neexistuje
+@msg no view named `{name}` zobrazení s názvem `{name}` neexistuje
+@msg node {_1} already has the text variable `{_2}` uzel {_1} už má textovou proměnnou `{_2}`
+@msg node {_1} is not a graph uzel {_1} není graf
+@msg node {_1} is not an image uzel {_1} není obraz
+@msg node {_1} is not type uzel {_1} není text
+@msg not a DCS file nejde o soubor DCS
+@msg not a PDF presets file (format `{_1}`) nejde o soubor přednastavení PDF (formát `{_1}`)
+@msg not a PDF presets file: {e} nejde o soubor přednastavení PDF: {e}
+@msg not a RegExp object nejde o objekt RegExp
+@msg not a Word document: its WordDocument stream doesn't start with a file information block nejde o dokument Word: jeho proud WordDocument nezačíná blokem informací o souboru
+@msg not a Word document: its compound file has no WordDocument stream nejde o dokument Word: jeho složený soubor nemá proud WordDocument
+@msg not a Word document: its main part is not a WordprocessingML document nejde o dokument Word: jeho hlavní část není dokument WordprocessingML
+@msg not a Word document: its package has no main document part nejde o dokument Word: jeho balíček nemá hlavní část dokumentu
+@msg not a closed path nejde o uzavřenou cestu
+@msg not a flattener presets file (format `{_1}`) nejde o soubor přednastavení sloučení průhlednosti (formát `{_1}`)
+@msg not a flattener presets file: {e} nejde o soubor přednastavení sloučení průhlednosti: {e}
+@msg not a layer or group nejde o vrstvu ani skupinu
+@msg not a library file: {e} nejde o soubor knihovny: {e}
+@msg not a mesh envelope point nejde o bod síťové obálky
+@msg not a perspective grid presets file (format `{_1}`) nejde o soubor přednastavení perspektivní mřížky (formát `{_1}`)
+@msg not a perspective grid presets file: {e} nejde o soubor přednastavení perspektivní mřížky: {e}
+@msg not a print presets file (format `{_1}`) nejde o soubor přednastavení tisku (formát `{_1}`)
+@msg not a print presets file: {e} nejde o soubor přednastavení tisku: {e}
+@msg not a regular expression nejde o regulární výraz
+@msg not a stored or deflated central record nejde o uložený ani komprimovaný centrální záznam
+@msg not a valid SVGZ file: {e} nejde o platný soubor SVGZ: {e}
+@msg not an RTF file: it doesn't start with {\rtf nejde o soubor RTF: nezačíná na {\rtf
+@msg not an action set: the text doesn't start with /version and a number nejde o sadu akcí: text nezačíná na /version a číslo
+@msg not an image VectorCraft reads (see document.formats) nejde o obrázek, který VectorCraft čte (viz document.formats)
+@msg not area type (text in a frame) outside perspective nejde o text v oblasti (text v rámci) mimo perspektivu
+@msg not editing an opacity mask maska krytí se neupravuje
+@msg not type on a path outside perspective nejde o text na cestě mimo perspektivu
+@msg nothing prints nic se netiskne
+@msg nothing to bind není co svázat
+@msg nothing to change není co změnit
+@msg nothing to change: give dx, dy, hScale, vScale or rotation není co změnit: zadejte dx, dy, hScale, vScale nebo rotation
+@msg nothing to crop není co oříznout
+@msg nothing to export není co exportovat
+@msg nothing to export: the document has no visible art není co exportovat: dokument nemá žádnou viditelnou kresbu
+@msg nothing to mark není co označit
+@msg nothing to print: every artboard picked is blank není co tisknout: každá zvolená pracovní plocha je prázdná
+@msg nothing to print: the document has no art that prints není co tisknout: dokument nemá žádnou kresbu, která se tiskne
+@msg nothing to repeat není co opakovat
+@msg numbers out of order in {_1} quantifier čísla v kvantifikátoru {_1} jsou ve špatném pořadí
+@msg object is not a path objekt není cesta
+@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object průhlednost objektu nelze nabrat na zvýrazněné znaky; vypněte Průhlednost ve volbách kapátka nebo vyberte celý objekt
+@msg object {_1} can't be set as point type objekt {_1} nelze nastavit jako bodový text
+@msg object {_1} has lost its file's contents: relink it objekt {_1} ztratil obsah svého souboru: propojte ho znovu
+@msg object {_1} has no path objekt {_1} nemá cestu
+@msg object {_1} has no size objekt {_1} nemá velikost
+@msg object {_1} is hidden or locked, or on a locked artboard objekt {_1} je skrytý nebo zamknutý, nebo je na zamknuté pracovní ploše
+@msg object {_1} is not a placed document objekt {_1} není umístěný dokument
+@msg object {_1} is not an Intertwine group objekt {_1} není skupina propletení
+@msg object {_1} is not an image objekt {_1} není obraz
+@msg object {_1} is not an image or a linked file objekt {_1} není obraz ani propojený soubor
+@msg object {_1} is not an image or a placed document objekt {_1} není obraz ani umístěný dokument
+@msg object {_1} is not in this Intertwine group objekt {_1} není v této skupině propletení
+@msg object {_1} needs a 3D effect objekt {_1} vyžaduje 3D efekt
+@msg object {_1} needs a 3D effect at index {index} objekt {_1} vyžaduje 3D efekt na indexu {index}
+@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic objekt {_1}: duté Vysunutí a zkosení nemá zakončení: chcete-li mapovat grafiku, zapněte Zakončení
+@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front objekt {_1}: při hloubce 0 má objekt jednu plochu: mapujte jeho grafiku na přední stranu
+@msg object {_1}: {_2} objekt {_1}: {_2}
+@msg object {_1}: {e} objekt {_1}: {e}
+@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams objekt {_1}: {name} na indexu {index} nemá žádná přednastavení osvětlení; světla nastavte pomocí effect.setParams
+@msg object {id} has anchors 0–{_1} for `corners`, not {past} objekt {id} má pro `corners` kotevní body 0–{_1}, ne {past}
+@msg object {id} has {n} anchors, so no anchor {past} objekt {id} má {n} kotevních bodů, takže kotevní bod {past} neexistuje
+@msg objects must be none, move or copy objects musí být none, move nebo copy
+@msg objects were left out objekty byly vynechány
+@msg offset out of range posun je mimo rozsah
+@msg on must be true or false on musí být true nebo false
+@msg only groups, layers, type and images have a Contents (Characters) row řádek Obsah (Znaky) mají jen skupiny, vrstvy, text a obrazy
+@msg only interior mesh lines can be deleted odstranit lze jen vnitřní linie sítě
+@msg only solid colours can be added (not gradients or patterns) přidat lze jen plné barvy (ne přechody ani vzorky)
+@msg only solid-colour swatches can be spot colours přímými barvami mohou být jen položky vzorníku s plnou barvou
+@msg only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first z tohoto souboru Affinity šlo přečíst jen vložený náhled: pomocí Soubor › Otevřít ho zobrazíte s varováním, nebo nejprve exportujte SVG či PDF z Affinity
+@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning z tohoto souboru Affinity šlo přečíst jen vložený náhled; pomocí Soubor › Otevřít ho zobrazíte s varováním
+@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto, above, below, icon a umístění ve sloupci patří k window.panel.float a window.panel.dock
+@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto, above, below, icon a umístění ve sloupci nepřijímají anchor, side, zone ani before
+@msg onto, above, below, icon and a column place take no x, y, width or height onto, above, below, icon a umístění ve sloupci nepřijímají x, y, width ani height
+@msg opacity must be a number opacity musí být číslo
+@msg open a document to look for its linked files otevřete dokument, jehož propojené soubory se mají hledat
+@msg operand too large operand je příliš velký
+@msg options must be an object (see file.formatOptions) options musí být objekt (viz file.formatOptions)
+@msg order is a whole number from 0 to {others}, not `{v}` order je celé číslo od 0 do {others}, ne `{v}`
+@msg order must be leftToRight or rightToLeft order musí být leftToRight nebo rightToLeft
+@msg orientation must be portrait or landscape, not `{o}` orientation musí být portrait nebo landscape, ne `{o}`
+@msg outlineText {v}: true or false outlineText {v}: true nebo false
+@msg output intent profile: {e} profil výstupního záměru: {e}
+@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination: žádný profil RGB ani CMYK se nejmenuje „{name}“
+@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination: soubory {_1} jsou CMYK a „{name}“ je profil RGB
+@msg output.destination: {e} output.destination: {e}
+@msg output.inks: no ink is set to print output.inks: žádná tisková barva není nastavena k tisku
+@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent: žádný profil se nejmenuje „{name}“ a výstupní záměr {std} svůj profil vkládá
+@msg output.outputIntent: no profile is called “{name}”: a {std} output intent embeds a profile, or names a registered printing condition (outputConditionId and registry) output.outputIntent: žádný profil se nejmenuje „{name}“: výstupní záměr {std} profil vkládá, nebo uvádí registrovanou tiskovou podmínku (outputConditionId a registry)
+@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent: soubory {std} se tisknou v CMYK a „{name}“ je profil RGB
+@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent: „{name}“ je profil odstínů šedi, který nemůže být výstupním záměrem {std}
+@msg overlap: {o} pt is half a tile or more překrytí: {o} pt je polovina dlaždice nebo více
+@msg overlapping Shaper paths must be in the same layer or group překrývající se cesty Shaper musí být ve stejné vrstvě nebo skupině
+@msg overlapping edits překrývající se úpravy
+@msg page art conflicts with image data for {key} kresba stránky je v rozporu s obrazovými daty pro {key}
+@msg page art conflicts with pattern {name} kresba stránky je v rozporu se vzorkem {name}
+@msg page art conflicts with swatch {name} kresba stránky je v rozporu s položkou vzorníku {name}
+@msg page art is missing image data for {key} kresbě stránky chybí obrazová data pro {key}
+@msg page art is missing pattern {name} kresbě stránky chybí vzorek {name}
+@msg page art is missing swatch {name} kresbě stránky chybí položka vzorníku {name}
+@msg page must be a whole number from 1 page musí být celé číslo od 1
+@msg page thumbnail: {e} miniatura stránky: {e}
+@msg page {page} of the PDF has no objects stránka {page} dokumentu PDF nemá žádné objekty
+@msg page {page}: `{file}` has {n} {artboards} stránka {page}: `{file}` má {n} {artboards}
+@msg page {page}: `{name}` has {n} {pages} stránka {page}: `{name}` má {n} {pages}
+@msg pages must be a range such as "2-3" or a page number, not {v} pages musí být rozsah jako "2-3" nebo číslo stránky, ne {v}
+@msg pages: {e} pages: {e}
+@msg panel position must be finite pozice panelu musí být konečná
+@msg parameters must be an object parametry musí být objekt
+@msg params must be an object params musí být objekt
+@msg passwords can have up to {MAX_PASSWORD} bytes hesla mohou mít nejvýše {MAX_PASSWORD} bajtů
+@msg path chunks must share their boundary point úseky cesty musí sdílet svůj hraniční bod
+@msg path has no open end cesta nemá žádný otevřený konec
+@msg paths must be strings paths musí být řetězce
+@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false vyžaduje všechny pracovní plochy, aby se zachovala upravitelná kresba
+@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false vyžaduje pracovní plochy, aby se zachovala upravitelná kresba, když je useArtboards false
+@msg pdfs `{m}`: single or multiple pdfs `{m}`: single nebo multiple
+@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom je "artwork" nebo "swatches", ne {v}
+@msg pins and moved must be non-empty lists of the same length pins a moved musí být neprázdné seznamy stejné délky
+@msg pins must be [[x,y]…] pins musí být [[x,y]…]
+@msg pins must be finite pins musí být konečné
+@msg place is above, below or inside, not `{other}` place je above, below nebo inside, ne `{other}`
+@msg plane must be left|right|ground plane musí být left|right|ground
+@msg plane must be left|right|ground|none plane musí být left|right|ground|none
+@msg point is outside the mesh bod je mimo síť
+@msg point x must be a number x bodu musí být číslo
+@msg point y must be a number y bodu musí být číslo
+@msg points must be [t, left, right] with non-negative widths points musí být [t, left, right] s nezápornými šířkami
+@msg points must be [x,y] pairs points musí být dvojice [x,y]
+@msg points must be a non-empty [[x,y,pressure?]…] list points musí být neprázdný seznam [[x,y,pressure?]…]
+@msg points must be a non-empty list of finite [x,y] points musí být neprázdný seznam konečných [x,y]
+@msg points must be an array or null points musí být pole nebo null
+@msg points must be finite points musí být konečné
+@msg points must be finite [x,y] pairs points musí být konečné dvojice [x,y]
+@msg points must be finite canvas points points musí být konečné body plátna
+@msg points must contain 1–4096 coordinates points musí obsahovat 1–4096 souřadnic
+@msg position must be normal, superscript or subscript position musí být normal, superscript nebo subscript
+@msg position must be topLeft, topRight, bottomLeft or bottomRight position musí být topLeft, topRight, bottomLeft nebo bottomRight
+@msg positions must be between 0 and 100000 pt positions musí být mezi 0 a 100000 pt
+@msg ppi must be a positive number, not {ppi} ppi musí být kladné číslo, ne {ppi}
+@msg ppi must be between 1 and 2400 ppi musí být mezi 1 a 2400
+@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}` není transforms|bounds|fileDimensions|fit|fill
+@msg preserveShape must be clippingMask|transparency preserveShape musí být clippingMask|transparency
+@msg preset `{id}`: mobile or density přednastavení `{id}`: mobile nebo density
+@msg preset must be standard, diffuse, topLeft or right preset musí být standard, diffuse, topLeft nebo right
+@msg preset must be the name of a print preset preset musí být název přednastavení tisku
+@msg previewMode must be default, pixel or overprint previewMode musí být default, pixel nebo overprint
+@msg print settings: {e} nastavení tisku: {e}
+@msg procedures nest too deeply procedury jsou vnořeny příliš hluboko
+@msg procedures nested too deeply procedury byly vnořeny příliš hluboko
+@msg quotes must be {double?, single?} quotes musí být {double?, single?}
+@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k} musí být otevírací a uzavírací uvozovka, např. "“”"
+@msg range out of order in character class rozsah ve třídě znaků je ve špatném pořadí
+@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi musí být 1–2400 (72 obrazovka, 150 střední, 300 vysoké)
+@msg rating must be a whole number from 0 to {MAX_RATING} rating musí být celé číslo od 0 do {MAX_RATING}
+@msg rect and {k} contradict each other: give the area or the artboards rect a {k} si odporují: zadejte oblast, nebo pracovní plochy
+@msg rect applies to raster formats, not {_1} rect platí pro rastrové formáty, ne pro {_1}
+@msg rect is [x, y, width, height] in points rect je [x, y, width, height] v bodech
+@msg rect must be [x, y, width, height] rect musí být [x, y, width, height]
+@msg rect must be [x, y, width, height] with a positive width and height rect musí být [x, y, width, height] s kladnou šířkou a výškou
+@msg rect needs finite numbers and a positive width and height rect vyžaduje konečná čísla a kladnou šířku a výšku
+@msg redraw needs id redraw vyžaduje id
+@msg redraw within must be a finite distance of 0 or more within v redraw musí být konečná vzdálenost 0 nebo větší
+@msg reduce of an empty array with no initial value reduce prázdného pole bez počáteční hodnoty
+@msg registration must be 0..8 registration musí být 0..8
+@msg regular expression flag '{_1}' given twice příznak regulárního výrazu '{_1}' je zadán dvakrát
+@msg release a layer or a group uvolněte vrstvu nebo skupinu
+@msg replace keeps the replaced object's place and transform: drop template, at and rect replace zachová umístění a transformaci nahrazeného objektu: vynechte template, at a rect
+@msg replace: select the one object to replace replace: vyberte jeden objekt, který se má nahradit
+@msg resolution must be a number or screen|medium|high resolution musí být číslo nebo screen|medium|high
+@msg resolution must be between 1 and 2400 ppi resolution musí být mezi 1 a 2400 ppi
+@msg rotation must be a number of degrees rotation musí být počet stupňů
+@msg rotation must be finite rotation musí být konečné
+@msg rows and cols must be 1..50 rows a cols musí být 1..50
+@msg rows and columns must be 1..500 rows a columns musí být 1..500
+@msg rows of objects whose appearances differ can't be reordered together řádky objektů s odlišným vzhledem nelze přeskupovat společně
+@msg sampled text would exceed the canvas (coordinates out of range) nabraný text by přesáhl plátno (souřadnice mimo rozsah)
+@msg save the document first: Package collects a saved document nejprve dokument uložte: Balíček shromažďuje uložený dokument
+@msg save the document first: copy puts the file in its folder nejprve dokument uložte: copy vloží soubor do jeho složky
+@msg savedPreset: {e} savedPreset: {e}
+@msg scale `{s}`: a factor such as 2x, a width (100w), a height (100h) or a resolution (72ppi) scale `{s}`: násobek jako 2x, šířka (100w), výška (100h) nebo rozlišení (72ppi)
+@msg scale must be a number above 0, at most 64 scale musí být číslo větší než 0, nejvýše 64
+@msg scale must be non-zero scale nesmí být nula
+@msg scope must be selection or all (got {other}) scope musí být selection nebo all (zadáno {other})
+@msg scribble is too long čmáranice je příliš dlouhá
+@msg segment does not exist segment neexistuje
+@msg select a Shaper group vyberte skupinu Shaper
+@msg select a blend to set easing or color shift vyberte prolnutí, u kterého se má nastavit náběh nebo posun barev
+@msg select a compound shape vyberte složený tvar
+@msg select a graph vyberte graf
+@msg select a group or layer with visible, unlocked members vyberte skupinu nebo vrstvu s viditelnými odemknutými členy
+@msg select a layer vyberte vrstvu
+@msg select a text object vyberte textový objekt
+@msg select an SVG filter vyberte filtr SVG
+@msg select an image (or an Image Trace group) vyberte obraz (nebo skupinu vektorizace obrazu)
+@msg select an image, or pass id vyberte obraz, nebo předejte id
+@msg select an object (or pass `id`) vyberte objekt (nebo předejte `id`)
+@msg select an object with a 3D effect vyberte objekt s 3D efektem
+@msg select an object with a 3D effect, or give its id in ids vyberte objekt s 3D efektem, nebo zadejte jeho ID v ids
+@msg select an object with a freeform gradient vyberte objekt s volným přechodem
+@msg select an object with an opacity mask vyberte objekt s maskou krytí
+@msg select area type vyberte text v oblasti
+@msg select area type (text in a frame) vyberte text v oblasti (text v rámci)
+@msg select area type and closed paths (at least one area type object) vyberte text v oblasti a uzavřené cesty (alespoň jeden objekt textu v oblasti)
+@msg select area type whose first line can fit its frame vyberte text v oblasti, jehož první řádek se vejde do rámce
+@msg select artwork to add its colours vyberte kresbu, jejíž barvy se mají přidat
+@msg select artwork to make a group of its colours vyberte kresbu, z jejíchž barev se má vytvořit skupina
+@msg select artwork to recolour, or give a colour `group` vyberte kresbu k přebarvení, nebo zadejte skupinu barev v `group`
+@msg select exactly two path ends to join vyberte právě dva konce cest ke spojení
+@msg select members of a compound shape vyberte členy složeného tvaru
+@msg select objects or give ids vyberte objekty nebo zadejte ids
+@msg select objects to align to the guide vyberte objekty, které se mají zarovnat k vodítku
+@msg select one Intertwine group vyberte jednu skupinu propletení
+@msg select one image or pass id vyberte jeden obraz nebo předejte id
+@msg select one layer, group or object to rename vyberte jednu vrstvu, skupinu nebo objekt k přejmenování
+@msg select paths vyberte cesty
+@msg select paths or compound paths vyberte cesty nebo složené cesty
+@msg select rows or objects to collect vyberte řádky nebo objekty ke shromáždění
+@msg select something to export vyberte něco k exportu
+@msg select something to export (or turn off selectedOnly) vyberte něco k exportu (nebo vypněte selectedOnly)
+@msg select text to redefine the style from vyberte text, podle kterého se má styl předefinovat
+@msg select the art and, on top of it, the mask object vyberte kresbu a nad ní objekt masky
+@msg select the art for the design vyberte kresbu pro návrh
+@msg select the art for the pattern tile vyberte kresbu pro dlaždici vzorku
+@msg select the art to add first nejprve vyberte kresbu, která se má přidat
+@msg select the art to collect (or give its ids) vyberte kresbu ke shromáždění (nebo zadejte její ids)
+@msg select the art to weed vyberte kresbu pro vybírání
+@msg select the content and a path on top vyberte obsah a cestu nad ním
+@msg select the images to relink, or pass ids vyberte obrazy k novému propojení, nebo předejte ids
+@msg select the images, or pass ids vyberte obrazy, nebo předejte ids
+@msg select the linked images or placed documents, or pass ids vyberte propojené obrazy nebo umístěné dokumenty, nebo předejte ids
+@msg select the object to copy from vyberte objekt, ze kterého se má kopírovat
+@msg select the objects (or give their ids) vyberte objekty (nebo zadejte jejich ids)
+@msg select the objects to attach to the path vyberte objekty, které se mají připojit k cestě
+@msg select the objects to bind vyberte objekty, které se mají svázat
+@msg select two or more objects to intertwine vyberte dva nebo více objektů k propletení
+@msg select two or more slices to combine vyberte dva nebo více řezů ke spojení
+@msg select type on a path vyberte text na cestě
+@msg select type on a path or area type vyberte text na cestě nebo text v oblasti
+@msg selection `{m}`: one or each selection `{m}`: one nebo each
+@msg settings is an object of document.exportForScreens params settings je objekt s parametry document.exportForScreens
+@msg settings must be an object settings musí být objekt
+@msg show `{show}`: all, missing, modified or embedded show `{show}`: all, missing, modified nebo embedded
+@msg show must be true or false show musí být true nebo false
+@msg side is left or right, not `{v}` side je left nebo right, ne `{v}`
+@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size musí být "small", "medium", "large" nebo počet bodů větší než 0 a nejvýše {_1}
+@msg size must be positive size musí být kladné
+@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode je "fixed" nebo "pressure", ne `{other}`
+@msg sizeMode is "fixed" or "pressure", not {other} sizeMode je "fixed" nebo "pressure", ne {other}
+@msg sizeVariation must be 0 or more sizeVariation musí být 0 nebo více
+@msg smallCaps must be true or false smallCaps musí být true nebo false
+@msg sort `{x}`: name, kind or status sort `{x}`: name, kind nebo status
+@msg source paths are too large for Shaper zdrojové cesty jsou pro Shaper příliš velké
+@msg source text transform is collapsed or unrepresentable transformace zdrojového textu je zborcená nebo ji nelze vyjádřit
+@msg spot, mode and group apply to solid colours only spot, mode a group platí jen pro plné barvy
+@msg status `{s}`: linked, embedded, missing or modified status `{s}`: linked, embedded, missing nebo modified
+@msg steps must be 1..1000 steps musí být 1..1000
+@msg steps must be 1..{MAX_STEPS} steps musí být 1..{MAX_STEPS}
+@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) zarážka {i}: `plates` musí být čtyři čísla od 0 do 1 (C, M, Y, K)
+@msg stopped before the archive was finished zastaveno před dokončením archivu
+@msg strokeMode must be within|along|across, got {m} strokeMode musí být within|along|across, zadáno {m}
+@msg strokeOptions must be an object of stroke.set options strokeOptions musí být objekt s volbami stroke.set
+@msg strokeWidth must be a positive number of pixels strokeWidth musí být kladný počet obrazových bodů
+@msg subpath {si} is not an open subpath dílčí cesta {si} není otevřená dílčí cesta
+@msg subpaths required when joining the same path při spojování téže cesty je vyžadováno subpaths
+@msg swatchConflict must be "merge" or "add", not {v} swatchConflict musí být "merge" nebo "add", ne {v}
+@msg table must be true or false table musí být true nebo false
+@msg takes one name přijímá jeden název
+@msg target text transform is collapsed or unrepresentable transformace cílového textu je zborcená nebo ji nelze vyjádřit
+@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text musí být {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?}
+@msg text transform is collapsed or unrepresentable transformace textu je zborcená nebo ji nelze vyjádřit
+@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces musí být celé číslo od 2 do 100
+@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}`: použijte "{_1}" nebo "{_2}"
+@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage: zadejte {path} nebo {name, dataBase64} souboru obrázku, nebo texturu tak, jak ji vrací effect.list
+@msg textureImage: {e} textureImage: {e}
+@msg that makes more than {MAX_PIECES} slices tím vznikne více než {MAX_PIECES} řezů
+@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer tím vznikne příliš mnoho dílů (nejvýše 100000) nebo kroků čárkování: zjednodušte vzor čárkování nebo prodlužte čárky
+@msg that object is not in the overlap tento objekt není v překryvu
+@msg the Affinity document has no page with a size dokument Affinity nemá žádnou stránku s velikostí
+@msg the CMYK image couldn't be written obraz CMYK nešlo zapsat
+@msg the Layers panel is not shown panel Vrstvy není zobrazen
+@msg the Layers panel's search, filter or Show Layers Only hides its row jeho řádek skrývá hledání, filtr nebo volba Zobrazit pouze vrstvy v panelu Vrstvy
+@msg the Objects on Path group is hidden or locked skupina Objekty na cestě je skrytá nebo zamknutá
+@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected nastavení zabezpečení PDF bylo uloženo bez hesel: zadejte znovu openPassword nebo permissionsPassword, nebo pro export bez ochrany zadejte security bez passwordRequired
+@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) soubor RTF má příliš mnoho odstavců nebo změn formátování na umístění jako text (nejvýše {MAX_PARAGRAPHS} odstavců a {MAX_RUNS} úseků)
+@msg the RTF file is too large to place as type (at most {_1} MB) soubor RTF je příliš velký na umístění jako text (nejvýše {_1} MB)
+@msg the SVG expands to more than {0} nested SVG images SVG se rozvine na více než {0} vnořených obrázků SVG
+@msg the SVG expands to more than {0} objects SVG se rozvine na více než {0} objektů
+@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes soubor SVG je větší než {MAX_IMPORT_BYTES} bajtů
+@msg the SVG has no drawable objects SVG nemá žádné vykreslitelné objekty
+@msg the SVGZ file unpacks to more than {_1} MB soubor SVGZ se rozbalí na více než {_1} MB
+@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place dokument Word má více než {MAX_PAGES} stránek vlastností, což je na umístění příliš mnoho
+@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place dokument Word má více než {MAX_RUNS} úseků vlastností znaků nebo odstavců, což je na umístění příliš mnoho
+@msg the Word document has more than {_1} MB of formatting properties, too many to place dokument Word má více než {_1} MB vlastností formátování, což je na umístění příliš mnoho
+@msg the Word document has too much text or formatting to place as type (at most {MAX_PARAGRAPHS} paragraphs, {MAX_RUNS} runs and {_1} MB of text) dokument Word má příliš mnoho textu nebo formátování na umístění jako text (nejvýše {MAX_PARAGRAPHS} odstavců, {MAX_RUNS} úseků a {_1} MB textu)
+@msg the Word document has too much text to place as type (at most {_1} MB) dokument Word má příliš mnoho textu na umístění jako text (nejvýše {_1} MB)
+@msg the Word document is encrypted or protected with a password; save it without one to place it dokument Word je zašifrovaný nebo chráněný heslem; chcete-li ho umístit, uložte ho bez hesla
+@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) dokument Word je příliš velký na umístění (jeho proud {name} obsahuje více než {_1} MB)
+@msg the Word document was last saved with fast save, which VectorCraft doesn't read; save it again with fast save turned off dokument Word byl naposledy uložen rychlým ukládáním, které VectorCraft nečte; uložte ho znovu s vypnutým rychlým ukládáním
+@msg the Word document was saved by Word 6.0 or Word 95, which VectorCraft doesn't read; save it as a Word 97-2003 document dokument Word byl uložen ve Wordu 6.0 nebo Wordu 95, což VectorCraft nečte; uložte ho jako dokument Word 97-2003
+@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place text dokumentu Word se skládá z více než {MAX_PIECES} částí, což je na umístění příliš mnoho
+@msg the XML operation needs an XML or XMLList value operace XML vyžaduje hodnotu XML nebo XMLList
+@msg the XPath expression does not evaluate to a node-set výraz XPath se nevyhodnotí na množinu uzlů
+@msg the XPath expression ends early výraz XPath končí předčasně
+@msg the XPath expression has an unmatched ) výraz XPath obsahuje nespárovanou )
+@msg the XPath expression has an unmatched ] výraz XPath obsahuje nespárovanou ]
+@msg the XPath expression has an unmatched closing bracket výraz XPath obsahuje nespárovanou uzavírací závorku
+@msg the XPath expression selects a namespace node, which is not an XML value výraz XPath vybírá uzel jmenného prostoru, což není hodnota XML
+@msg the XPath expression selects the root node, which is not an XML value výraz XPath vybírá kořenový uzel, což není hodnota XML
+@msg the XPath function {name} needs a node-set funkce XPath {name} vyžaduje množinu uzlů
+@msg the XPath function {name} takes the wrong number of arguments funkce XPath {name} dostala nesprávný počet argumentů
+@msg the XPath name functions need a node-set funkce XPath pro názvy vyžadují množinu uzlů
+@msg the XPath prefix {shown} is not bound to a namespace předpona XPath {shown} není svázána se jmenným prostorem
+@msg the XPath variable ${shown} is not bound proměnná XPath ${shown} není svázána
+@msg the action set ids are not a list ID sad akcí nejsou seznam
+@msg the active paint is not a gradient aktivní nátěr není přechod
+@msg the argument list is not an object seznam argumentů není objekt
+@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size pracovní plochy se nevejdou na plátno: zvolte méně pracovních ploch, menší rozestup nebo menší velikost
+@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out kresba je pro EPS příliš velká: vše, co následuje po 256 MB PostScriptu, bylo vynecháno
+@msg the bitmaps hold more pixels than one file may bitmapy obsahují více obrazových bodů, než smí mít jeden soubor
+@msg the character's size can't be measured velikost znaku nelze změřit
+@msg the code runs past its last instruction kód běží za svou poslední instrukci
+@msg the compound shape is too complex to expand složený tvar je příliš složitý na rozložení
+@msg the container of the listed objects is gone kontejner uvedených objektů už neexistuje
+@msg the copy `{user_id}` can't be read as a library kopii `{user_id}` nelze přečíst jako knihovnu
+@msg the copy would leave the canvas kopie by opustila plátno
+@msg the curve tolerance must be from 0.01 to 10 points tolerance křivky musí být od 0.01 do 10 bodů
+@msg the custom dictionary couldn't be saved to {_1}: {e} vlastní slovník nešlo uložit do {_1}: {e}
+@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is vlastní slovník {_1} nelze přečíst ({why}), proto zůstává beze změny
+@msg the cut cannot be made on this art: {e} řez na této kresbě nelze provést: {e}
+@msg the data isn't an EMF or WMF picture data nejsou obrázek EMF ani WMF
+@msg the data isn't an EPS or PostScript picture data nejsou obrázek EPS ani PostScript
+@msg the deferred paste is no longer available odložené vložení už není dostupné
+@msg the design is too big to draw at every data point of this graph návrh je příliš velký na vykreslení v každém datovém bodě tohoto grafu
+@msg the design is too big to draw in every column of this graph návrh je příliš velký na vykreslení v každém sloupci tohoto grafu
+@msg the dialog it was opened over is no longer open dialog, nad kterým byl otevřen, už není otevřený
+@msg the dictionary stack overflowed zásobník slovníků přetekl
+@msg the document already has an SVG filter named {name} dokument už má filtr SVG s názvem {name}
+@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it dokument má jiný filtr SVG s názvem {name}: použijte svgFilter.apply, nebo ho upravte pomocí svgFilter.define s `replace`
+@msg the document has no SVG filter named {name} dokument nemá žádný filtr SVG s názvem {name}
+@msg the document has no SVG filter named {r} dokument nemá žádný filtr SVG s názvem {r}
+@msg the document has no color swatches dokument nemá žádné barevné položky vzorníku
+@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 vzorník dokumentu obsahuje {_1} barev; redukce podle dokumentu jich přijímá nejvýše 255
+@msg the editing data's hash has no place pro hash dat pro úpravy není místo
+@msg the entities the document declares expand to too much text entity, které dokument deklaruje, se rozvinou na příliš mnoho textu
+@msg the envelope's content has no bounds obsah obálky nemá hranice
+@msg the envelope's mesh is malformed síť obálky je poškozená
+@msg the file holds no `variableSets` element soubor neobsahuje žádný prvek `variableSets`
+@msg the file is larger than {_1} MB soubor je větší než {_1} MB
+@msg the file is too large soubor je příliš velký
+@msg the file isn't valid SVG: {e} soubor není platné SVG: {e}
+@msg the file names no plate soubor neuvádí žádný výtažek
+@msg the file was updated or encrypted already soubor už byl aktualizován nebo zašifrován
+@msg the first character of a line has no character before it to kern: it moves only up and down (dy) první znak řádku nemá před sebou znak, vůči kterému by se vyrovnával: posouvá se jen nahoru a dolů (dy)
+@msg the font {_1} is not available písmo {_1} není dostupné
+@msg the format has no file extension formát nemá příponu souboru
+@msg the function's prototype is not an object prototyp funkce není objekt
+@msg the graphic can't be read: {e} grafiku nelze přečíst: {e}
+@msg the graphic is empty grafika je prázdná
+@msg the graphic's data is damaged data grafiky jsou poškozená
+@msg the graphic's document has no artboard dokument grafiky nemá žádnou pracovní plochu
+@msg the guide is hidden or locked vodítko je skryté nebo zamknuté
+@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them záhlaví `{cell}` pro `{name}` by se zpětně načetlo jako proměnná `{other}`: jednu z nich přejmenujte
+@msg the image `{path}` isn't followed: {r} obrázek `{path}` se nenačítá: {r}
+@msg the image has no embedded pixels obraz nemá žádné vložené obrazové body
+@msg the image is empty obraz je prázdný
+@msg the image is fully transparent obraz je zcela průhledný
+@msg the image is not on an artboard; pass rect obraz není na pracovní ploše; předejte rect
+@msg the image's pixels are missing chybí obrazové body obrazu
+@msg the image's pixels aren't in the document obrazové body obrazu nejsou v dokumentu
+@msg the image's pixels can't be read: {e} obrazové body obrazu nelze přečíst: {e}
+@msg the image's transparency doesn't fit it průhlednost obrazu k němu nepasuje
+@msg the job would print more than {MAX_PAGES} pages úloha by vytiskla více než {MAX_PAGES} stránek
+@msg the job would print more than {_1} pages úloha by vytiskla více než {_1} stránek
+@msg the layers hold no art to mask vrstvy neobsahují žádnou kresbu k maskování
+@msg the layout moved rozvržení se posunulo
+@msg the library has a group `{name}` already knihovna už má skupinu `{name}`
+@msg the library has no group `{name}` knihovna nemá skupinu `{name}`
+@msg the library has no item `{item}` knihovna nemá položku `{item}`
+@msg the library has no item `{other}` knihovna nemá položku `{other}`
+@msg the library holds {MAX_GROUPS} groups already knihovna už obsahuje {MAX_GROUPS} skupin
+@msg the library holds {MAX_ITEMS} items of this kind already knihovna už obsahuje {MAX_ITEMS} položek tohoto druhu
+@msg the library's file is outside the read roots soubor knihovny je mimo kořenové složky pro čtení
+@msg the line is too long, or the paths it passes too many, to check čára je příliš dlouhá, nebo prochází přes příliš mnoho cest, takže ji nelze zkontrolovat
+@msg the method needs an XML value metoda vyžaduje hodnotu XML
+@msg the name is empty název je prázdný
+@msg the object has no fill, stroke or effect objekt nemá výplň, tah ani efekt
+@msg the object has no graphic style (pass `name`) objekt nemá grafický styl (předejte `name`)
+@msg the objects don't match the plan objekty neodpovídají plánu
+@msg the open and permissions passwords must differ heslo pro otevření a heslo pro oprávnění se musí lišit
+@msg the operand stack overflowed zásobník operandů přetekl
+@msg the page has no area stránka nemá plochu
+@msg the page's type in an unread object's box isn't in its page art text stránky v rámečku nepřečteného objektu není v kresbě jeho stránky
+@msg the paint behind the proxy is not a gradient nátěr za aktivním políčkem není přechod
+@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) nátěr není volný přechod (použijte ho pomocí paint.editGradient {kind})
+@msg the parameters or body do not form one function parametry nebo tělo netvoří jednu funkci
+@msg the parent must be a layer or sublayer nadřazený prvek musí být vrstva nebo podvrstva
+@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it vložený filtr SVG {name} se vykresluje jinak než filtr dokumentu s tímto názvem a nehodí se pro něj žádný volný název
+@msg the path can't be extended cestu nelze prodloužit
+@msg the path can't be inside one of the objects to attach cesta nemůže být uvnitř některého z připojovaných objektů
+@msg the path has no segments to measure cesta nemá žádné segmenty k měření
+@msg the path is empty cesta je prázdná
+@msg the path is too short to shorten that much cesta je příliš krátká na takové zkrácení
+@msg the paths are too many or too tangled to find where they cross cest je příliš mnoho nebo jsou příliš zamotané, takže nelze najít, kde se kříží
+@msg the pen table holds more than 255 pens tabulka per obsahuje více než 255 per
+@msg the picture has no objects obrázek nemá žádné objekty
+@msg the plates can't be kept výtažky nelze zachovat
+@msg the position table does not match the instructions tabulka pozic neodpovídá instrukcím
+@msg the program runs too long program běží příliš dlouho
+@msg the program uses too much memory program používá příliš mnoho paměti
+@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes text výzvy je delší než {MAX_PROMPT_BYTES} bajtů
+@msg the radix must be between 2 and 36 základ soustavy musí být mezi 2 a 36
+@msg the rectangle must have a width and a height obdélník musí mít šířku a výšku
+@msg the rectangle needs a size and must lie on the canvas obdélník potřebuje velikost a musí ležet na plátně
+@msg the rectangle or line reaches too many paths to check obdélník nebo čára zasahuje příliš mnoho cest, takže je nelze zkontrolovat
+@msg the right side of 'in' is not an object pravá strana operátoru 'in' není objekt
+@msg the right side of 'instanceof' is not a function pravá strana operátoru 'instanceof' není funkce
+@msg the selected art has no width vybraná kresba nemá šířku
+@msg the selected art is nested more than {MAX_DEPTH} levels deep vybraná kresba je vnořena hlouběji než {MAX_DEPTH} úrovní
+@msg the selected art is too large to measure vybraná kresba je příliš velká na změření
+@msg the selection draws nothing výběr nic nevykresluje
+@msg the selection has no area výběr nemá plochu
+@msg the selection has no art to export výběr nemá žádnou kresbu k exportu
+@msg the selection has no bounds výběr nemá hranice
+@msg the selection has no graphic style výběr nemá grafický styl
+@msg the selection has no outlines to weed around výběr nemá žádné obrysy, kolem kterých by šlo vybírat
+@msg the slice would leave the canvas řez by opustil plátno
+@msg the spot colour couldn't be added přímou barvu nešlo přidat
+@msg the subpath has no ends to extend (it is closed or a single point) dílčí cesta nemá konce k prodloužení (je uzavřená nebo je to jediný bod)
+@msg the symbols are nested too deeply to expand their instances symboly jsou vnořeny příliš hluboko na rozložení jejich instancí
+@msg the text file is too large to place as type (at most {_1} MB) textový soubor je příliš velký na umístění jako text (nejvýše {_1} MB)
+@msg the text is empty text je prázdný
+@msg the text is over {_1} MB text má více než {_1} MB
+@msg the top object must be a path horní objekt musí být cesta
+@msg the trace was cancelled vektorizace byla zrušena
+@msg the trailer has no size zakončení nemá velikost
+@msg the type object is flattened to nothing textový objekt se sloučením změní v nic
+@msg the value bind was called on is not a function hodnota, na které byla zavolána bind, není funkce
+@msg the value is not a constructor hodnota není konstruktor
+@msg the value is not a function hodnota není funkce
+@msg the value is not a valid XML name hodnota není platný název XML
+@msg the value is not an XML attribute name hodnota není název atributu XML
+@msg the value is not an XML name hodnota není název XML
+@msg the value of axis `{tag}` must be a number or null hodnota osy `{tag}` musí být číslo nebo null
+@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets knihovna proměnných deklaruje více než {MAX_LIBRARY_DATASETS} sad dat
+@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities knihovna proměnných deklaruje více než {MAX_LIBRARY_ENTITIES} entit
+@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables knihovna proměnných deklaruje více než {MAX_LIBRARY_VARIABLES} proměnných
+@msg the variable library declares no variables knihovna proměnných nedeklaruje žádné proměnné
+@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes knihovna proměnných je větší než {MAX_LIBRARY_BYTES} bajtů
+@msg the variable library is not UTF-8 knihovna proměnných není v UTF-8
+@msg the variable library is not well-formed XML: {e} knihovna proměnných není správně utvořené XML: {e}
+@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep knihovna proměnných vnořuje prvky hlouběji než {MAX_LIBRARY_NESTING} úrovní
+@msg the variable library's document type declaration holds something other than entities of plain text deklarace typu dokumentu knihovny proměnných obsahuje něco jiného než entity s prostým textem
+@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes entity knihovny proměnných se rozvinou na více než {MAX_LIBRARY_BYTES} bajtů
+@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines linie pro vybírání kříží obrysy kresby tolikrát, že je nelze změřit; použijte méně linií
+@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap linie pro vybírání procházejí příliš blízko příliš mnoha segmentů obrysu, takže je nelze změřit; použijte méně linií nebo menší mezeru
+@msg the weeding lines would be more than {max_pieces} pieces linie pro vybírání by tvořily více než {max_pieces} dílů
+@msg the window has no dock okno nemá žádný dok
+@msg the written PDF has no catalog zapsané PDF nemá katalog
+@msg the written PDF has no cross-reference table zapsané PDF nemá tabulku křížových odkazů
+@msg the written PDF's metadata can't be edited metadata zapsaného PDF nelze upravit
+@msg the {_1} plate lies outside the file výtažek {_1} leží mimo soubor
+@msg there are no colour swatches to draw nejsou žádné barevné položky vzorníku k vykreslení
+@msg there is already a design named `{name}` návrh s názvem `{name}` už existuje
+@msg there is already a design named `{to}` návrh s názvem `{to}` už existuje
+@msg there is already a toolbar named `{name}` panel nástrojů s názvem `{name}` už existuje
+@msg there is already a toolbar named `{to}` panel nástrojů s názvem `{to}` už existuje
+@msg there is no library: create one first žádná knihovna neexistuje: nejprve ji vytvořte
+@msg these paths are too degenerate for Shaper tyto cesty jsou pro Shaper příliš degenerované
+@msg this is a binary DXF file, which can't be read: save it from the CAD app as an ASCII DXF and open that toto je binární soubor DXF, který nelze přečíst: uložte ho z aplikace CAD jako ASCII DXF a otevřete ten
+@msg this warp's points can't be edited: use Reset with Mesh first body této deformace nelze upravovat: nejprve použijte Obnovit se sítí
+@msg thumbnail: no page {v} (the {what} has {count}) miniatura: stránka {v} neexistuje ({what} jich má {count})
+@msg tileRange of {what}: {e} tileRange pro {what}: {e}
+@msg title can't be empty title nesmí být prázdné
+@msg to (the new name) is required to (nový název) je povinné
+@msg to must be fill or stroke to musí být fill nebo stroke
+@msg to must be smooth or corner to musí být smooth nebo corner
+@msg toExponential() digits must be between 0 and 20 počet číslic pro toExponential() musí být mezi 0 a 20
+@msg toFixed() digits must be between 0 and 20 počet číslic pro toFixed() musí být mezi 0 a 20
+@msg toISOString is not a function toISOString není funkce
+@msg toPrecision() argument must be between 1 and 21 argument toPrecision() musí být mezi 1 a 21
+@msg toSource: a getter cannot be written as source toSource: getter nelze zapsat jako zdrojový kód
+@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource: pole s vlastnostmi mimo své prvky, s vlastností, která není výčtová, nebo s jiným prototypem nelze zapsat jako zdrojový kód
+@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource: objekt s vlastností, která není výčtová, nebo jehož prototyp není Object.prototype, nelze zapsat jako zdrojový kód
+@msg toSource: the source is too long toSource: zdrojový kód je příliš dlouhý
+@msg toSource: the value contains itself toSource: hodnota obsahuje sama sebe
+@msg toSource: the value has too many entries toSource: hodnota má příliš mnoho položek
+@msg toSource: the value is nested too deep toSource: hodnota je vnořena příliš hluboko
+@msg toString is not a function toString není funkce
+@msg too large for a zip archive: package to a folder příliš velké pro archiv zip: zabalte do složky
+@msg too many Shaper edits příliš mnoho úprav Shaper
+@msg too many `points` (at most 100000) příliš mnoho `points` (nejvýše 100000)
+@msg too many arguments příliš mnoho argumentů
+@msg too many characters: at most {MAX_CHARS} příliš mnoho znaků: nejvýše {MAX_CHARS}
+@msg too many dots for this size: lower the frequency příliš mnoho bodů pro tuto velikost: snižte frekvenci
+@msg too many filters over one file příliš mnoho filtrů nad jedním souborem
+@msg too many overlapping paths for Shaper příliš mnoho překrývajících se cest pro Shaper
+@msg too many quantifiers příliš mnoho kvantifikátorů
+@msg too many saved clips příliš mnoho uložených ořezů
+@msg too many saved graphics states příliš mnoho uložených grafických stavů
+@msg toolbar (a toolbar's name) must be a string toolbar (název panelu nástrojů) musí být řetězec
+@msg toolbar name can't be empty název panelu nástrojů nesmí být prázdný
+@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v}: "flatten" (výchozí) nebo "omit"
+@msg two saved selections would be named `{_1}` dva uložené výběry by se jmenovaly `{_1}`
+@msg type dimension cannot be represented with this text transform rozměr textu nelze vyjádřit touto transformací textu
+@msg type dimension must be finite rozměr textu musí být konečný
+@msg unexpected resources neočekávané prostředky
+@msg unknown CMYK profile `{name}` neznámý profil CMYK `{name}`
+@msg unknown File Info field `{k}` neznámé pole informací o souboru `{k}`
+@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; prefix - to turn off; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take an alternate's number as `tag=N` with N from 0 to {_1}, as `salt=2`) neznámá funkce OpenType `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; předpona - funkci vypne; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom a aalt přijímají číslo alternativy jako `tag=N` s N od 0 do {_1}, například `salt=2`)
+@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) neznámá funkce OpenType `{t}` ve featureChanges (značky, které přijímá `features`)
+@msg unknown PDF preset `{name}` (presets: {_1}) neznámé přednastavení PDF `{name}` (přednastavení: {_1})
+@msg unknown Warp style neznámý styl deformace
+@msg unknown alignToPath `{a}` neznámé alignToPath `{a}`
+@msg unknown arrowhead {v} neznámá šipka {v}
+@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) neznámý klíč artboardLayout `{k}` (layout, columns, spacing, rightToLeft)
+@msg unknown attribute `{attribute}` neznámý atribut `{attribute}`
+@msg unknown attribute `{k}` neznámý atribut `{k}`
+@msg unknown axis `{other}` (vertical|horizontal|free) neznámá osa `{other}` (vertical|horizontal|free)
+@msg unknown blend mode `{b}` neznámý režim míchání `{b}`
+@msg unknown blend mode {b} neznámý režim míchání {b}
+@msg unknown brush type `{o}` neznámý typ štětce `{o}`
+@msg unknown category `{c}` neznámá kategorie `{c}`
+@msg unknown category `{c}` ({_1}) neznámá kategorie `{c}` ({_1})
+@msg unknown character `{c}` neznámý znak `{c}`
+@msg unknown character style number field neznámé číselné pole stylu znaků
+@msg unknown dither `{d}` (diffusion, pattern, noise or none) neznámý rozklad barev `{d}` (diffusion, pattern, noise nebo none)
+@msg unknown effect `{e}` neznámý efekt `{e}`
+@msg unknown end {e} neznámý konec {e}
+@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) neznámé flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution nebo uložené přednastavení: viz flattener.presets.list)
+@msg unknown format `{f}` (vcswatches, gpl, ase or css) neznámý formát `{f}` (vcswatches, gpl, ase nebo css)
+@msg unknown from {f} neznámé from {f}
+@msg unknown graph type `{t}` neznámý typ grafu `{t}`
+@msg unknown grid colours `{name}` (one of {_1}) neznámé barvy mřížky `{name}` (jedna z hodnot {_1})
+@msg unknown gridType `{t}` neznámé gridType `{t}`
+@msg unknown highlight `{v}` neznámé zvýraznění `{v}`
+@msg unknown intent `{i}` neznámý záměr `{i}`
+@msg unknown issue kind {id} neznámý druh problému {id}
+@msg unknown issue kind {v} neznámý druh problému {v}
+@msg unknown knockout state {k} neznámý stav vykrojení {k}
+@msg unknown language `{language}` (one of {_1}) neznámý jazyk `{language}` (jeden z {_1})
+@msg unknown language `{l}` (one of {_1}) neznámý jazyk `{l}` (jeden z {_1})
+@msg unknown method `{m}` ({_1}) neznámá metoda `{m}` ({_1})
+@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) neznámý režim `{other}` (gray, rgb, hsb, lab, cmyk nebo web)
+@msg unknown mode `{other}` (mono|cmyk) neznámý režim `{other}` (mono|cmyk)
+@msg unknown mode `{other}` (rgb|cmyk|gray|global) neznámý režim `{other}` (rgb|cmyk|gray|global)
+@msg unknown mode `{other}` (set, add, subtract) neznámý režim `{other}` (set, add, subtract)
+@msg unknown mode `{o}` neznámý režim `{o}`
+@msg unknown mode {m} neznámý režim {m}
+@msg unknown model `{other}` neznámý model `{other}`
+@msg unknown model `{other}` (keep, new or document) neznámý model `{other}` (keep, new nebo document)
+@msg unknown op {o} neznámé op {o}
+@msg unknown order `{o}`: toggle, front, forward, backward or back neznámé pořadí `{o}`: toggle, front, forward, backward nebo back
+@msg unknown overprints `{v}` (preserve, simulate or discard) neznámé overprints `{v}` (preserve, simulate nebo discard)
+@msg unknown panel neznámý panel
+@msg unknown panel `{panel}` neznámý panel `{panel}`
+@msg unknown parameter `{key}` neznámý parametr `{key}`
+@msg unknown parameter `{within}settings.{format}`; {why} neznámý parametr `{within}settings.{format}`; {why}
+@msg unknown parameter `{within}{key}`; {why} neznámý parametr `{within}{key}`; {why}
+@msg unknown pieLegend `{v}` (none, standard or wedges) neznámé pieLegend `{v}` (none, standard nebo wedges)
+@msg unknown piePosition `{v}` (even, ratio or stacked) neznámé piePosition `{v}` (even, ratio nebo stacked)
+@msg unknown pieSort `{v}` (none, all or first) neznámé pieSort `{v}` (none, all nebo first)
+@msg unknown plate `{n}` neznámý výtažek `{n}`
+@msg unknown preference `{k}` neznámá předvolba `{k}`
+@msg unknown profile {id} (see stroke.widthProfile.list) neznámý profil {id} (viz stroke.widthProfile.list)
+@msg unknown proof target `{t}` neznámý cíl nátisku `{t}`
+@msg unknown quotes key `{k}` (double, single) neznámý klíč quotes `{k}` (double, single)
+@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) neznámá redukce `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform nebo document)
+@msg unknown resolution `{v}` neznámé rozlišení `{v}`
+@msg unknown rule `{rule}` ({_1}) neznámé pravidlo `{rule}` ({_1})
+@msg unknown rule `{r}` (see color.harmony) neznámé pravidlo `{r}` (viz color.harmony)
+@msg unknown setting `{k}` neznámé nastavení `{k}`
+@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) neznámý tvar `{v}` (circle|ellipse|square|diamond|line)
+@msg unknown spacing `{o}` (smooth|steps|distance) neznámé rozestupy `{o}` (smooth|steps|distance)
+@msg unknown tileType `{t}` ({_1}) neznámé tileType `{t}` ({_1})
+@msg unknown units neznámé jednotky
+@msg unknown units `{u}` neznámé jednotky `{u}`
+@msg unknown value axis `{v}` (left, right or both) neznámá osa hodnot `{v}` (left, right nebo both)
+@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) neznámá variace `{v}` (tintsShades|warmCool|vividMuted)
+@msg unknown warp style `{s}` neznámý styl deformace `{s}`
+@msg unknown {key} `{v}` (none, short or full) neznámé {key} `{v}` (none, short nebo full)
+@msg unknown {key} profile `{n}` neznámý profil {key} `{n}`
+@msg unknown {k} key `{key}` (size, position) neznámý klíč {k} `{key}` (size, position)
+@msg unmatched ')' nespárovaná ')'
+@msg unreadable object nečitelný objekt
+@msg unsafe destination file name nebezpečný název cílového souboru
+@msg unterminated character class neukončená třída znaků
+@msg unterminated group neukončená skupina
+@msg use paths or compound paths použijte cesty nebo složené cesty
+@msg useArtboards must be true or false, not {v} useArtboards musí být true nebo false, ne {v}
+@msg value for `{var}` must match its variable kind hodnota pro `{var}` musí odpovídat druhu její proměnné
+@msg variable `{_1}` is already defined proměnná `{_1}` je už definována
+@msg variable `{name}` has no `trait` proměnná `{name}` nemá `trait`
+@msg variable `{name}` is already defined proměnná `{name}` je už definována
+@msg variable `{name}` is declared twice proměnná `{name}` je deklarována dvakrát
+@msg variable `{to}` already exists proměnná `{to}` už existuje
+@msg view is required view je povinné
+@msg view must be one of {_1} view musí být jedna z hodnot {_1}
+@msg warpStyle can only edit an applied Warp effect warpStyle může upravit jen použitý efekt Deformace
+@msg width and height must be more than 0 and at most {limit} width a height musí být více než 0 a nejvýše {limit}
+@msg width factors must be finite koeficienty šířky musí být konečné
+@msg width must be positive width musí být kladné
+@msg width out of range šířka je mimo rozsah
+@msg width/height must be > 0 width/height musí být > 0
+@msg wordGap and lineGap need area type in the selection wordGap a lineGap vyžadují ve výběru text v oblasti
+@msg x and y must be finite numbers x a y musí být konečná čísla
+@msg x and y must be given together x a y je nutné zadat společně
+@msg xpath needs its variables as an object xpath vyžaduje své proměnné jako objekt
+@msg xpath needs the expression as a string xpath vyžaduje výraz jako řetězec
+@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone vyžaduje kotvu: ukotvěte vedle panelu pomocí anchor, nebo do nového sloupce doku pomocí side (Start nebo End)
+@msg zoom must be positive zoom musí být kladné
+@msg {C}: no effect at index {list} {C}: na indexu {list} není žádný efekt
+@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET} je chráněné a zůstává
+@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET} je chráněné: uložte nastavení pod jiným názvem
+@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED} a jeho nastavení a mezipaměť nelze přesunout zpět z {_1} do {_2}: {e}
+@msg {LABEL} produced an empty result {LABEL} vytvořilo prázdný výsledek
+@msg {_1} MB of document, at most {_2} MB {_1} MB dokumentu, nejvýše {_2} MB
+@msg {_1} MB of images, at most {_2} MB {_1} MB obrázků, nejvýše {_2} MB
+@msg {_1} bytes of RGBA data for a {width} × {height} image {_1} bajtů dat RGBA pro obraz {width} × {height}
+@msg {_1} can't be read, or changed since it was read: relink or update it first {_1} nelze přečíst, nebo se od přečtení změnil: nejprve ho znovu propojte nebo aktualizujte
+@msg {_1} can't go into {into} {_1} nelze vložit do {into}
+@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1} nemůže omezit tisk na nízké rozlišení: zvolte PDF 1.4 nebo novější, nebo povolte tisk ve vysokém rozlišení
+@msg {_1} files can't be opened yet soubory {_1} zatím nelze otevřít
+@msg {_1} files can't be password-protected: remove the passwords or choose no standard soubory {_1} nelze chránit heslem: odeberte hesla nebo nezvolte žádný standard
+@msg {_1} files can't be {_2} soubory {_1} nemohou být {_2}
+@msg {_1} files can't carry editing data: turn off preserveEditing soubory {_1} nemohou obsahovat data pro úpravy: vypněte preserveEditing
+@msg {_1} files can't have PDF layers: turn off createLayers soubory {_1} nemohou mít vrstvy PDF: vypněte createLayers
+@msg {_1} files need a creation date, and this platform has no clock to give one soubory {_1} vyžadují datum vytvoření a tato platforma nemá hodiny, které by ho poskytly
+@msg {_1} is not a folder: {e} {_1} není složka: {e}
+@msg {_1} isn't a path {_1} není cesta
+@msg {_1} options: {e} volby {_1}: {e}
+@msg {_1} paints nothing to rasterize {_1} nevykresluje nic, co by šlo rastrovat
+@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) hesla {_1} mohou mít nejvýše {MAX_LEGACY_PASSWORD} znaků ASCII (PDF 1.7 a novější přijímají libovolné znaky)
+@msg {_1} with linked images or an image map writes several files: use document.export {_1} s propojenými obrázky nebo obrazovou mapou zapisuje několik souborů: použijte document.export
+@msg {_1} writes one file per artboard here: use document.export, or name one artboard {_1} zde zapisuje jeden soubor na každou pracovní plochu: použijte document.export, nebo uveďte jednu pracovní plochu
+@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} obrazových bodů je pro {_3} příliš mnoho (nejvýše {max} obrazových bodů na stranu): zmenšete měřítko
+@msg {_1}: belongs to another account {_1}: patří jinému účtu
+@msg {_1}: not a folder of its own {_1}: nejde o jeho vlastní složku
+@msg {_1}: not a plain file {_1}: nejde o obyčejný soubor
+@msg {_1}: {_2} and {_3} are in the way {_1}: překáží {_2} a {_3}
+@msg {code}: {0} {code}: {0}
+@msg {dest}: {e} {dest}: {e}
+@msg {dir}: {e} {dir}: {e}
+@msg {doc} has no folder {doc} nemá složku
+@msg {file} isn't linked {file} není propojený
+@msg {file} names more than one linked JavaScript file {file} uvádí více než jeden propojený soubor JavaScript
+@msg {folder} is not a package folder {folder} není složka balíčku
+@msg {folder}: more than {max} files and folders {folder}: více než {max} souborů a složek
+@msg {folder}: {e} {folder}: {e}
+@msg {g} is not a Live Paint group {g} není skupina živé malby
+@msg {id} is not a slice {id} není řez
+@msg {key} must be 0–{MAX_ADD_AROUND} pt {key} musí být 0–{MAX_ADD_AROUND} pt
+@msg {key} must be [x,y] {key} musí být [x,y]
+@msg {key} must be a percentage {min}..100 {key} musí být procento {min}..100
+@msg {key} must be a positive number, not {n} {key} musí být kladné číslo, ne {n}
+@msg {key} must be a positive number, not {v} {key} musí být kladné číslo, ne {v}
+@msg {key} must be one of {_1} {key} musí být jedna z hodnot {_1}
+@msg {key} must be {_1}, got {s} {key} musí být {_1}, zadáno {s}
+@msg {key} must name a floating panel {key} musí uvádět plovoucí panel
+@msg {key} must name a panel in another group {key} musí uvádět panel v jiné skupině
+@msg {key} must name a panel that shows {key} musí uvádět panel, který je zobrazen
+@msg {key} must name a registered panel {key} musí uvádět registrovaný panel
+@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) soubory {kind} nemají průhlednost a soubor by obsahoval {what}: nejprve ji slučte (object.flattenTransparency)
+@msg {kind} images can't be opened (see document.formats) obrázky {kind} nelze otevřít (viz document.formats)
+@msg {k} `{v}` is not one of the choices {k} `{v}` není jedna z nabízených voleb
+@msg {k} has control characters{_1} {k} obsahuje řídicí znaky{_1}
+@msg {k} is too long (at most {MAX_TEXT} characters) {k} je příliš dlouhé (nejvýše {MAX_TEXT} znaků)
+@msg {k} must be > 0 {k} musí být > 0
+@msg {k} must be a number (pt) or a length such as "210 mm" {k} musí být číslo (pt) nebo délka jako "210 mm"
+@msg {k} must be a number from {min} to {max} {k} musí být číslo od {min} do {max}
+@msg {k} must be a number or "auto" {k} musí být číslo nebo "auto"
+@msg {k} must be a string {k} musí být řetězec
+@msg {k} must be text {k} musí být text
+@msg {k} must be true or false {k} musí být true nebo false
+@msg {k} must be {{size?, position?}} {k} musí být {{size?, position?}}
+@msg {k}: a custom arrowhead's path needs an area to fill and at most {most} anchors and {most} subpaths, each anchor within {far} head units of its tip {k}: cesta vlastní šipky potřebuje plochu k vyplnění a nejvýše {most} kotevních bodů a {most} dílčích cest, každý kotevní bod nejdále {far} jednotek hrotu od její špičky
+@msg {k}: bad custom arrowhead path: {e} {k}: chybná cesta vlastní šipky: {e}
+@msg {k}: bad custom arrowhead: {e} {k}: chybná vlastní šipka: {e}
+@msg {k}: can't read `{s}` as a length {k}: `{s}` nelze přečíst jako délku
+@msg {k}: custom must be a name {k}: custom musí být název
+@msg {label} must be a number at most {max} {label} musí být číslo nejvýše {max}
+@msg {label} must be a number from -1296 to 1296 {label} musí být číslo od -1296 do 1296
+@msg {label} must be a number from 1 to 10000 {label} musí být číslo od 1 do 10000
+@msg {name} `{s}`: {ids} {name} `{s}`: {ids}
+@msg {name} is built in and can't be deleted {name} je vestavěné a nelze ho odstranit
+@msg {name} must be a number {name} musí být číslo
+@msg {name} must be {lo}–{hi}{unit} (got {v}) {name} musí být {lo}–{hi}{unit} (zadáno {v})
+@msg {name}: CRC or size mismatch {name}: nesouhlasí CRC nebo velikost
+@msg {name}: only this Affinity file's embedded preview could be read; use File › Open to see it with its warning, or export SVG or PDF from Affinity before placing {name}: z tohoto souboru Affinity šlo přečíst jen vložený náhled; pomocí Soubor › Otevřít ho zobrazíte s varováním, nebo před umístěním exportujte SVG či PDF z Affinity
+@msg {name}: {e} {name}: {e}
+@msg {native}; its embedded preview can't be read either ({e}) {native}; jeho vložený náhled také nelze přečíst ({e})
+@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}: copy přijímá soubor DCS, jen když jsou jeho výtažky uvnitř něj; propojte ho znovu tam, kde jsou soubory jeho výtažků
+@msg {path}: larger than {_1} MB {path}: větší než {_1} MB
+@msg {shown} is not an XPath 1.0 function {shown} není funkce XPath 1.0
+@msg {std} files are CMYK, and the file would have colours in {_1} soubory {std} jsou CMYK a soubor by obsahoval barvy v {_1}
+@msg {std} files embed their fonts, and the file would have one that isn't soubory {std} vkládají svá písma a soubor by obsahoval písmo, které vložené není
+@msg {target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway {target} je soubor, ze kterého byl tento dokument načten, a při čtení bylo něco vynecháno ({what}): přepsáním by se to nenávratně ztratilo. Zapište místo toho jiný soubor, nebo předejte acknowledgeLoss: true, chcete-li ho přesto nahradit
+@msg {ty} brushes need selected art (or params.{k}) štětce typu {ty} vyžadují vybranou kresbu (nebo params.{k})
+@msg {v} is not an asset id {v} není ID prvku
+@msg {v} names no layer or sublayer {v} neuvádí žádnou vrstvu ani podvrstvu
+@msg {what} can't be intertwined {what} nelze proplést
+@msg {what} doesn't fit the paper with its bleed and marks {what} se na papír se spadávkou a značkami nevejde
+@msg {what} is not a function {what} není funkce
+@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what} by vytisklo více než {MAX_COPIES_PER_SHEET} kopií na arch: zvětšete měřítko
+@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} by se vytisklo na více než {MAX_TILES} dlaždic: zmenšete měřítko
+@msg {what} {i} out of range {what} {i} je mimo rozsah
+@msg `{key}` must be from 0.01 to 1000 pt `{key}` musí být od 0.01 do 1000 pt
+@msg select the banner, or mark every artboard vyberte banner, nebo označte každou pracovní plochu
+@msg the banner has no width or height banner nemá šířku nebo výšku
+@msg the grommet inset is too large for the banner odstup průchodek od okraje je pro banner příliš velký
+@msg the hem is too deep for the banner lem je pro banner příliš hluboký
+@msg the spacing makes more than {MAX_GROMMETS} grommets při tomto rozestupu vznikne více než {MAX_GROMMETS} průchodek
+@msg there is no banner to mark není žádný banner k označení
+@msg turn on at least one side zapněte alespoň jednu stranu
+@msg with no hem and no grommets there is nothing to mark bez lemu a bez průchodek není co označit
+@msg {saving} failed: {e} {saving} se nezdařilo: {e}
diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv
index 433c6af3d6..5fad269299 100644
--- a/crates/ui-egui/src/i18n/da.tsv
+++ b/crates/ui-egui/src/i18n/da.tsv
@@ -924,6 +924,7 @@
VectorCraft Title Bar on Every Desktop VectorCrafts titellinje på alle skriveborde
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Miljøvariablen VECTORCRAFT_TITLEBAR vælger titellinjen, så længe den er angivet.
System Title Bar takes precedence over this option while it is on. Systemtitellinje har forrang for denne indstilling, så længe den er slået til.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Så længe VectorCrafts titellinje på alle skriveborde er slået fra, tegner dette skrivebord vinduets titellinje, uanset om denne indstilling er slået til eller fra.
Exit Afslut
Distort Forvræng
Diffuse Glow… Diffus glød…
@@ -4795,11 +4796,11 @@ imageTrace Outline Kontur
@msg unknown effect `{id}` ukendt effekt `{id}`
@msg unknown format ukendt format
@msg unknown format `{f}` (Save writes {_1}) ukendt format `{f}` (Gem skriver {_1})
-@msg unknown freeform mode `{m}` (points, lines) ukendt freeform-tilstand `{m}` (punkter, linjer)
-@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) ukendt gradienttype `{k}` (lineær, radial, konisk, diamant, freeform)
+@msg unknown freeform mode `{m}` (points, lines) ukendt freeform-tilstand `{m}` (points, lines)
+@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) ukendt gradienttype `{k}` (linear, radial, conic, diamond, freeform)
@msg unknown highlight `{h}` ukendt fremhævning `{h}`
@msg unknown option `{path}.{k}` ukendt indstilling `{path}.{k}`
-@msg unknown overprints `{o}` (preserve, simulate or discard) ukendt overprint `{o}` (bevar, simulér eller forkast)
+@msg unknown overprints `{o}` (preserve, simulate or discard) ukendt overprint `{o}` (preserve, simulate eller discard)
@msg unknown panel `{raw}` ukendt panel `{raw}`
@msg unknown preference `{key}` ukendt præference `{key}`
@msg Preferences imported. Præferencerne er importeret.
@@ -4817,10 +4818,10 @@ imageTrace Outline Kontur
@msg `{key}` must be an object `{key}` skal være et objekt
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` køres alene, ikke som et trin i en batch
@msg unknown preset (one of {_1}) ukendt forudindstilling (én af {_1})
-@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) ukendt forudindstilling `{name}` (høj, mellem, lav eller en gemt: se flattener.presets.list)
+@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) ukendt forudindstilling `{name}` (high, medium, low eller en gemt: se flattener.presets.list)
@msg unknown preset `{name}` (see flattener.presets.list) ukendt forudindstilling `{name}` (se flattener.presets.list)
@msg unknown preset `{name}` (see webExport.presets.list) ukendt forudindstilling `{name}` (se webExport.presets.list)
-@msg unknown side `{other}` (left|right|top|bottom) ukendt side `{other}` (venstre|højre|top|bund)
+@msg unknown side `{other}` (left|right|top|bottom) ukendt side `{other}` (left|right|top|bottom)
@msg unknown tool `{tool}` ukendt værktøj `{tool}`
@msg unknown tool `{t}` ukendt værktøj `{t}`
@msg unknown tool or command `{key}` ukendt værktøj eller kommando `{key}`
@@ -6073,6 +6074,8 @@ axis Y Axis Y-akse
Action Options… Indstillinger for handling…
Action Options Indstillinger for handling
Playback Options… Afspilningsindstillinger…
+ Button Mode Knaptilstand
+ No actions. Ingen handlinger.
Insert Stop… Indsæt stop…
Insert Stop Indsæt stop
Record Stop Optag stop
diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv
index 951f6f9c37..e33ed2a7a9 100644
--- a/crates/ui-egui/src/i18n/de.tsv
+++ b/crates/ui-egui/src/i18n/de.tsv
@@ -5219,6 +5219,7 @@ theme Appearance Erscheinungsbild
VectorCraft Title Bar on Every Desktop VectorCraft-Titelleiste auf jedem Desktop
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Solange die Umgebungsvariable VECTORCRAFT_TITLEBAR gesetzt ist, bestimmt sie die Titelleiste.
System Title Bar takes precedence over this option while it is on. Solange Systemtitelleiste eingeschaltet ist, hat sie Vorrang vor dieser Option.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Solange VectorCraft-Titelleiste auf jedem Desktop ausgeschaltet ist, zeichnet dieser Desktop die Titelleiste des Fensters, ob diese Option ein- oder ausgeschaltet ist.
# Brush Options: Scatter, Art and Pattern brushes.
Scatter Brush Options Optionen für Spezialpinsel
Art Brush Options Optionen für Bildpinsel
@@ -6192,6 +6193,8 @@ axis Y Axis Y-Achse
Action Options… Aktionsoptionen…
Action Options Aktionsoptionen
Playback Options… Wiedergabeoptionen…
+ Button Mode Schaltflächenmodus
+ No actions. Keine Aktionen.
Insert Stop… Stopp einfügen…
Insert Stop Stopp einfügen
Record Stop Stopp aufzeichnen
diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv
index 62e3adc491..9ff2eae9a2 100644
--- a/crates/ui-egui/src/i18n/el.tsv
+++ b/crates/ui-egui/src/i18n/el.tsv
@@ -5234,6 +5234,7 @@ digits Malayalam Μαλαγιάλαμ
VectorCraft Title Bar on Every Desktop Γραμμή τίτλου του VectorCraft σε κάθε επιφάνεια εργασίας
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Όσο είναι ορισμένη, η μεταβλητή περιβάλλοντος VECTORCRAFT_TITLEBAR επιλέγει τη γραμμή τίτλου.
System Title Bar takes precedence over this option while it is on. Όσο είναι ενεργή, η Γραμμή τίτλου συστήματος υπερισχύει αυτής της επιλογής.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Όσο η Γραμμή τίτλου του VectorCraft σε κάθε επιφάνεια εργασίας είναι ανενεργή, αυτή η επιφάνεια εργασίας σχεδιάζει τη γραμμή τίτλου του παραθύρου, είτε αυτή η επιλογή είναι ενεργή είτε όχι.
Value Axis Άξονας τιμών
On Left Side Στην αριστερή πλευρά
On Right Side Στη δεξιά πλευρά
@@ -6552,6 +6553,8 @@ axis Y Axis Άξονας Y
Action Options… Επιλογές ενέργειας…
Action Options Επιλογές ενέργειας
Playback Options… Επιλογές αναπαραγωγής…
+ Button Mode Λειτουργία κουμπιών
+ No actions. Δεν υπάρχουν ενέργειες.
Insert Stop… Εισαγωγή διακοπής…
Insert Stop Εισαγωγή διακοπής
Record Stop Καταγραφή διακοπής
diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv
index d1c6aa53af..e51ba240a8 100644
--- a/crates/ui-egui/src/i18n/es.tsv
+++ b/crates/ui-egui/src/i18n/es.tsv
@@ -5323,6 +5323,7 @@ axis Both Ambos
VectorCraft Title Bar on Every Desktop Barra de título de VectorCraft en todos los escritorios
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Mientras está definida, la variable de entorno VECTORCRAFT_TITLEBAR elige la barra de título.
System Title Bar takes precedence over this option while it is on. Mientras está activada, Barra de título del sistema tiene prioridad sobre esta opción.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Mientras Barra de título de VectorCraft en todos los escritorios está desactivada, este escritorio dibuja la barra de título de la ventana tanto si esta opción está activada como si no.
Value Axis Eje de valores
On Left Side En el lado izquierdo
On Right Side En el lado derecho
@@ -6543,6 +6544,8 @@ axis Y Axis Eje Y
Action Options… Opciones de acción…
Action Options Opciones de acción
Playback Options… Opciones de reproducción…
+ Button Mode Modo de botones
+ No actions. No hay acciones.
Insert Stop… Insertar parada…
Insert Stop Insertar parada
Record Stop Grabar parada
diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv
index 45e47fd355..36f4c9f5a9 100644
--- a/crates/ui-egui/src/i18n/fi.tsv
+++ b/crates/ui-egui/src/i18n/fi.tsv
@@ -985,6 +985,7 @@
VectorCraft Title Bar on Every Desktop VectorCraftin otsikkorivi kaikissa työpöytäympäristöissä
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Ympäristömuuttuja VECTORCRAFT_TITLEBAR valitsee otsikkorivin niin kauan kuin se on asetettu.
System Title Bar takes precedence over this option while it is on. Järjestelmän otsikkorivi ohittaa tämän asetuksen niin kauan kuin se on käytössä.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Niin kauan kuin VectorCraftin otsikkorivi kaikissa työpöytäympäristöissä ei ole käytössä, tämä työpöytäympäristö piirtää ikkunan otsikkorivin, oli tämä asetus käytössä tai ei.
Exit Lopeta
Distort Vääristä
Diffuse Glow… Hajautettu hehku…
@@ -6630,6 +6631,8 @@ axis Y Axis Y-akseli
Action Options… Toiminnon asetukset…
Action Options Toiminnon asetukset
Playback Options… Toistoasetukset…
+ Button Mode Painiketila
+ No actions. Ei toimintoja.
Insert Stop… Lisää pysäytys…
Insert Stop Lisää pysäytys
Record Stop Nauhoita pysäytys
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
index 057dce6872..a3d1c9b785 100644
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -5331,6 +5331,7 @@ effect Soft Doux
VectorCraft Title Bar on Every Desktop Barre de titre de VectorCraft sur tous les bureaux
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Tant qu’elle est définie, la variable d’environnement VECTORCRAFT_TITLEBAR choisit la barre de titre.
System Title Bar takes precedence over this option while it is on. Tant qu’elle est activée, Barre de titre du système a priorité sur cette option.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Tant que Barre de titre de VectorCraft sur tous les bureaux est désactivée, ce bureau dessine la barre de titre de la fenêtre, que cette option soit activée ou non.
Value Axis Axe des valeurs
On Left Side À gauche
On Right Side À droite
@@ -6595,6 +6596,8 @@ axis Y Axis Axe Y
Action Options… Options de script…
Action Options Options de script
Playback Options… Options d’exécution…
+ Button Mode Mode Bouton
+ No actions. Aucun script.
Insert Stop… Insérer un arrêt…
Insert Stop Insérer un arrêt
Record Stop Enregistrer un arrêt
diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv
index 9d056e085a..dff4c3503c 100644
--- a/crates/ui-egui/src/i18n/hi.tsv
+++ b/crates/ui-egui/src/i18n/hi.tsv
@@ -3560,6 +3560,8 @@ antiAlias Sharp शार्प
@msg the action set would be larger than 8 MiB एक्शन सेट 8 MiB से बड़ा हो जाएगा
Playback Options प्लेबैक ऑप्शन्स
Playback Options… प्लेबैक ऑप्शन्स…
+ Button Mode बटन मोड
+ No actions. कोई कार्रवाई नहीं।
Plug-in Info प्लगइन जानकारी
Plug-in… प्लग-इन…
Plug-ins प्लग-इन्स
@@ -4997,6 +4999,7 @@ antiAlias Sharp शार्प
VectorCraft Title Bar on Every Desktop हर डेस्कटॉप पर VectorCraft टाइटल बार
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. जब तक एनवायरनमेंट वेरिएबल VECTORCRAFT_TITLEBAR सेट है, टाइटल बार वही चुनता है।
System Title Bar takes precedence over this option while it is on. जब तक सिस्टम टाइटल बार चालू है, उसे इस विकल्प पर प्राथमिकता मिलती है।
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. जब तक हर डेस्कटॉप पर VectorCraft टाइटल बार बंद है, यह डेस्कटॉप विंडो का टाइटल बार बनाता है, चाहे यह विकल्प चालू हो या बंद।
Tab Tab
Tab Stops टैब स्टॉप्स
Table तालिका
@@ -5926,7 +5929,7 @@ antiAlias Sharp शार्प
@msg `{key}` is longer than {MAX_NAME} characters `{key}` {MAX_NAME} अक्षरों से अधिक लंबा है
@msg `{key}` is out of range `{key}` रेंज से बाहर है
@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` को प्रतिशत में [min, desired, max] या {{min?, desired?, max?}} होना चाहिए
-@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` होना चाहिए [x, y, चौड़ाई, ऊंचाई]: चार परिमित संख्या, चौड़ाई और ऊंचाई नकारात्मक नहीं है
+@msg `{key}` must be [x, y, width, height]: four finite numbers, width and height not negative `{key}` होना चाहिए [x, y, width, height]: चार परिमित संख्या, चौड़ाई और ऊंचाई नकारात्मक नहीं है
@msg `{key}` must be [x, y] `{key}` होना चाहिए [x, y]
@msg `{key}` must be `auto` or a language code such as `en` or `zh-hant` `{key}` को `auto` या `en` या `zh-hant` जैसे भाषा कोड होना चाहिए।
@msg `{key}` must be 0..{max} `{key}` होना चाहिए 0.{max}
@@ -6011,7 +6014,7 @@ antiAlias Sharp शार्प
@msg `a` and `b` must be two different paths `a` और `b` को दो अलग-अलग पाथ होना चाहिए
@msg `align` is start, center or end `align` शुरू, केंद्र या अंत है
@msg `align` must be "left", "center" or "right", not {other} `align` होना चाहिए "left", "center" या "right", नहीं {other}
-@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` को [subpath, एंकर] जोड़े की एक सारणी होना चाहिए
+@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` को [subpath, anchor] जोड़े की एक सारणी होना चाहिए
@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` में एंकर इंडेक्स (0, 1, 2…) की सूची होनी चाहिए, {k} नहीं
@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` में एंकर इंडेक्स (0, 1, 2…) की सूची होनी चाहिए, {v} नहीं
@msg `antiAlias` must be "none", "sharp", "crisp" or "strong" `antiAlias` "none", "sharp", "crisp" या "strong" होना चाहिए
@@ -6397,7 +6400,7 @@ antiAlias Sharp शार्प
@msg area type in frames too short for a line of the font it shows in got taller frames ({_1}): their type wouldn't show otherwise अपने फ़ॉन्ट की एक लाइन के लिए बहुत छोटे फ़्रेम वाले एरिया टाइप को लंबे फ़्रेम दिए गए ({_1}): वरना उनका टाइप नहीं दिखता
@msg area type needs a path that encloses an area एरिया टाइप को ऐसा पाथ चाहिए जो किसी क्षेत्र को घेरता हो
@msg arrowAlign must be extend|tip, got {o} arrowAlign को extend|tip होना चाहिए, {o} मिला
-@msg arrowScale must be [start, end] arrowScale होना चाहिए [शुरू, अंत]
+@msg arrowScale must be [start, end] arrowScale होना चाहिए [start, end]
@msg artboard {0} does not exist आर्टबोर्ड {0} मौजूद नहीं है
@msg artboard {i} doesn't print आर्टबोर्ड {i} प्रिंट नहीं करता है
@msg artboard is a 0-based index artboard 0 से शुरू होने वाला इंडेक्स है
@@ -6458,14 +6461,14 @@ antiAlias Sharp शार्प
@msg bad edit बुरा संपादन
@msg bad fill {v} बुरा भर {v}
@msg bad fit: {e} बुरा फिट: {e}
-@msg bad model {other} (keep, new or document) बुरा मॉडल {other} (keep, नया या डॉक्यूमेंट)
+@msg bad model {other} (keep, new or document) बुरा मॉडल {other} (keep, new या document)
@msg bad new colour {v} बुरा नया रंग {v}
@msg bad object बुरा ऑब्जेक्ट
@msg bad object number खराब ऑब्जेक्ट संख्या
@msg bad offset बुरा ऑफसेट
@msg bad page tree बुरा पेज पेड़
@msg bad path data: {e} बुरा पाथ डेटा: {e}
-@msg bad point {v}: expected [x, y] or [x, y, pressure] बुरा बिंदु {v}: उम्मीद [x, y] या [x, y, दबाव]
+@msg bad point {v}: expected [x, y] or [x, y, pressure] बुरा बिंदु {v}: उम्मीद [x, y] या [x, y, pressure]
@msg bad reference बुरा संदर्भ
@msg bad shortcut बुरा शॉर्टकट
@msg bad stream length खराब धारा की लंबाई
@@ -6995,7 +6998,7 @@ antiAlias Sharp शार्प
@msg Internal error: {msg} (please report this bug) आंतरिक त्रुटि: {msg} (कृपया इस बग की रिपोर्ट करें)
@msg invalid `color` अमान्य `color`
@msg invalid anchor index {v}: expected a non-negative integer अमान्य एंकर इंडेक्स {v}: एक गैर-नकारात्मक पूर्णांक की उम्मीद
-@msg invalid anchor reference {v}: expected [subpath, anchor] अमान्य एंकर संदर्भ {v}: उम्मीद [subpath, एंकर]
+@msg invalid anchor reference {v}: expected [subpath, anchor] अमान्य एंकर संदर्भ {v}: उम्मीद [subpath, anchor]
@msg invalid angle अमान्य कोण
@msg invalid brush: {e} अमान्य ब्रश: {e}
@msg invalid color अमान्य रंग
@@ -7172,7 +7175,7 @@ antiAlias Sharp शार्प
@msg missing `value` लापता `value`
@msg missing anchor लापता एंकर
@msg missing anchors लापता एंकर
-@msg missing anchors [[subpath, anchor]…] लापता एंकर [[subpath, एंकर]...]
+@msg missing anchors [[subpath, anchor]…] लापता एंकर [[subpath, anchor]...]
@msg missing array `{key}` लापता सरणी `{key}`
@msg missing case लापता मामला
@msg missing color लापता रंग
@@ -8384,7 +8387,7 @@ antiAlias Sharp शार्प
@msg Unembedded to {_1} एम्बेड हटाकर {_1} में रखा गया
@msg unexpected host trap होस्ट में अप्रत्याशित ट्रैप
@msg unexpected resources अप्रत्याशित संसाधन
-@msg unknown {k} key `{key}` (size, position) अज्ञात {k} कुंजी `{key}` (आकार, स्थिति)
+@msg unknown {k} key `{key}` (size, position) अज्ञात {k} कुंजी `{key}` (size, position)
@msg unknown {key} `{v}` (none, short or full) अज्ञात {key} `{v}` (none, short या full)
@msg unknown {key} profile `{n}` अज्ञात {key} प्रोफ़ाइल `{n}`
@msg unknown align `{other}` अज्ञात संरेखण `{other}`
@@ -8417,9 +8420,9 @@ antiAlias Sharp शार्प
@msg unknown format अज्ञात स्वरूप
@msg unknown format `{f}` (Save writes {_1}) अज्ञात फ़ॉर्मैट `{f}` (सेव {_1} लिखता है)
@msg unknown format `{f}` (vcswatches, gpl, ase or css) अज्ञात फ़ॉर्मैट `{f}` (vcswatches, gpl, ase या css)
-@msg unknown freeform mode `{m}` (points, lines) अज्ञात फ्रीफॉर्म मोड `{m}` (बिंदुओं, लाइनों)
+@msg unknown freeform mode `{m}` (points, lines) अज्ञात फ्रीफॉर्म मोड `{m}` (points, lines)
@msg unknown from {f} {f} से अज्ञात
-@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) अज्ञात ग्रेडिएंट `{k}` (रैखिक, रेडियल, शंक्वाकार, डायमंड, फ्रीफॉर्म)
+@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) अज्ञात ग्रेडिएंट `{k}` (linear, radial, conic, diamond, freeform)
@msg unknown graph type `{t}` अज्ञात ग्राफ प्रकार `{t}`
@msg unknown grid colours `{name}` (one of {_1}) अज्ञात ग्रिड रंग `{name}` ({_1} में से एक)
@msg unknown gridType `{t}` अज्ञात gridType `{t}`
@@ -8434,20 +8437,20 @@ antiAlias Sharp शार्प
@msg unknown method `{m}` ({_1}) अज्ञात विधि `{m}` ({_1})
@msg unknown mode {m} अज्ञात मोड {m}
@msg unknown mode `{o}` अज्ञात मोड `{o}`
-@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) अज्ञात मोड `{other}` (ग्रे, RGB, hsb, लैब, cmyk या वेब)
+@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) अज्ञात मोड `{other}` (gray, rgb, hsb, lab, cmyk या web)
@msg unknown mode `{other}` (mono|cmyk) अज्ञात मोड `{other}` (mono|cmyk)
@msg unknown mode `{other}` (rgb|cmyk|gray|global) अज्ञात मोड `{other}` (rgb|cmyk|gray|global)
-@msg unknown mode `{other}` (set, add, subtract) अज्ञात मोड `{other}` (सेट, ऐड, घटाना)
+@msg unknown mode `{other}` (set, add, subtract) अज्ञात मोड `{other}` (set, add, subtract)
@msg unknown model `{other}` अज्ञात मॉडल `{other}`
-@msg unknown model `{other}` (keep, new or document) अज्ञात मॉडल `{other}` (keep, नया या डॉक्यूमेंट)
+@msg unknown model `{other}` (keep, new or document) अज्ञात मॉडल `{other}` (keep, new या document)
@msg unknown op {o} अज्ञात op {o}
@msg unknown OpenType feature `{t}` अज्ञात ओपन टाइप सुविधा `{t}`
@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom; prefix - to turn off) अज्ञात OpenType फ़ीचर `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom; बंद करने के लिए आगे - लगाएँ)
@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) featureChanges में अज्ञात OpenType फ़ीचर `{t}` (वे टैग जो `features` लेता है)
@msg unknown option `{path}.{k}` अज्ञात ऑप्शन `{path}.{k}`
@msg unknown order `{o}`: toggle, front, forward, backward or back अज्ञात आदेश `{o}`: टॉगल, फ्रंट, फॉरवर्ड, बैकवर्ड या बैक
-@msg unknown overprints `{o}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{o}` (preserve, अनुकरण या discard)
-@msg unknown overprints `{v}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{v}` (प्ररक्षित, अनुकरण या त्यागपत्र)
+@msg unknown overprints `{o}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{o}` (preserve, simulate या discard)
+@msg unknown overprints `{v}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{v}` (preserve, simulate या discard)
@msg unknown panel अज्ञात पैनल
@msg unknown panel `{raw}` अज्ञात पैनल `{raw}`
@msg panels can't be shown in Presentation Mode प्रस्तुति मोड में पैनल्स नहीं दिखाए जा सकते
@@ -8462,7 +8465,7 @@ antiAlias Sharp शार्प
@msg `{path}` is nested too deeply `{path}` बहुत गहराई तक नेस्ट किया गया है
@msg unknown PDF preset `{name}` (presets: {_1}) अज्ञात PDF प्रीसेट `{name}` (presets: {_1})
@msg unknown pieLegend `{v}` (none, standard or wedges) अज्ञात pieLegend `{v}` (none, standard या wedges)
-@msg unknown piePosition `{v}` (even, ratio or stacked) अज्ञात piePosition `{v}` (यहां तक कि अनुपात या स्टैक्ड)
+@msg unknown piePosition `{v}` (even, ratio or stacked) अज्ञात piePosition `{v}` (even, ratio या stacked)
@msg unknown pieSort `{v}` (none, all or first) अज्ञात pieSort `{v}` (none, all या first)
@msg unknown plate `{n}` अज्ञात प्लेट `{n}`
@msg unknown preference `{k}` अज्ञात वरीयता `{k}`
@@ -8488,7 +8491,7 @@ antiAlias Sharp शार्प
@msg unknown preset `{name}`: not a built-in or saved preset (see imageTrace.presets) अज्ञात प्रीसेट `{name}`: एक अंतर्निहित या सेव किए गए प्रीसेट नहीं (imageTrace.presets देखें)
@msg unknown profile {id} (see stroke.widthProfile.list) अज्ञात प्रोफ़ाइल {id} (stroke.widthProfile.list देखें)
@msg unknown proof target `{t}` अज्ञात प्रमाण लक्ष्य `{t}`
-@msg unknown quotes key `{k}` (double, single) अज्ञात उद्धरण कुंजी `{k}` (डबल, सिंगल)
+@msg unknown quotes key `{k}` (double, single) अज्ञात उद्धरण कुंजी `{k}` (double, single)
@msg unknown resolution `{v}` अज्ञात रिज़ॉल्यूशन `{v}`
@msg unknown rule `{r}` (see color.harmony) अज्ञात नियम `{r}` (देखें color.harmony)
@msg unknown rule `{rule}` ({_1}) अज्ञात नियम `{rule}` ({_1})
@@ -8504,7 +8507,7 @@ antiAlias Sharp शार्प
@msg Unknown unit अज्ञात इकाई
@msg unknown units अज्ञात इकाइयों
@msg unknown units `{u}` अज्ञात इकाइयों `{u}`
-@msg unknown value axis `{v}` (left, right or both) अज्ञात मान अक्ष `{v}` (बाएं, दाएं या दोनों)
+@msg unknown value axis `{v}` (left, right or both) अज्ञात मान अक्ष `{v}` (left, right या both)
@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) अज्ञात variation `{v}` (tintsShades|warmCool|vividMuted)
@msg unknown Warp style अज्ञात ताना स्टाइल
@msg unknown warp style `{s}` अज्ञात ताना स्टाइल `{s}`
diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv
index c2f1a52cd9..8549bd5d63 100644
--- a/crates/ui-egui/src/i18n/hu.tsv
+++ b/crates/ui-egui/src/i18n/hu.tsv
@@ -4311,6 +4311,7 @@ mark-style Japanese Japán
VectorCraft Title Bar on Every Desktop A VectorCraft címsora minden asztali környezetben
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Amíg a VECTORCRAFT_TITLEBAR környezeti változó be van állítva, az választja ki a címsort.
System Title Bar takes precedence over this option while it is on. Amíg a Rendszer címsora be van kapcsolva, elsőbbséget élvez ezzel a beállítással szemben.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Amíg a VectorCraft címsora minden asztali környezetben beállítás ki van kapcsolva, ez az asztali környezet rajzolja az ablak címsorát, akár be van kapcsolva ez a beállítás, akár nem.
Tab Tabulátor
Tab Stops Tabulátorpontok
Table Táblázat
@@ -6583,6 +6584,8 @@ axis Y Axis Y tengely
Action Options… Művelet beállításai…
Action Options Művelet beállításai
Playback Options… Lejátszási beállítások…
+ Button Mode Gomb mód
+ No actions. Nincs művelet.
Insert Stop… Megállás beszúrása…
Insert Stop Megállás beszúrása
Record Stop Megállás rögzítése
diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv
index a36c629332..e27253376e 100644
--- a/crates/ui-egui/src/i18n/id.tsv
+++ b/crates/ui-egui/src/i18n/id.tsv
@@ -924,6 +924,7 @@
VectorCraft Title Bar on Every Desktop Bilah judul VectorCraft di setiap desktop
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Selama ditetapkan, variabel lingkungan VECTORCRAFT_TITLEBAR memilih bilah judul.
System Title Bar takes precedence over this option while it is on. Selama aktif, Bilah judul sistem lebih diutamakan daripada opsi ini.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Selama Bilah judul VectorCraft di setiap desktop nonaktif, desktop ini menggambar bilah judul jendela, baik opsi ini aktif maupun tidak.
Exit Keluar
Distort Distorsi
Diffuse Glow… Pendar difusi…
@@ -4795,11 +4796,11 @@ imageTrace Outline Kontur
@msg unknown effect `{id}` efek `{id}` tidak dikenal
@msg unknown format format tidak dikenal
@msg unknown format `{f}` (Save writes {_1}) format `{f}` tidak dikenal (Simpan menulis {_1})
-@msg unknown freeform mode `{m}` (points, lines) mode bentuk bebas `{m}` tidak dikenal (titik, garis)
-@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) jenis gradien `{k}` tidak dikenal (linear, radial, kerucut, wajik, bentuk bebas)
+@msg unknown freeform mode `{m}` (points, lines) mode bentuk bebas `{m}` tidak dikenal (points, lines)
+@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) jenis gradien `{k}` tidak dikenal (linear, radial, conic, diamond, freeform)
@msg unknown highlight `{h}` sorotan `{h}` tidak dikenal
@msg unknown option `{path}.{k}` opsi `{path}.{k}` tidak dikenal
-@msg unknown overprints `{o}` (preserve, simulate or discard) overprint `{o}` tidak dikenal (pertahankan, simulasikan, atau buang)
+@msg unknown overprints `{o}` (preserve, simulate or discard) overprint `{o}` tidak dikenal (preserve, simulate, atau discard)
@msg unknown panel `{raw}` panel `{raw}` tidak dikenal
@msg unknown preference `{key}` preferensi `{key}` tidak dikenal
@msg Preferences imported. Preferensi diimpor.
@@ -4817,10 +4818,10 @@ imageTrace Outline Kontur
@msg `{key}` must be an object `{key}` harus berupa objek
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` dijalankan sendiri, bukan sebagai langkah batch
@msg unknown preset (one of {_1}) preset tidak dikenal (salah satu dari {_1})
-@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) preset `{name}` tidak dikenal (tinggi, sedang, rendah, atau yang tersimpan: lihat flattener.presets.list)
+@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) preset `{name}` tidak dikenal (high, medium, low, atau yang tersimpan: lihat flattener.presets.list)
@msg unknown preset `{name}` (see flattener.presets.list) preset `{name}` tidak dikenal (lihat flattener.presets.list)
@msg unknown preset `{name}` (see webExport.presets.list) preset `{name}` tidak dikenal (lihat webExport.presets.list)
-@msg unknown side `{other}` (left|right|top|bottom) sisi `{other}` tidak dikenal (kiri|kanan|atas|bawah)
+@msg unknown side `{other}` (left|right|top|bottom) sisi `{other}` tidak dikenal (left|right|top|bottom)
@msg unknown tool `{tool}` alat `{tool}` tidak dikenal
@msg unknown tool `{t}` alat `{t}` tidak dikenal
@msg unknown tool or command `{key}` alat atau perintah `{key}` tidak dikenal
@@ -6073,6 +6074,8 @@ axis Y Axis Sumbu Y
Action Options… Opsi tindakan…
Action Options Opsi tindakan
Playback Options… Opsi pemutaran…
+ Button Mode Mode tombol
+ No actions. Tidak ada aksi.
Insert Stop… Sisipkan henti…
Insert Stop Sisipkan henti
Record Stop Rekam henti
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
index 0ac2816b07..2381051c9b 100644
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -5377,6 +5377,7 @@ effect Regular Normale
VectorCraft Title Bar on Every Desktop Barra del titolo di VectorCraft su ogni desktop
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Finché è impostata, la variabile d'ambiente VECTORCRAFT_TITLEBAR sceglie la barra del titolo.
System Title Bar takes precedence over this option while it is on. Finché è attiva, Barra del titolo di sistema ha la precedenza su questa opzione.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Finché Barra del titolo di VectorCraft su ogni desktop è disattivata, questo desktop disegna la barra del titolo della finestra, sia con questa opzione attiva sia disattivata.
Value Axis Asse dei valori
On Left Side Sul lato sinistro
On Right Side Sul lato destro
@@ -6226,6 +6227,8 @@ axis Y Axis Asse Y
Action Options… Opzioni azione…
Action Options Opzioni azione
Playback Options… Opzioni di riproduzione…
+ Button Mode Modalità pulsante
+ No actions. Nessuna azione.
Insert Stop… Inserisci interruzione…
Insert Stop Inserisci interruzione
Record Stop Registra interruzione
diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv
index 71de1ce91c..1458bf2aa1 100644
--- a/crates/ui-egui/src/i18n/ja.tsv
+++ b/crates/ui-egui/src/i18n/ja.tsv
@@ -5419,6 +5419,7 @@ effect Vertical 垂直
VectorCraft Title Bar on Every Desktop すべてのデスクトップで VectorCraft のタイトルバー
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. 環境変数 VECTORCRAFT_TITLEBAR が設定されている間は、この変数でタイトルバーが決まります。
System Title Bar takes precedence over this option while it is on. システムのタイトルバーがオンの間は、そちらがこのオプションより優先されます。
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. すべてのデスクトップで VectorCraft のタイトルバーがオフの間は、このオプションのオンとオフにかかわらず、このデスクトップがウィンドウのタイトルバーを描画します。
Value Axis 数値軸
On Left Side 左側
On Right Side 右側
@@ -6292,6 +6293,8 @@ axis Y Axis Y 軸
Action Options… アクションオプション…
Action Options アクションオプション
Playback Options… 再生オプション…
+ Button Mode ボタンモード
+ No actions. アクションがありません。
Insert Stop… 停止を挿入…
Insert Stop 停止を挿入
Record Stop 停止を記録
diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv
index 2cff51d94b..9105fe50e7 100644
--- a/crates/ui-egui/src/i18n/ka.tsv
+++ b/crates/ui-egui/src/i18n/ka.tsv
@@ -890,6 +890,7 @@
VectorCraft Title Bar on Every Desktop VectorCraft-ის სათაურის ზოლი ყველა სამუშაო მაგიდაზე
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. სანამ გარემოს ცვლადი VECTORCRAFT_TITLEBAR დაყენებულია, სათაურის ზოლს ის ირჩევს.
System Title Bar takes precedence over this option while it is on. სანამ სისტემის სათაურის ზოლი ჩართულია, მას ამ პარამეტრზე უპირატესობა აქვს.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. სანამ VectorCraft-ის სათაურის ზოლი ყველა სამუშაო მაგიდაზე გამორთულია, ეს სამუშაო მაგიდა ფანჯრის სათაურის ზოლს ხატავს, ეს პარამეტრი ჩართულია თუ გამორთული.
Exit გასვლა
Distort დეფორმაცია
Diffuse Glow… გაბნეული ნათება…
@@ -6551,6 +6552,8 @@ axis Y Axis Y ღერძი
Action Options… მოქმედების პარამეტრები…
Action Options მოქმედების პარამეტრები
Playback Options… შესრულების პარამეტრები…
+ Button Mode ღილაკების რეჟიმი
+ No actions. მოქმედებები არ არის.
Insert Stop… გაჩერების ჩასმა…
Insert Stop გაჩერების ჩასმა
Record Stop გაჩერების ჩაწერა
diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv
index 4715a413f4..fd75f983ce 100644
--- a/crates/ui-egui/src/i18n/ko.tsv
+++ b/crates/ui-egui/src/i18n/ko.tsv
@@ -5346,6 +5346,7 @@ effect Vertical 세로
VectorCraft Title Bar on Every Desktop 모든 데스크톱에서 VectorCraft 제목 표시줄
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. 환경 변수 VECTORCRAFT_TITLEBAR가 설정되어 있는 동안에는 이 변수가 제목 표시줄을 정합니다.
System Title Bar takes precedence over this option while it is on. 시스템 제목 표시줄이 켜져 있는 동안에는 그 설정이 이 옵션보다 우선합니다.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. 모든 데스크톱에서 VectorCraft 제목 표시줄이 꺼져 있는 동안에는 이 옵션을 켜든 끄든 이 데스크톱이 창의 제목 표시줄을 그립니다.
Value Axis 값 축
On Left Side 왼쪽
On Right Side 오른쪽
@@ -6187,6 +6188,8 @@ axis Y Axis Y축
Action Options… 액션 옵션…
Action Options 액션 옵션
Playback Options… 재생 옵션…
+ Button Mode 버튼 모드
+ No actions. 동작이 없습니다.
Insert Stop… 중지 삽입…
Insert Stop 중지 삽입
Record Stop 중지 기록
diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv
index 94957fb782..cd3aa43966 100644
--- a/crates/ui-egui/src/i18n/nl.tsv
+++ b/crates/ui-egui/src/i18n/nl.tsv
@@ -1441,6 +1441,7 @@
VectorCraft Title Bar on Every Desktop VectorCraft-titelbalk op elk bureaublad
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Zolang de omgevingsvariabele VECTORCRAFT_TITLEBAR is ingesteld, kiest die de titelbalk.
System Title Bar takes precedence over this option while it is on. Zolang Systeemtitelbalk is ingeschakeld, heeft die voorrang op deze optie.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Zolang VectorCraft-titelbalk op elk bureaublad is uitgeschakeld, tekent dit bureaublad de titelbalk van het venster, of deze optie nu aan of uit staat.
Exit Afsluiten
Distort Vervormen
Diffuse Glow… Diffuse gloed…
@@ -5312,11 +5313,11 @@ imageTrace Outline Contour
@msg unknown effect `{id}` onbekend effect `{id}`
@msg unknown format onbekende indeling
@msg unknown format `{f}` (Save writes {_1}) onbekende indeling `{f}` (Opslaan schrijft {_1})
-@msg unknown freeform mode `{m}` (points, lines) onbekende vrijvormmodus `{m}` (punten, lijnen)
-@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) onbekend verlooptype `{k}` (lineair, radiaal, conisch, ruit, vrijvorm)
+@msg unknown freeform mode `{m}` (points, lines) onbekende vrijvormmodus `{m}` (points, lines)
+@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) onbekend verlooptype `{k}` (linear, radial, conic, diamond, freeform)
@msg unknown highlight `{h}` onbekende markering `{h}`
@msg unknown option `{path}.{k}` onbekende optie `{path}.{k}`
-@msg unknown overprints `{o}` (preserve, simulate or discard) onbekende overdruk `{o}` (behouden, simuleren of verwerpen)
+@msg unknown overprints `{o}` (preserve, simulate or discard) onbekende overdruk `{o}` (preserve, simulate of discard)
@msg unknown panel `{raw}` onbekend paneel `{raw}`
@msg unknown preference `{key}` onbekende voorkeur `{key}`
@msg Preferences imported. Voorkeuren geïmporteerd.
@@ -5334,10 +5335,10 @@ imageTrace Outline Contour
@msg `{key}` must be an object `{key}` moet een object zijn
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` wordt los uitgevoerd, niet als stap van een batch
@msg unknown preset (one of {_1}) onbekende voorinstelling (één van {_1})
-@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) onbekende voorinstelling `{name}` (hoog, middel, laag of een opgeslagen voorinstelling: zie flattener.presets.list)
+@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) onbekende voorinstelling `{name}` (high, medium, low of een opgeslagen voorinstelling: zie flattener.presets.list)
@msg unknown preset `{name}` (see flattener.presets.list) onbekende voorinstelling `{name}` (zie flattener.presets.list)
@msg unknown preset `{name}` (see webExport.presets.list) onbekende voorinstelling `{name}` (zie webExport.presets.list)
-@msg unknown side `{other}` (left|right|top|bottom) onbekende zijde `{other}` (links|rechts|boven|onder)
+@msg unknown side `{other}` (left|right|top|bottom) onbekende zijde `{other}` (left|right|top|bottom)
@msg unknown tool `{tool}` onbekend gereedschap `{tool}`
@msg unknown tool `{t}` onbekend gereedschap `{t}`
@msg unknown tool or command `{key}` onbekend gereedschap of opdracht `{key}`
@@ -6695,6 +6696,8 @@ axis Y Axis Y-as
Action Options… Actieopties…
Action Options Actieopties
Playback Options… Afspeelopties…
+ Button Mode Knopmodus
+ No actions. Geen acties.
Insert Stop… Stop invoegen…
Insert Stop Stop invoegen
Record Stop Stop opnemen
diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv
index 48f2671c2d..5f274cef09 100644
--- a/crates/ui-egui/src/i18n/pl.tsv
+++ b/crates/ui-egui/src/i18n/pl.tsv
@@ -942,6 +942,7 @@
VectorCraft Title Bar on Every Desktop Pasek tytułu VectorCraft na każdym pulpicie
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Dopóki zmienna środowiskowa VECTORCRAFT_TITLEBAR jest ustawiona, to ona wybiera pasek tytułu.
System Title Bar takes precedence over this option while it is on. Dopóki Systemowy pasek tytułu jest włączony, ma pierwszeństwo przed tą opcją.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Dopóki Pasek tytułu VectorCraft na każdym pulpicie jest wyłączony, ten pulpit rysuje pasek tytułu okna niezależnie od tego, czy ta opcja jest włączona.
Exit Zakończ
Distort Zniekształcenie
Diffuse Glow… Rozproszony blask…
@@ -6455,6 +6456,8 @@ axis Y Axis Oś Y
Action Options… Opcje operacji…
Action Options Opcje operacji
Playback Options… Opcje odtwarzania…
+ Button Mode Tryb przycisków
+ No actions. Brak operacji.
Insert Stop… Wstaw zatrzymanie…
Insert Stop Wstaw zatrzymanie
Record Stop Nagraj zatrzymanie
diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv
index f6e54552d4..a9e56041f4 100644
--- a/crates/ui-egui/src/i18n/pt-br.tsv
+++ b/crates/ui-egui/src/i18n/pt-br.tsv
@@ -3490,6 +3490,7 @@ imageTrace Outline Contorno
VectorCraft Title Bar on Every Desktop Barra de título do VectorCraft em todas as áreas de trabalho
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Enquanto estiver definida, a variável de ambiente VECTORCRAFT_TITLEBAR escolhe a barra de título.
System Title Bar takes precedence over this option while it is on. Enquanto estiver ativada, Barra de título do sistema tem prioridade sobre esta opção.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Enquanto Barra de título do VectorCraft em todas as áreas de trabalho estiver desativada, esta área de trabalho desenha a barra de título da janela com esta opção ativada ou desativada.
Exit Sair
Distort Distorcer
Diffuse Glow… Brilho difuso…
@@ -6515,6 +6516,8 @@ axis Y Axis Eixo Y
Action Options… Opções da Ação…
Action Options Opções da Ação
Playback Options… Opções de Reprodução…
+ Button Mode Modo de Botão
+ No actions. Nenhuma ação.
Insert Stop… Inserir Parada…
Insert Stop Inserir Parada
Record Stop Gravar Parada
@@ -7641,7 +7644,7 @@ swatch Yellow Green Verde-amarelado
@msg `alignOn` must be a string `alignOn` deve ser uma string
@msg `align` is start, center or end `align` é start, center ou end
@msg `align` must be a string `align` deve ser uma string
-@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` deve ser uma matriz de pares [subcaminho, ponto de ancoragem]
+@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` deve ser uma matriz de pares [subpath, anchor]
@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` deve listar índices de pontos de ancoragem (0, 1, 2…), não {k}
@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` deve listar índices de pontos de ancoragem (0, 1, 2…), não {v}
@msg `answer` for this {kind} dialog must be one of: {_1} `answer` para esta caixa de diálogo {kind} deve ser um de: {_1}
@@ -7706,7 +7709,7 @@ swatch Yellow Green Verde-amarelado
@msg `fidelity` must be from {lo} to {hi} pt `fidelity` deve estar entre {lo} e {hi} pt
@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` deve ter de 1 a 1000 caminhos de arquivos de fonte encontrados pela última pesquisa de text.findFontFiles
@msg `focal` and `radius` cannot be used together `focal` e `radius` não podem ser usados juntos
-@msg `focal` must be an array of [subpath, anchor] pairs `focal` deve ser uma matriz de pares [subcaminho, ponto de ancoragem]
+@msg `focal` must be an array of [subpath, anchor] pairs `focal` deve ser uma matriz de pares [subpath, anchor]
@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` indica [{si}, {ai}], que não é um ponto de ancoragem do caminho
@msg `folder` must be an absolute folder path `folder` deve ser um caminho absoluto de pasta
@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` deve ser um objeto de tags de eixo e valores, como {"wght"}
@@ -8371,7 +8374,7 @@ swatch Yellow Green Verde-amarelado
@msg instruction {pc} reached with stack depths {seen} and {d} instrução {pc} alcançada com profundidades de pilha {seen} e {d}
@msg invalid `color` `color` inválido
@msg invalid anchor index {v}: expected a non-negative integer índice de ponto de ancoragem inválido {v}: esperado um inteiro não negativo
-@msg invalid anchor reference {v}: expected [subpath, anchor] referência de ponto de ancoragem inválida {v}: esperado [subcaminho, ponto de ancoragem]
+@msg invalid anchor reference {v}: expected [subpath, anchor] referência de ponto de ancoragem inválida {v}: esperado [subpath, anchor]
@msg invalid angle ângulo inválido
@msg invalid array length comprimento de matriz inválido
@msg invalid brush: {e} pincel inválido: {e}
@@ -8480,7 +8483,7 @@ swatch Yellow Green Verde-amarelado
@msg missing `{k}` falta `{k}`
@msg missing anchor falta o ponto de ancoragem
@msg missing anchors faltam os pontos de ancoragem
-@msg missing anchors [[subpath, anchor]…] faltam os pontos de ancoragem [[subcaminho, ponto de ancoragem]…]
+@msg missing anchors [[subpath, anchor]…] faltam os pontos de ancoragem [[subpath, anchor]…]
@msg missing array `{key}` falta a matriz `{key}`
@msg missing case falta case
@msg missing color falta a cor
diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv
index 8dc8336089..4ea3511c36 100644
--- a/crates/ui-egui/src/i18n/ru.tsv
+++ b/crates/ui-egui/src/i18n/ru.tsv
@@ -5362,6 +5362,7 @@ axis Both Оба
VectorCraft Title Bar on Every Desktop Строка заголовка VectorCraft на любом рабочем столе
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Пока переменная среды VECTORCRAFT_TITLEBAR задана, строку заголовка выбирает она.
System Title Bar takes precedence over this option while it is on. Пока включена «Системная строка заголовка», она имеет приоритет над этим параметром.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Пока «Строка заголовка VectorCraft на любом рабочем столе» выключена, этот рабочий стол рисует строку заголовка окна независимо от того, включён ли этот параметр.
Value Axis Ось значений
On Left Side Слева
On Right Side Справа
@@ -6211,6 +6212,8 @@ axis Y Axis Ось Y
Action Options… Параметры действия…
Action Options Параметры действия
Playback Options… Параметры воспроизведения…
+ Button Mode Режим кнопок
+ No actions. Нет действий.
Insert Stop… Вставить остановку…
Insert Stop Вставить остановку
Record Stop Записать остановку
diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv
index cfb9d9734b..9a0de40468 100644
--- a/crates/ui-egui/src/i18n/sv.tsv
+++ b/crates/ui-egui/src/i18n/sv.tsv
@@ -5235,6 +5235,7 @@ effect Soft Mjuk
VectorCraft Title Bar on Every Desktop VectorCrafts namnlist på alla skrivbord
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Miljövariabeln VECTORCRAFT_TITLEBAR väljer namnlisten så länge den är angiven.
System Title Bar takes precedence over this option while it is on. Systemets namnlist har företräde framför det här alternativet så länge den är på.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Så länge VectorCrafts namnlist på alla skrivbord är av ritar det här skrivbordet fönstrets namnlist, oavsett om det här alternativet är på eller av.
Value Axis Värdeaxel
On Left Side På vänster sida
On Right Side På höger sida
@@ -6580,6 +6581,8 @@ axis Y Axis Y-axel
Action Options… Alternativ för åtgärd…
Action Options Alternativ för åtgärd
Playback Options… Uppspelningsalternativ…
+ Button Mode Knappläge
+ No actions. Inga åtgärder.
Insert Stop… Infoga stopp…
Insert Stop Infoga stopp
Record Stop Spela in stopp
@@ -7545,7 +7548,7 @@ digits Hindi Hindi
@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` är längre än {MAX_TEXT} byte
@msg `{what}` must be text `{what}` måste vara text
@msg `align` is start, center or end `align` är start, center eller end
-@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` måste vara en array av par [delbana, ankarpunkt]
+@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` måste vara en array av par [subpath, anchor]
@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` måste lista ankarpunktsindex (0, 1, 2…), inte {k}
@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` måste lista ankarpunktsindex (0, 1, 2…), inte {v}
@msg `artboards` must be a list of artboard indices `artboards` måste vara en lista med ritytsindex
@@ -8070,7 +8073,7 @@ digits Hindi Hindi
@msg instruction budget instruktionsbudget
@msg invalid `color` ogiltig `color`
@msg invalid anchor index {v}: expected a non-negative integer ogiltigt ankarindex {v}: ett icke-negativt heltal förväntades
-@msg invalid anchor reference {v}: expected [subpath, anchor] ogiltig ankarreferens {v}: [delbana, ankare] förväntades
+@msg invalid anchor reference {v}: expected [subpath, anchor] ogiltig ankarreferens {v}: [subpath, anchor] förväntades
@msg invalid angle ogiltig vinkel
@msg invalid brush: {e} ogiltig pensel: {e}
@msg invalid color ogiltig färg
@@ -8159,7 +8162,7 @@ digits Hindi Hindi
@msg missing `value` `value` saknas
@msg missing anchor ankare saknas
@msg missing anchors ankare saknas
-@msg missing anchors [[subpath, anchor]…] ankare saknas [[delbana, ankare]…]
+@msg missing anchors [[subpath, anchor]…] ankare saknas [[subpath, anchor]…]
@msg missing array `{key}` arrayen `{key}` saknas
@msg missing case skiftläge saknas
@msg missing color färg saknas
diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs
index 705fc68db2..bb79a093ec 100644
--- a/crates/ui-egui/src/i18n/tests.rs
+++ b/crates/ui-egui/src/i18n/tests.rs
@@ -4030,6 +4030,87 @@ fn translated_messages_keep_the_command_ids_their_english_names() {
assert!(missing.is_empty(), "{} rows drop a command id their English names:\n{}", missing.len(), missing.join("\n"));
}
+/// Whether `word` is an ASCII name a command reads: a letter followed by letters, digits or
+/// underscores.
+fn is_ascii_name(word: &str) -> bool {
+ word.chars().next().is_some_and(|c| c.is_ascii_alphabetic()) && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
+}
+
+/// The field names and accepted values that `s` spells out of its backticked spans: each name of a
+/// comma-separated list in square brackets, such as `[x, y, width, height]` or
+/// `[[subpath, anchor]…]`; each `a|b|c` list of values, whole; and the values an `unknown …` or
+/// `bad model …` message lists in parentheses, such as `(none, standard or wedges)`, up to the first
+/// item that is not a single name, so `(high, medium, low or a saved one: see …)` gives `high`,
+/// `medium` and `low`, and `(or give its ids)` gives none.
+fn field_names_and_values(s: &str) -> Vec {
+ let outside = s.split('`').step_by(2).collect::>().join(" ");
+ let mut found = Vec::new();
+ for piece in outside.split('[').skip(1) {
+ let Some(end) = piece.find(']') else { continue };
+ let names: Vec<&str> = piece[..end].split(',').map(str::trim).collect();
+ if names.len() > 1 && names.iter().all(|name| is_ascii_name(name)) {
+ found.extend(names.into_iter().map(str::to_string));
+ }
+ }
+ let lists = outside.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '|'));
+ found.extend(lists.filter(|w| w.contains('|') && w.split('|').all(is_ascii_name)).map(str::to_string));
+ for (at, _) in outside.match_indices('(') {
+ let before = &outside[..at];
+ if !(holds_ascii_word(before, "unknown") || before.contains("bad model")) {
+ continue;
+ }
+ let Some(end) = outside[at..].find(')') else { continue };
+ let items = outside[at + 1..at + end].split(", ").flat_map(|item| item.split(" or "));
+ let values: Vec<&str> = items.map(str::trim).take_while(|item| is_ascii_name(item)).collect();
+ if values.len() > 1 {
+ found.extend(values.into_iter().map(str::to_string));
+ }
+ }
+ found
+}
+
+#[test]
+fn field_names_and_values_finds_bracketed_names_and_listed_values() {
+ assert_eq!(field_names_and_values("`{key}` must be [x, y, width, height]: four numbers"), ["x", "y", "width", "height"]);
+ assert_eq!(field_names_and_values("missing anchors [[subpath, anchor]…]"), ["subpath", "anchor"]);
+ assert_eq!(field_names_and_values("arrowScale must be [start, end]"), ["start", "end"]);
+ assert_eq!(field_names_and_values("unknown pieLegend {v} (none, standard or wedges)"), ["none", "standard", "wedges"]);
+ assert_eq!(field_names_and_values("unknown side `{other}` (left|right|top|bottom)"), ["left|right|top|bottom"]);
+ assert_eq!(field_names_and_values("bad model {other} (keep, new or document)"), ["keep", "new", "document"]);
+ assert_eq!(
+ field_names_and_values("unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list)"),
+ ["high", "medium", "low"]
+ );
+ assert_eq!(field_names_and_values("unknown object (or give its ids)"), Vec::::new());
+ assert_eq!(field_names_and_values("unknown color `{c}` (#rrggbb or a preset name)"), Vec::::new());
+ assert_eq!(field_names_and_values("unknown resolution (72 screen, 150 medium, 300 high)"), Vec::::new());
+ assert_eq!(field_names_and_values("the art is [1, 2] (left, right) and `[a, b]`"), Vec::::new());
+}
+
+/// A translated status or error message keeps each field name and accepted value its English text
+/// spells out (see [`field_names_and_values`]), as the English spells it: the message tells a user
+/// what to pass, and a command reads only the English names and values.
+#[test]
+fn translated_messages_keep_the_field_names_and_values_their_english_lists() {
+ let mut missing = Vec::new();
+ for l in &LANGUAGES {
+ let (entries, _) = parse_entries(l.source);
+ for (ctx, src, tr) in entries.iter().filter(|(ctx, _, _)| ctx == "@msg" || ctx == "@msgplural") {
+ let plural = ctx == "@msgplural";
+ let english = if plural { src.rsplit('|').next().unwrap_or(src) } else { src.as_str() };
+ let forms: Vec<&str> = if plural { tr.split('|').collect() } else { vec![tr.as_str()] };
+ for form in forms {
+ for name in field_names_and_values(english) {
+ if !holds_ascii_word(form, &name) {
+ missing.push(format!("{}: {name} in {src:?} -> {form:?}", l.code));
+ }
+ }
+ }
+ }
+ }
+ assert!(missing.is_empty(), "{} rows drop a field name or value their English lists:\n{}", missing.len(), missing.join("\n"));
+}
+
/// Languages whose catalogs translate status and error messages; [`untranslated_report`] lists
/// the messages each one lacks.
const COMPLETE_MESSAGES: &[&str] = &[
diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv
index 2b3e843ba3..a457467ef6 100644
--- a/crates/ui-egui/src/i18n/tr.tsv
+++ b/crates/ui-egui/src/i18n/tr.tsv
@@ -4702,11 +4702,11 @@ axis Both Her İkisi
@msg unknown effect `{id}` bilinmeyen etki `{id}`
@msg unknown format bilinmeyen format
@msg unknown format `{f}` (Save writes {_1}) bilinmeyen format `{f}` (Kaydet, {_1} yazar)
-@msg unknown freeform mode `{m}` (points, lines) bilinmeyen serbest biçimli mod `{m}` (noktalar, çizgiler)
-@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) bilinmeyen gradyan türü `{k}` (doğrusal, radyal, konik, elmas, serbest biçimli)
+@msg unknown freeform mode `{m}` (points, lines) bilinmeyen serbest biçimli mod `{m}` (points, lines)
+@msg unknown gradient kind `{k}` (linear, radial, conic, diamond, freeform) bilinmeyen gradyan türü `{k}` (linear, radial, conic, diamond, freeform)
@msg unknown highlight `{h}` bilinmeyen vurgu `{h}`
@msg unknown option `{path}.{k}` bilinmeyen seçenek `{path}.{k}`
-@msg unknown overprints `{o}` (preserve, simulate or discard) bilinmeyen üst baskılar `{o}` (koru, benzet veya at)
+@msg unknown overprints `{o}` (preserve, simulate or discard) bilinmeyen üst baskılar `{o}` (preserve, simulate veya discard)
@msg unknown panel `{raw}` bilinmeyen panel `{raw}`
@msg panels can't be shown in Presentation Mode Sunum Modunda paneller gösterilemez
@msg unknown preference `{key}` bilinmeyen tercih `{key}`
@@ -4725,11 +4725,11 @@ axis Both Her İkisi
@msg `{key}` must be an object `{key}` bir nesne olmalıdır
@msg `prefs.import` runs on its own, not as a batch step `prefs.import` tek başına çalışır, toplu iş adımı olarak değil
@msg unknown preset (one of {_1}) bilinmeyen ön ayar ({_1}'den biri)
-@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) bilinmeyen ön ayar `{name}` (yüksek, orta, düşük veya kaydedilmiş bir ayar: bkz. flattener.presets.list)
+@msg unknown preset `{name}` (high, medium, low or a saved one: see flattener.presets.list) bilinmeyen ön ayar `{name}` (high, medium, low veya kaydedilmiş bir ayar: bkz. flattener.presets.list)
@msg unknown preset `{name}` (see flattener.presets.list) bilinmeyen ön ayar `{name}` (bkz. flattener.presets.list)
@msg unknown preset `{name}` (see webExport.presets.list) bilinmeyen ön ayar `{name}` (bkz. webExport.presets.list)
@msg unknown preset `{name}`: not a built-in or saved preset (see imageTrace.presets) bilinmeyen ön ayar `{name}`: yerleşik ya da kayıtlı bir ön ayar değil (bkz. imageTrace.presets)
-@msg unknown side `{other}` (left|right|top|bottom) bilinmeyen taraf `{other}` (sol|sağ|üst|alt)
+@msg unknown side `{other}` (left|right|top|bottom) bilinmeyen taraf `{other}` (left|right|top|bottom)
@msg unknown tool `{tool}` bilinmeyen araç `{tool}`
@msg unknown tool `{t}` bilinmeyen araç `{t}`
@msg unknown tool or command `{key}` bilinmeyen araç veya komut `{key}`
@@ -5038,6 +5038,7 @@ effect Soft Yumuşak
VectorCraft Title Bar on Every Desktop Her Masaüstünde VectorCraft Başlık Çubuğu
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. VECTORCRAFT_TITLEBAR ortam değişkeni ayarlı olduğu sürece başlık çubuğunu o seçer.
System Title Bar takes precedence over this option while it is on. Sistem Başlık Çubuğu açık olduğu sürece bu seçeneğe göre önceliklidir.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Her Masaüstünde VectorCraft Başlık Çubuğu kapalı olduğu sürece, bu seçenek açık da olsa kapalı da olsa pencerenin başlık çubuğunu bu masaüstü çizer.
Value Axis Değer Ekseni
On Left Side Sol Tarafta
On Right Side Sağ Tarafta
@@ -6556,6 +6557,8 @@ axis Y Axis Y ekseni
Action Options… Eylem Seçenekleri…
Action Options Eylem Seçenekleri
Playback Options… Oynatma Seçenekleri…
+ Button Mode Düğme Modu
+ No actions. Eylem yok.
Insert Stop… Durak Ekle…
Insert Stop Durak Ekle
Record Stop Durak Kaydet
diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv
index 50690fa0c7..18a118822f 100644
--- a/crates/ui-egui/src/i18n/uk.tsv
+++ b/crates/ui-egui/src/i18n/uk.tsv
@@ -5359,6 +5359,7 @@ axis Both Обидві
VectorCraft Title Bar on Every Desktop Рядок заголовка VectorCraft на будь-якій стільниці
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Поки змінну середовища VECTORCRAFT_TITLEBAR задано, рядок заголовка вибирає вона.
System Title Bar takes precedence over this option while it is on. Поки ввімкнено «Системний рядок заголовка», він має пріоритет над цим параметром.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Поки «Рядок заголовка VectorCraft на будь-якій стільниці» вимкнено, ця стільниця малює рядок заголовка вікна незалежно від того, чи ввімкнено цей параметр.
Value Axis Вісь значень
On Left Side Ліворуч
On Right Side Праворуч
@@ -6206,6 +6207,8 @@ axis Y Axis Вісь Y
Action Options… Параметри дії…
Action Options Параметри дії
Playback Options… Параметри відтворення…
+ Button Mode Режим кнопок
+ No actions. Нема дій.
Insert Stop… Вставити зупинку…
Insert Stop Вставити зупинку
Record Stop Записати зупинку
diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv
index d8469bce53..db36624af7 100644
--- a/crates/ui-egui/src/i18n/vi.tsv
+++ b/crates/ui-egui/src/i18n/vi.tsv
@@ -4036,6 +4036,7 @@ mark-style Japanese Kiểu Nhật
VectorCraft Title Bar on Every Desktop Thanh tiêu đề VectorCraft trên mọi màn hình nền
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. Khi biến môi trường VECTORCRAFT_TITLEBAR được đặt, biến này chọn thanh tiêu đề.
System Title Bar takes precedence over this option while it is on. Khi Thanh tiêu đề hệ thống được bật, tùy chọn đó được ưu tiên hơn tùy chọn này.
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. Khi Thanh tiêu đề VectorCraft trên mọi màn hình nền đang tắt, màn hình nền này vẽ thanh tiêu đề của cửa sổ dù tùy chọn này bật hay tắt.
Tab Tab
Tab Stops Vị trí tab
Table Bảng
@@ -6207,6 +6208,8 @@ axis Y Axis Trục Y
Action Options… Tùy chọn hành động…
Action Options Tùy chọn hành động
Playback Options… Tùy chọn phát lại…
+ Button Mode Chế độ nút
+ No actions. Không có hành động.
Insert Stop… Chèn điểm dừng…
Insert Stop Chèn điểm dừng
Record Stop Ghi điểm dừng
diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv
index d9d9110276..0b0639e602 100644
--- a/crates/ui-egui/src/i18n/zh-hans.tsv
+++ b/crates/ui-egui/src/i18n/zh-hans.tsv
@@ -5659,6 +5659,7 @@ effect Vertical 垂直
VectorCraft Title Bar on Every Desktop 在所有桌面上使用 VectorCraft 标题栏
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. 设置了环境变量 VECTORCRAFT_TITLEBAR 时,由它选择标题栏。
System Title Bar takes precedence over this option while it is on. 开启系统标题栏时,它优先于此选项。
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. 未开启“在所有桌面上使用 VectorCraft 标题栏”时,无论此选项是否开启,此桌面都会绘制窗口的标题栏。
Value Axis 数值轴
On Left Side 左侧
On Right Side 右侧
@@ -6439,6 +6440,8 @@ axis Y Axis Y 轴
Action Options… 动作选项…
Action Options 动作选项
Playback Options… 播放选项…
+ Button Mode 按钮模式
+ No actions. 没有动作。
Insert Stop… 插入停止…
Insert Stop 插入停止
Record Stop 记录停止
diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv
index a0d987fe69..e72c0d1a0d 100644
--- a/crates/ui-egui/src/i18n/zh-hant.tsv
+++ b/crates/ui-egui/src/i18n/zh-hant.tsv
@@ -5493,6 +5493,7 @@ effect Soft 柔和
VectorCraft Title Bar on Every Desktop 在所有桌面上使用 VectorCraft 標題列
The VECTORCRAFT_TITLEBAR environment variable chooses the title bar while it is set. 設定了環境變數 VECTORCRAFT_TITLEBAR 時,由它選擇標題列。
System Title Bar takes precedence over this option while it is on. 開啟系統標題列時,它優先於此選項。
+ This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off. 未開啟「在所有桌面上使用 VectorCraft 標題列」時,無論此選項是否開啟,此桌面都會繪製視窗的標題列。
Value Axis 數值軸
On Left Side 左側
On Right Side 右側
@@ -6245,6 +6246,8 @@ axis Y Axis Y 軸
Action Options… 動作選項…
Action Options 動作選項
Playback Options… 播放選項…
+ Button Mode 按鈕模式
+ No actions. 沒有動作。
Insert Stop… 插入停止…
Insert Stop 插入停止
Record Stop 記錄停止
@@ -7138,3 +7141,1818 @@ breakText Glyphs 字符
Grommets 扣眼
Grommet Inset 扣眼邊距
Diameter 直徑
+
+# Status and error messages of commands, scripts, file import and export, and plug-ins.
+@msg 'caller', 'callee' and 'arguments' cannot be used on strict mode functions 嚴格模式的函式不能使用 'caller'、'callee' 和 'arguments'
+@msg Alignment must be one of the choices 對齊方式必須是選項之一
+@msg Boolean.prototype.{name} was called on a value that is not a Boolean 在不是 Boolean 的值上呼叫了 Boolean.prototype.{name}
+@msg CMYK images whose samples couldn't be read in CMYK were imported in RGB 無法以 CMYK 讀取取樣的 CMYK 影像已以 RGB 匯入
+@msg Cannot write to the location of “{name}”; choose where to save it 無法寫入「{name}」的位置;請選擇儲存位置
+@msg DCS files can only be placed linked: use File › Place with Link on DCS 檔案只能以連結方式置入:請使用「檔案 › 置入」並開啟「連結」
+@msg Date.prototype method called on a value that is not a Date object 在不是 Date 物件的值上呼叫了 Date.prototype 方法
+@msg Error.prototype.toString was called on a value that is not an object 在不是物件的值上呼叫了 Error.prototype.toString
+@msg Function.prototype.toString was called on a value that is not a function 在不是函式的值上呼叫了 Function.prototype.toString
+@msg Import SVG Filter reads SVG files, not SVGZ 「匯入 SVG 濾鏡」讀取 SVG 檔案,不讀取 SVGZ
+@msg JSON.parse: {why} (at offset {at}) JSON.parse:{why} (位於偏移 {at})
+@msg JSON.stringify cannot write a structure that contains itself JSON.stringify 無法寫入包含自身的結構
+@msg Line Gap must be a number 行間距必須是數字
+@msg Namespace.prototype.toString needs a Namespace Namespace.prototype.toString 需要 Namespace
+@msg New Document profiles need the preferences folder, which this run doesn't have 新增文件描述檔需要偏好設定資料夾,而這次執行沒有該資料夾
+@msg Number.prototype.{name} was called on a value that is not a number 在不是數字的值上呼叫了 Number.prototype.{name}
+@msg Object.create's prototype must be an object or null Object.create 的原型必須是物件或 null
+@msg Object.{function} was given a value that is not an object Object.{function} 收到了不是物件的值
+@msg QName.prototype.toString needs a QName QName.prototype.toString 需要 QName
+@msg RegExp.prototype.{name} was called on a value that is not a RegExp object 在不是 RegExp 物件的值上呼叫了 RegExp.prototype.{name}
+@msg Stencil Bridges: the selected art is too large or too detailed to bridge 鏤空版連接橋:所選圖稿過大或過於細緻,無法建立連接橋
+@msg String.prototype.toString was called on a value that is not a string 在不是字串的值上呼叫了 String.prototype.toString
+@msg The {name} profile could not be read, so the new document has the default settings: {reason} 無法讀取 {name} 描述檔,因此新文件使用預設設定:{reason}
+@msg Trap: more than {_1} pairs of objects lie close enough to trap, or too many objects share a horizontal range; trap fewer objects at a time 補漏白:有超過 {_1} 對物件近到需要補漏白,或有太多物件位於相同的水平範圍;請一次對較少的物件補漏白
+@msg Weed Box and Lines: no weeding line fits outside the art, and the weed box is off 除廢框與除廢線:圖稿外側放不下任何除廢線,而且除廢框已關閉
+@msg \ at end of pattern \ 出現在模式結尾
+@msg `alignOn` cannot be empty `alignOn` 不能為空
+@msg `alignOn` must be a string `alignOn` 必須是字串
+@msg `align` must be a string `align` 必須是字串
+@msg `answer` for this {kind} dialog must be one of: {_1} 此 {kind} 對話框的 `answer` 必須是下列其中之一:{_1}
+@msg `answers` has more than {MAX_ANSWERS} answers `answers` 的答案超過 {MAX_ANSWERS} 個
+@msg `answers` is larger than 1 MiB `answers` 大於 1 MiB
+@msg `answers` must be a list of answers `answers` 必須是答案清單
+@msg `artboard` must be the index of an artboard of the document `artboard` 必須是文件中某個工作區域的索引
+@msg `artboardsAt` must be [x, y] `artboardsAt` 必須是 [x, y]
+@msg `artboards` names an artboard the document does not have `artboards` 指定了文件中沒有的工作區域
+@msg `artboards` names an artboard twice `artboards` 將同一個工作區域指定了兩次
+@msg `at` must be [x, y] (got {at}) `at` 必須是 [x, y] (收到 {at})
+@msg `at` must be a point [x, y] `at` 必須是點 [x, y]
+@msg `beside` needs `side` `beside` 需要 `side`
+@msg `code` must be a whole number `code` 必須是整數
+@msg `colorMode` must be one of rgb, cmyk `colorMode` 必須是 rgb、cmyk 其中之一
+@msg `colors` does not apply to the {name} reduction `colors` 不適用於 {name} 縮減
+@msg `colors` must be a whole number from 2 to 256 `colors` 必須是 2 到 256 之間的整數
+@msg `colors` must be at least 9 for the uniform reduction 使用均勻縮減時,`colors` 至少必須是 9
+@msg `colors` must be between 1 and {MAX_THEME_COLORS} (got {n}) `colors` 必須介於 1 和 {MAX_THEME_COLORS} 之間 (收到 {n})
+@msg `count` must be a whole number from 0 to {MAX_COUNT} `count` 必須是 0 到 {MAX_COUNT} 之間的整數
+@msg `ditherAmount` must be a number from 0 to 100 `ditherAmount` 必須是 0 到 100 之間的數字
+@msg `fidelity` must be from {lo} to {hi} pt `fidelity` 必須介於 {lo} 到 {hi} pt 之間
+@msg `focal` and `radius` cannot be used together `focal` 和 `radius` 不能同時使用
+@msg `focal` must be an array of [subpath, anchor] pairs `focal` 必須是 [subpath, anchor] 配對的陣列
+@msg `focal` names [{si}, {ai}], which is not an anchor of the path `focal` 指定了 [{si}, {ai}],它不是該路徑的錨點
+@msg `glyphAlternate` takes only null, which takes a glyph choice away (text.setGlyphAlternate makes one) `glyphAlternate` 只接受 null,表示移除字符選擇 (text.setGlyphAlternate 可建立選擇)
+@msg `group` must be one of {_1} `group` 必須是 {_1} 其中之一
+@msg `group` must be the Objects on Path group of `member` and `target` `group` 必須是 `member` 和 `target` 所屬的「路徑上的物件」群組
+@msg `id` is an object id, not {v} `id` 是物件 ID,而非 {v}
+@msg `item` needs `index` `item` 需要 `index`
+@msg `kerningBefore` belongs to a line's first character, not to a character style (text.setRangeStyle sets it) `kerningBefore` 屬於一行的第一個字元,不屬於字元樣式 (由 text.setRangeStyle 設定)
+@msg `kinsokuType` must be "pushInFirst", "pushOutFirst" or "pushOutOnly" `kinsokuType` 必須是 "pushInFirst"、"pushOutFirst" 或 "pushOutOnly"
+@msg `leader` must be a string `leader` 必須是字串
+@msg `max` must be 1 to {MAX_SUGGESTIONS} `max` 必須是 1 到 {MAX_SUGGESTIONS}
+@msg `name` must be a name that is not empty `name` 必須是不為空的名稱
+@msg `name` must not be empty `name` 不能為空
+@msg `origin` must be [x, y] in points `origin` 必須是以點為單位的 [x, y]
+@msg `picks` must be an array (got {v}) `picks` 必須是陣列 (收到 {v})
+@msg `picks` must hold 1 to {MAX_PICKS} picks (got {_1}) `picks` 必須包含 1 到 {MAX_PICKS} 個選擇 (收到 {_1})
+@msg `points` must hold from 2 to 100000 points `points` 必須包含 2 到 100000 個點
+@msg `radius` must be 0 or more `radius` 必須大於或等於 0
+@msg `rect` must be [x, y, width, height] `rect` 必須是 [x, y, width, height]
+@msg `rect` must be [x0, y0, x1, y1] (got {r}) `rect` 必須是 [x0, y0, x1, y1] (收到 {r})
+@msg `replace` must be a string `replace` 必須是字串
+@msg `rows` must be lists of numbers or nulls, not {r} `rows` 必須是由數字或 null 組成的清單,而非 {r}
+@msg `scale` must be a number from 0.001 to 1000 `scale` 必須是 0.001 到 1000 之間的數字
+@msg `scope.{key}` must be a non-empty array `scope.{key}` 必須是非空陣列
+@msg `scope` must be "document", {artboards} or {layers} `scope` 必須是 "document"、{artboards} 或 {layers}
+@msg `side` must be "left", "right", "top" or "bottom" `side` 必須是 "left"、"right"、"top" 或 "bottom"
+@msg `side` places the artboard: leave out x, y, width and height `side` 會決定工作區域的位置:請不要提供 x、y、width 和 height
+@msg `source` is the only path in series group `target` `source` 是系列群組 `target` 中唯一的路徑
+@msg `spacing` makes more than {MAX_COUNT} lines across the box `spacing` 會在方框內產生超過 {MAX_COUNT} 條線
+@msg `spacing` must be more than 0 and at most {MAX_DISTANCE} pt `spacing` 必須大於 0 且不超過 {MAX_DISTANCE} pt
+@msg `target` must be an object of the same Objects on Path group as `member` `target` 必須是與 `member` 屬於同一個「路徑上的物件」群組的物件
+@msg `tolerance` must be from 0 to 100 pt `tolerance` 必須介於 0 到 100 pt 之間
+@msg `version` must be 0 or {_1} `version` 必須是 0 或 {_1}
+@msg `version` must be 0 or {newest} `version` 必須是 0 或 {newest}
+@msg `warichuAlign` must be one of {_1} `warichuAlign` 必須是 {_1} 其中之一
+@msg `warichuLines` must be a whole number from {_1} to {_2} `warichuLines` 必須是 {_1} 到 {_2} 之間的整數
+@msg `weight` must be from 0.01 to 1000 pt `weight` 必須介於 0.01 到 1000 pt 之間
+@msg `weights` must be a list of {_1} numbers, one per row (got {v}) `weights` 必須是包含 {_1} 個數字的清單,每列一個 (收到 {v})
+@msg `xml` must be a string `xml` 必須是字串
+@msg `{KEY}` entry {n} gives the paragraph at byte {start} of {_1} other indents than an entry before it `{KEY}` 項目 {n} 為 {_1} 中位於位元組 {start} 的段落指定的縮排與先前的項目不同
+@msg `{KEY}` entry {n} must be an object `{KEY}` 項目 {n} 必須是物件
+@msg `{KEY}` entry {n} sets neither `leftIndent` nor `firstLineIndent` `{KEY}` 項目 {n} 既未設定 `leftIndent`,也未設定 `firstLineIndent`
+@msg `{KEY}` entry {n}: `id` must be an object id `{KEY}` 項目 {n}:`id` 必須是物件 ID
+@msg `{KEY}` entry {n}: `start` must be a byte offset `{KEY}` 項目 {n}:`start` 必須是位元組偏移
+@msg `{KEY}` entry {n}: `{k}` must be a number from -{MAX_PARA} to {MAX_PARA} pt `{KEY}` 項目 {n}:`{k}` 必須是 -{MAX_PARA} 到 {MAX_PARA} pt 之間的數字
+@msg `{KEY}` entry {n}: no paragraph of {_1} starts at byte {start} `{KEY}` 項目 {n}:{_1} 中沒有從位元組 {start} 開始的段落
+@msg `{KEY}` entry {n}: unknown key `{k}` `{KEY}` 項目 {n}:未知的鍵 `{k}`
+@msg `{KEY}` entry {n}: {_1} is not a type object `{KEY}` 項目 {n}:{_1} 不是文字物件
+@msg `{KEY}` is empty `{KEY}` 為空
+@msg `{KEY}` must be a list `{KEY}` 必須是清單
+@msg `{KEY}` takes at most {MAX_PARAGRAPH_INDENTS} entries (got {_1}) `{KEY}` 最多接受 {MAX_PARAGRAPH_INDENTS} 個項目 (收到 {_1})
+@msg `{KEY}` takes no `{other}` beside it `{KEY}` 不能與 `{other}` 一起使用
+@msg `{base}` is larger than {_1} MB `{base}` 大於 {_1} MB
+@msg `{base}` is not in the DCS file's folder `{base}` 不在 DCS 檔案的資料夾中
+@msg `{key}` holds more than {MAX_EDIT_WORDS} words `{key}` 包含超過 {MAX_EDIT_WORDS} 個單字
+@msg `{key}` must be 0 or more `{key}` 必須大於或等於 0
+@msg `{key}` must be [x, y] numbers `{key}` 必須是 [x, y] 數字
+@msg `{key}` must be a list of words `{key}` 必須是單字清單
+@msg `{key}` must be a list, not {v} `{key}` 必須是清單,而非 {v}
+@msg `{key}` must be a number or null, not {v} `{key}` 必須是數字或 null,而非 {v}
+@msg `{key}` must be a whole number of 0 or more `{key}` 必須是大於或等於 0 的整數
+@msg `{key}` must be from 0 to {MAX_DISTANCE} pt `{key}` 必須介於 0 到 {MAX_DISTANCE} pt 之間
+@msg `{key}` must be from 0.01 to 1000 pt `{key}` 必須介於 0.01 到 1000 pt 之間
+@msg `{key}` must be from {lo} to {hi}, not {v} `{key}` 必須介於 {lo} 到 {hi} 之間,而非 {v}
+@msg `{key}` must be one of auto, always, never `{key}` 必須是 auto、always、never 其中之一
+@msg `{key}` must be one of miter, round, bevel `{key}` 必須是 miter、round、bevel 其中之一
+@msg `{key}` must be one word of at most {_1} characters `{key}` 必須是一個不超過 {_1} 個字元的單字
+@msg `{key}` must hold single words of at most {_1} characters `{key}` 中的每一項必須是不超過 {_1} 個字元的單字
+@msg `{key}` must name an object attached to the path of an Objects on Path group `{key}` 必須指定附加在「路徑上的物件」群組路徑上的物件
+@msg `{key}` names an object that is hidden or locked `{key}` 指定的物件已隱藏或鎖定
+@msg `{name}` is a file of its own, which can only be found next to the DCS file's path `{name}` 是獨立的檔案,只能在 DCS 檔案的路徑旁找到
+@msg `{name}`: {_1}: {e} `{name}`:{_1}:{e}
+@msg `{u}` is no Unicode character `{u}` 不是 Unicode 字元
+@msg `{word}` isn't in the custom dictionary `{word}` 不在自訂字典中
+@msg a CMap has too many mappings CMap 的對應過多
+@msg a \u escape needs four hexadecimal digits \u 逸出序列需要四個十六進位數字
+@msg a comma in an XPath expression must separate function arguments XPath 運算式中的逗號必須用來分隔函式引數
+@msg a document holds at most {_1} SVG filters 一份文件最多包含 {_1} 個 SVG 濾鏡
+@msg a document with unsaved changes has no recovery copy 有未儲存變更的文件沒有復原副本
+@msg a font's data is too large 字體的資料過大
+@msg a font's subroutines are too large 字體的副程式過大
+@msg a function declaration without a name 函式宣告沒有名稱
+@msg a host value has too many entries 主機值的項目過多
+@msg a host value is nested too deep 主機值的巢狀層次過深
+@msg a namespace declaration uses a reserved prefix or URI 命名空間宣告使用了保留的前置詞或 URI
+@msg a namespace qualifier is undefined 命名空間限定詞未定義
+@msg a namespace with an empty URI cannot have a prefix URI 為空的命名空間不能有前置詞
+@msg a number has no digits 數值中沒有數字
+@msg a number's exponent has no digits 數值的指數部分沒有數字
+@msg a number's fraction has no digits 數值的小數部分沒有數字
+@msg a property descriptor cannot have both a value or writable and a get or set 屬性描述元不能同時具有 value 或 writable,以及 get 或 set
+@msg a property descriptor must be an object 屬性描述元必須是物件
+@msg a property's {name} must be a function or undefined 屬性的 {name} 必須是函式或 undefined
+@msg a stream in it would decode to more than 256 MB, or its decode parameters can't be applied 其中有一個資料流解碼後會超過 256 MB,或無法套用其解碼參數
+@msg a string holds a control character 字串包含控制字元
+@msg a string holds an escape sequence JSON does not have 字串包含 JSON 沒有的逸出序列
+@msg a string is not closed 字串未結束
+@msg an SVG filter is named by its id: give `xml` with `name` SVG 濾鏡以其 id 作為名稱:請搭配 `name` 提供 `xml`
+@msg an XML element has two attributes with the same name XML 元素有兩個同名的屬性
+@msg an XML value cannot be inserted into itself or its descendants XML 值不能插入其本身或其後代
+@msg an XML value's children cannot be assigned by position 不能依位置指定 XML 值的子項
+@msg an XML value's children cannot be deleted by position 不能依位置刪除 XML 值的子項
+@msg an XMLList converts to XML only when it has one item XMLList 只有在包含一個項目時才能轉換為 XML
+@msg an XPath node test is missing XPath 運算式少了節點測試
+@msg an XPath operand is missing XPath 運算式少了運算元
+@msg an XPath operator is missing XPath 運算式少了運算子
+@msg an XPath predicate or step cannot follow here 這裡不能接 XPath 述詞或步驟
+@msg an XPath predicate or step follows a value that is not a node-set XPath 述詞或步驟接在不是節點集的值之後
+@msg an XPath step is missing XPath 運算式少了步驟
+@msg an image couldn't be decoded and is missing from the picture 有一個影像無法解碼,因此未出現在圖片中
+@msg an image of {w} × {h} pixels is too large to convert (at most {_1} pixels) {w} × {h} 像素的影像過大,無法轉換 (最多 {_1} 像素)
+@msg apply was called on a value that is not a function 在不是函式的值上呼叫了 apply
+@msg area type in a thread cannot use Dynamic Text: {frames} 串連文字中的區域文字無法使用動態文字:{frames}
+@msg array literal too long 陣列常值過長
+@msg assignment to a constant binding 對常數繫結進行了指定
+@msg at least one row that isn't excluded needs a weight above 0 至少需要一個未排除的列具有大於 0 的權重
+@msg bad point {v}: expected [x, y] 無效的點 {v}:應為 [x, y]
+@msg blend must be a mode name blend 必須是模式名稱
+@msg both sides of an XPath | must select nodes XPath | 的兩側都必須選取節點
+@msg canceled 已取消
+@msg cannot convert the object to a primitive value 無法將物件轉換為基本型別值
+@msg cannot convert undefined or null to an object 無法將 undefined 或 null 轉換為物件
+@msg character dimensions cannot be represented with the source text transform 字元尺寸無法以來源文字變形表示
+@msg character dimensions cannot be represented with the target text transform 字元尺寸無法以目標文字變形表示
+@msg continuation must be an array of path chunks continuation 必須是路徑片段的陣列
+@msg continuation requires points continuation 需要 points
+@msg each path chunk must contain 1 to 4096 points 每個路徑片段必須包含 1 到 4096 個點
+@msg each pick is {{at: [x, y]}} or {{rect: [x0, y0, x1, y1]}} (got {v}) 每個選擇都是 {{at: [x, y]}} 或 {{rect: [x0, y0, x1, y1]}} (收到 {v})
+@msg each weight must be a number of 0 or more (got {v}) 每個權重必須是大於或等於 0 的數字 (收到 {v})
+@msg finally table {t} out of range finally 表格 {t} 超出範圍
+@msg flags cannot be given with a RegExp object 提供 RegExp 物件時不能再提供旗標
+@msg function declaration index out of range 函式宣告索引超出範圍
+@msg give `char`, `text`, `code` or `unicode` 請提供 `char`、`text`、`code` 或 `unicode`
+@msg give `count` or `spacing`, not both 請提供 `count` 或 `spacing`,不要兩者都提供
+@msg give `name`, an SVG filter's name 請提供 `name`,即 SVG 濾鏡的名稱
+@msg give `path`, an SVG file, or `svg`, SVG text 請提供 `path` (SVG 檔案) 或 `svg` (SVG 文字)
+@msg give `xml`, one SVG element 請提供 `xml`,即一個 SVG 元素
+@msg give exactly one of `at`, `rect` and `points` `at`、`rect` 和 `points` 請只提供其中一個
+@msg give exactly one of `path`, `data` and `dataBase64` `path`、`data` 和 `dataBase64` 請只提供其中一個
+@msg give index or indexes, not both 請提供 index 或 indexes,不要兩者都提供
+@msg give one element 請提供一個 元素
+@msg give opacity or blend 請提供 opacity 或 blend
+@msg handler at {_1} expects {_2} values but its range starts with {d} 位於 {_1} 的處理常式預期有 {_2} 個值,但其範圍的起始深度為 {d}
+@msg handler depth too large 處理常式深度過大
+@msg handler range {_1}..{_2} out of range 處理常式範圍 {_1}..{_2} 超出範圍
+@msg hiddenLayers must be true or false hiddenLayers 必須是 true 或 false
+@msg hvcC hvcC
+@msg images in a spot ink whose samples couldn't be read were imported in RGB 取樣無法讀取的特別色油墨影像已以 RGB 匯入
+@msg images past the most pixels one file may hold were left out 超過單一檔案像素上限的影像已略過
+@msg images too large to read were left out 過大而無法讀取的影像已略過
+@msg indexes may hold at most {MAX_GUIDE_INDEXES} entries indexes 最多可包含 {MAX_GUIDE_INDEXES} 個項目
+@msg indexes must be an array of guide indexes indexes 必須是參考線索引的陣列
+@msg instruction out of range 指令超出範圍
+@msg instruction {pc} ({op}) pops below the stack 指令 {pc} ({op}) 彈出的值超出堆疊底部
+@msg instruction {pc} reached with stack depths {seen} and {d} 到達指令 {pc} 時的堆疊深度有 {seen} 和 {d} 兩種
+@msg invalid array length 陣列長度無效
+@msg invalid group 群組無效
+@msg invalid object id {v}: expected a non-negative integer 物件 ID {v} 無效:應為非負整數
+@msg invalid regular expression flag '{_1}' 無效的正規表示式旗標 '{_1}'
+@msg invalid regular expression: {m} 正規表示式無效:{m}
+@msg invalid time value 時間值無效
+@msg is a confirm's answer, whose `value` must be true or false 是 confirm 的答案,其 `value` 必須是 true 或 false
+@msg is a prompt's answer, whose `value` must be a string or null 是 prompt 的答案,其 `value` 必須是字串或 null
+@msg is an alert's answer, whose `value` must be "ok" 是 alert 的答案,其 `value` 必須是 "ok"
+@msg is longer than 16 KiB 長度超過 16 KiB
+@msg it draws its image through a clipping path 它透過剪裁路徑繪製影像
+@msg it draws no image 它沒有繪製影像
+@msg it is larger than {MAX_DICTIONARY_FILE_BYTES} bytes 它大於 {MAX_DICTIONARY_FILE_BYTES} 位元組
+@msg it isn't a file 它不是檔案
+@msg it names {_1} plates, and at most {MAX_PLATES} are read 它指定了 {_1} 個色版,而最多只讀取 {MAX_PLATES} 個
+@msg its art is too long or too detailed for a head 其圖稿過長或過於細緻,無法作為箭頭
+@msg its art paints nothing 其圖稿沒有繪製任何內容
+@msg its image is too large to read 其影像過大,無法讀取
+@msg its plates are too large: more than {MAX_SAMPLES} pixels together 其色版過大:合計超過 {MAX_SAMPLES} 像素
+@msg its plates are too large: {_1} plates of {w} × {h} pixels 其色版過大:{_1} 個 {w} × {h} 像素的色版
+@msg its plates can't be kept together 其色版無法保持在一起
+@msg its shapes could not be combined into one 其形狀無法合併為一個
+@msg jump to {pc}, past the end 跳躍至 {pc},已超過結尾
+@msg legacyNameEncoding must be one of {_1} legacyNameEncoding 必須是 {_1} 其中之一
+@msg localize: the first argument must be a localization object or a string localize:第一個引數必須是本地化物件或字串
+@msg missing `picks` 未提供 `picks`
+@msg missing `word` 未提供 `word`
+@msg must be an object with `kind` and `value` 必須是包含 `kind` 和 `value` 的物件
+@msg must have `kind` alert, confirm or prompt `kind` 必須是 alert、confirm 或 prompt
+@msg must have exactly `kind` and `value` 只能有 `kind` 和 `value`
+@msg needs a closing quote after the name 名稱之後需要結尾引號
+@msg needs a name 需要名稱
+@msg needs a name of at most 64 bytes 需要不超過 64 位元組的名稱
+@msg needs a name of letters, digits and _, or a quoted name 需要由字母、數字和 _ 組成的名稱,或加上引號的名稱
+@msg needs a name without control characters 需要不含控制字元的名稱
+@msg nesting too deep 巢狀層次過深
+@msg no HP-GL format 沒有 HP-GL 格式
+@msg no New Document profile called `{shown}` ({_1}) 沒有名為 `{shown}` 的新增文件描述檔 ({_1})
+@msg no SVG filter at index {k} 索引 {k} 沒有 SVG 濾鏡
+@msg no arrowheads file here 這裡沒有箭頭檔案
+@msg no character has code {code} 沒有代碼為 {code} 的字元
+@msg no custom arrowhead {name} (see stroke.arrowheads.list) 沒有自訂箭頭 {name} (請參閱 stroke.arrowheads.list)
+@msg no file to open 沒有可開啟的檔案
+@msg no instructions 沒有指令
+@msg no layer to rename 沒有可重新命名的圖層
+@msg no random numbers ({e}) 沒有亂數 ({e})
+@msg no selected or given object has a visible 3D effect 所選或指定的物件都沒有可見的 3D 效果
+@msg no such artboard {v} 沒有這個工作區域 {v}
+@msg no symbol named `{_1}` 沒有名為 `{_1}` 的符號
+@msg node {_1} already has the text variable `{_2}` 節點 {_1} 已有文字變數 `{_2}`
+@msg not a DCS file 不是 DCS 檔案
+@msg not a RegExp object 不是 RegExp 物件
+@msg not a regular expression 不是正規表示式
+@msg not an action set: the text doesn't start with /version and a number 不是動作組:文字的開頭不是 /version 和一個數字
+@msg nothing to repeat 沒有可重複的內容
+@msg numbers out of order in {_1} quantifier {_1} 量詞中的數字順序錯誤
+@msg object {_1} needs a 3D effect 物件 {_1} 需要 3D 效果
+@msg object {_1}: a hollow Extrude & Bevel has no caps: turn on Cap to map a graphic 物件 {_1}:空心的「突出與斜角」沒有端面:請開啟「端點」才能對應圖形
+@msg object {_1}: with a depth of 0 the object has one face: map its graphics onto the front 物件 {_1}:深度為 0 時物件只有一個面:請將圖形對應到正面
+@msg object {_1}: {_2} 物件 {_1}:{_2}
+@msg object {_1}: {e} 物件 {_1}:{e}
+@msg object {_1}: {name} at index {index} has no lighting presets; set its lights with effect.setParams 物件 {_1}:索引 {index} 的 {name} 沒有光源預設集;請以 effect.setParams 設定其光源
+@msg opacity must be a number opacity 必須是數字
+@msg operand too large 運算元過大
+@msg order is a whole number from 0 to {others}, not `{v}` order 是 0 到 {others} 之間的整數,而非 `{v}`
+@msg path chunks must share their boundary point 路徑片段必須共用其邊界點
+@msg pdfCompatible: false needs the artboards to keep the editable artwork when useArtboards is false pdfCompatible: false 在 useArtboards 為 false 時需要工作區域來保留可編輯的圖稿
+@msg point x must be a number 點的 x 必須是數字
+@msg point y must be a number 點的 y 必須是數字
+@msg points must be [x,y] pairs points 必須是 [x,y] 配對
+@msg points must be finite canvas points points 必須是有限的畫布座標點
+@msg range out of order in character class 字元類別中的範圍順序錯誤
+@msg reduce of an empty array with no initial value 對空陣列執行 reduce 時沒有初始值
+@msg regular expression flag '{_1}' given twice 正規表示式旗標 '{_1}' 提供了兩次
+@msg sampled text would exceed the canvas (coordinates out of range) 取樣的文字會超出畫布 (座標超出範圍)
+@msg select an SVG filter 請選取 SVG 濾鏡
+@msg select an object with a 3D effect 請選取具有 3D 效果的物件
+@msg select an object with a 3D effect, or give its id in ids 請選取具有 3D 效果的物件,或在 ids 中提供其 ID
+@msg select one layer, group or object to rename 請選取一個要重新命名的圖層、群組或物件
+@msg select the art to weed 請選取要除廢的圖稿
+@msg select the banner, or mark every artboard 請選取橫幅,或標記每個工作區域
+@msg select type on a path or area type 請選取路徑文字或區域文字
+@msg side is left or right, not `{v}` side 是 left 或 right,而非 `{v}`
+@msg source text transform is collapsed or unrepresentable 來源文字變形已塌陷或無法表示
+@msg table must be true or false table 必須是 true 或 false
+@msg takes one name 接受一個名稱
+@msg target text transform is collapsed or unrepresentable 目標文字變形已塌陷或無法表示
+@msg that makes too many pieces (at most 100000) or dash steps: simplify the dash pattern or make the dashes longer 這會產生過多片段 (最多 100000 個) 或虛線步數:請簡化虛線樣式或加長虛線
+@msg the Layers panel is not shown 「圖層」面板未顯示
+@msg the Layers panel's search, filter or Show Layers Only hides its row 「圖層」面板的搜尋、篩選或「僅顯示圖層」隱藏了它的列
+@msg the Objects on Path group is hidden or locked 「路徑上的物件」群組已隱藏或鎖定
+@msg the SVG file is larger than {MAX_IMPORT_BYTES} bytes SVG 檔案大於 {MAX_IMPORT_BYTES} 位元組
+@msg the XML operation needs an XML or XMLList value XML 運算需要 XML 或 XMLList 值
+@msg the XPath expression does not evaluate to a node-set XPath 運算式的結果不是節點集
+@msg the XPath expression ends early XPath 運算式提前結束
+@msg the XPath expression has an unmatched ) XPath 運算式中有不成對的 )
+@msg the XPath expression has an unmatched ] XPath 運算式中有不成對的 ]
+@msg the XPath expression has an unmatched closing bracket XPath 運算式中有不成對的右括號
+@msg the XPath expression selects a namespace node, which is not an XML value XPath 運算式選取了命名空間節點,而它不是 XML 值
+@msg the XPath expression selects the root node, which is not an XML value XPath 運算式選取了根節點,而它不是 XML 值
+@msg the XPath function {name} needs a node-set XPath 函式 {name} 需要節點集
+@msg the XPath function {name} takes the wrong number of arguments XPath 函式 {name} 的引數數目不正確
+@msg the XPath name functions need a node-set XPath 名稱函式需要節點集
+@msg the XPath prefix {shown} is not bound to a namespace XPath 前置詞 {shown} 未繫結到命名空間
+@msg the XPath variable ${shown} is not bound XPath 變數 ${shown} 未繫結
+@msg the argument list is not an object 引數清單不是物件
+@msg the artwork is too large for EPS: what follows 256 MB of PostScript was left out 圖稿對 EPS 而言過大:超過 256 MB PostScript 之後的內容已略過
+@msg the banner has no width or height 橫幅沒有寬度或高度
+@msg the bitmaps hold more pixels than one file may 點陣圖的像素總數超過單一檔案的上限
+@msg the code runs past its last instruction 程式碼執行超過了最後一個指令
+@msg the curve tolerance must be from 0.01 to 10 points 曲線容許度必須介於 0.01 到 10 點之間
+@msg the custom dictionary couldn't be saved to {_1}: {e} 自訂字典無法儲存到 {_1}:{e}
+@msg the custom dictionary {_1} can't be read ({why}), so it is left as it is 無法讀取自訂字典 {_1} ({why}),因此保持原狀
+@msg the document already has an SVG filter named {name} 文件中已有名為 {name} 的 SVG 濾鏡
+@msg the document has another SVG filter named {name}: use svgFilter.apply, or svgFilter.define with `replace` to edit it 文件中有另一個名為 {name} 的 SVG 濾鏡:請使用 svgFilter.apply,或以帶有 `replace` 的 svgFilter.define 編輯它
+@msg the document has no SVG filter named {name} 文件中沒有名為 {name} 的 SVG 濾鏡
+@msg the document has no SVG filter named {r} 文件中沒有名為 {r} 的 SVG 濾鏡
+@msg the document has no color swatches 文件中沒有顏色色票
+@msg the document's swatches hold {_1} colors; the document reduction takes at most 255 文件的色票包含 {_1} 種顏色;文件縮減最多接受 255 種
+@msg the file isn't valid SVG: {e} 檔案不是有效的 SVG:{e}
+@msg the file names no plate 檔案未指定任何色版
+@msg the function's prototype is not an object 函式的原型不是物件
+@msg the grommet inset is too large for the banner 扣眼邊距對橫幅而言過大
+@msg the guide is hidden or locked 參考線已隱藏或鎖定
+@msg the hem is too deep for the banner 折邊對橫幅而言過深
+@msg the library's file is outside the read roots 資料庫的檔案位於讀取根目錄之外
+@msg the line is too long, or the paths it passes too many, to check 線條過長,或經過的路徑過多,無法檢查
+@msg the method needs an XML value 此方法需要 XML 值
+@msg the paint is not a freeform gradient (apply one with paint.editGradient {kind}) 此上色不是任意形狀漸層 (請以 paint.editGradient {kind} 套用)
+@msg the parameters or body do not form one function 參數或主體無法構成一個函式
+@msg the pasted SVG filter {name} draws differently from the document's filter of that name, and no free name fits it 貼上的 SVG 濾鏡 {name} 的繪製結果與文件中同名的濾鏡不同,而且找不到可用的名稱
+@msg the paths are too many or too tangled to find where they cross 路徑過多或過於糾結,無法找出交叉處
+@msg the pen table holds more than 255 pens 筆表包含超過 255 支筆
+@msg the plates can't be kept 色版無法保留
+@msg the position table does not match the instructions 位置表與指令不相符
+@msg the prompt's text is longer than {MAX_PROMPT_BYTES} bytes 提示的文字超過 {MAX_PROMPT_BYTES} 位元組
+@msg the radix must be between 2 and 36 基數必須介於 2 和 36 之間
+@msg the rectangle or line reaches too many paths to check 矩形或線條觸及的路徑過多,無法檢查
+@msg the right side of 'in' is not an object 'in' 的右側不是物件
+@msg the right side of 'instanceof' is not a function 'instanceof' 的右側不是函式
+@msg the selected art is nested more than {MAX_DEPTH} levels deep 所選圖稿的巢狀層次超過 {MAX_DEPTH} 層
+@msg the selected art is too large to measure 所選圖稿過大,無法測量
+@msg the selection has no outlines to weed around 選取範圍沒有可供除廢的外框
+@msg the spacing makes more than {MAX_GROMMETS} grommets 此間距會產生超過 {MAX_GROMMETS} 個扣眼
+@msg the value bind was called on is not a function 呼叫 bind 時所用的值不是函式
+@msg the value is not a constructor 該值不是建構函式
+@msg the value is not a function 該值不是函式
+@msg the value is not a valid XML name 該值不是有效的 XML 名稱
+@msg the value is not an XML attribute name 該值不是 XML 屬性名稱
+@msg the value is not an XML name 該值不是 XML 名稱
+@msg the weeding lines cross the art's outlines too many times to measure; use fewer lines 除廢線與圖稿外框交叉的次數過多,無法測量;請減少線條
+@msg the weeding lines pass too close to too many outline segments to measure; use fewer lines or a smaller gap 除廢線過於接近太多外框線段,無法測量;請減少線條或縮小間隙
+@msg the weeding lines would be more than {max_pieces} pieces 除廢線會超過 {max_pieces} 段
+@msg the {_1} plate lies outside the file {_1} 色版位於檔案之外
+@msg there is no banner to mark 沒有可標記的橫幅
+@msg this is a binary DXF file, which can't be read: save it from the CAD app as an ASCII DXF and open that 這是二進位 DXF 檔案,無法讀取:請在 CAD 應用程式中將它儲存為 ASCII DXF,再開啟該檔案
+@msg this warp's points can't be edited: use Reset with Mesh first 無法編輯此彎曲的點:請先使用「以網格重設」
+@msg toExponential() digits must be between 0 and 20 toExponential() 的位數必須介於 0 和 20 之間
+@msg toFixed() digits must be between 0 and 20 toFixed() 的位數必須介於 0 和 20 之間
+@msg toISOString is not a function toISOString 不是函式
+@msg toPrecision() argument must be between 1 and 21 toPrecision() 的引數必須介於 1 和 21 之間
+@msg toSource: a getter cannot be written as source toSource:getter 無法寫成原始碼
+@msg toSource: an array with properties besides its elements, a property that is not enumerable or another prototype cannot be written as source toSource:除了元素之外還有其他屬性的陣列、不可列舉的屬性或其他原型都無法寫成原始碼
+@msg toSource: an object with a property that is not enumerable, or whose prototype is not Object.prototype, cannot be written as source toSource:具有不可列舉屬性的物件,或原型不是 Object.prototype 的物件,無法寫成原始碼
+@msg toSource: the source is too long toSource:原始碼過長
+@msg toSource: the value contains itself toSource:該值包含其本身
+@msg toSource: the value has too many entries toSource:該值的項目過多
+@msg toSource: the value is nested too deep toSource:該值的巢狀層次過深
+@msg toString is not a function toString 不是函式
+@msg too many arguments 引數過多
+@msg too many quantifiers 量詞過多
+@msg turn on at least one side 請至少開啟一側
+@msg unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt; prefix - to turn off; swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom and aalt take an alternate's number as `tag=N` with N from 0 to {_1}, as `salt=2`) 未知的 OpenType 功能 `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom, aalt;加上前置的 - 可關閉;swsh, ss01–ss20, salt, titl, pnum, lnum, sups, subs, numr, dnom 和 aalt 可用 `tag=N` 指定替代字的編號,N 為 0 到 {_1},例如 `salt=2`)
+@msg unknown dither `{d}` (diffusion, pattern, noise or none) 未知的仿色 `{d}` (diffusion、pattern、noise 或 none)
+@msg unknown panel `{panel}` 未知的面板 `{panel}`
+@msg unknown parameter `{key}` 未知的參數 `{key}`
+@msg unknown parameter `{within}settings.{format}`; {why} 未知的參數 `{within}settings.{format}`;{why}
+@msg unknown parameter `{within}{key}`; {why} 未知的參數 `{within}{key}`;{why}
+@msg unknown reduction `{reduction}` (selective, perceptual, adaptive, web, gray, blackWhite, uniform or document) 未知的縮減 `{reduction}` (selective、perceptual、adaptive、web、gray、blackWhite、uniform 或 document)
+@msg unmatched ')' 不成對的 ')'
+@msg unterminated character class 字元類別未結束
+@msg unterminated group 群組未結束
+@msg with no hem and no grommets there is nothing to mark 沒有折邊也沒有扣眼,因此沒有可標記的內容
+@msg wordGap and lineGap need area type in the selection wordGap 和 lineGap 需要選取範圍中有區域文字
+@msg xpath needs its variables as an object xpath 需要以物件形式提供變數
+@msg xpath needs the expression as a string xpath 需要以字串形式提供運算式
+@msg {GRANT_NOT_REMOVED}, and its settings and cache can't be moved back from {_1} to {_2}: {e} {GRANT_NOT_REMOVED},而且其設定和快取無法從 {_1} 移回 {_2}:{e}
+@msg {_1} is not a folder: {e} {_1} 不是資料夾:{e}
+@msg {_1}: {_2} and {_3} are in the way {_1}:{_2} 和 {_3} 擋住了
+@msg {code}: {0} {code}:{0}
+@msg {dir}: {e} {dir}:{e}
+@msg {file} names more than one linked JavaScript file {file} 指定了多個連結的 JavaScript 檔案
+@msg {k}: a custom arrowhead's path needs an area to fill and at most {most} anchors and {most} subpaths, each anchor within {far} head units of its tip {k}:自訂箭頭的路徑需要有可填色的區域,最多 {most} 個錨點和 {most} 個子路徑,且每個錨點與箭頭尖端的距離在 {far} 個箭頭單位以內
+@msg {k}: bad custom arrowhead path: {e} {k}:自訂箭頭路徑無效:{e}
+@msg {k}: bad custom arrowhead: {e} {k}:自訂箭頭無效:{e}
+@msg {k}: custom must be a name {k}:custom 必須是名稱
+@msg {name} must be a number {name} 必須是數字
+@msg {path}: copy takes a DCS file only when its plates are inside it; relink to it where its plate files are {path}:只有當 DCS 檔案的色版位於檔案內時才能複製它;請在其色版檔案所在位置重新連結
+@msg {path}: larger than {_1} MB {path}:大於 {_1} MB
+@msg {saving} failed: {e} {saving} 失敗:{e}
+@msg {shown} is not an XPath 1.0 function {shown} 不是 XPath 1.0 函式
+@msg {v} names no layer or sublayer {v} 不是任何圖層或子圖層的名稱
+@msg {what} is not a function {what} 不是函式
+@msg {what} would print more than {MAX_COPIES_PER_SHEET} copies on a sheet: scale it up {what} 會在一張紙上列印超過 {MAX_COPIES_PER_SHEET} 份:請放大
+@msg {what} {i} out of range {what} {i} 超出範圍
+@msg A profile supports one Revolve effect. Edit the existing effect's options instead. 一個剖面只支援一個「迴轉」效果。請改為編輯現有效果的選項。
+@msg Add Points at Crossings: a path went missing 在交叉處增加錨點:有一條路徑不見了
+@msg CSS options: {e} CSS 選項:{e}
+@msg CSV has a header but no data rows CSV 有標題列但沒有資料列
+@msg CSV holds NUL bytes: save it as UTF-8, or as UTF-16 with a byte-order mark CSV 包含 NUL 位元組:請儲存為 UTF-8,或帶有位元組順序記號的 UTF-16
+@msg CSV is empty CSV 為空
+@msg CSV is larger than {MAX_CSV_BYTES} bytes CSV 大於 {MAX_CSV_BYTES} 位元組
+@msg CSV is not UTF-16 CSV 不是 UTF-16
+@msg CSV is not UTF-8 CSV 不是 UTF-8
+@msg CSV would be larger than {MAX_CSV_BYTES} bytes CSV 會大於 {MAX_CSV_BYTES} 位元組
+@msg Choose original artwork, not a dimension's generated paths 請選擇原始圖稿,而不是尺寸標註產生的路徑
+@msg Contour: select paths or shapes 輪廓:請選取路徑或形狀
+@msg Contour: the Offset Path effect is not available 輪廓:無法使用「位移複製」效果
+@msg Create needs fills, strokes or both 「建立」需要填色、筆畫或兩者
+@msg DXF options: {e} DXF 選項:{e}
+@msg DXF scale must be a positive number up to {_1}, not {scale} DXF 比例必須是不超過 {_1} 的正數,而非 {scale}
+@msg DXF {field} `{s}`: {allowed} DXF {field} `{s}`:{allowed}
+@msg Dimension is locked or hidden 尺寸標註已鎖定或隱藏
+@msg Dimension mode does not match its source 尺寸標註模式與其來源不相符
+@msg Draw Inside needs exactly one selected path, compound path or text 「內部繪圖」需要剛好選取一個路徑、複合路徑或文字
+@msg EPS Save writes all artwork: use document.export for selected-only EPS EPS 儲存會寫入所有圖稿:若只要所選圖稿的 EPS,請使用 document.export
+@msg EPS flattener: {e} EPS 平面化:{e}
+@msg EPS options: {e} EPS 選項:{e}
+@msg EPS {field} `{s}`: {allowed} EPS {field} `{s}`:{allowed}
+@msg Export for Screens writes png, png8, jpg, webp, gif, tiff, bmp, tga, psd, svg, svgz, pdf, eps, ai or vectorcraft 「匯出為螢幕適用」可寫入 png、png8、jpg、webp、gif、tiff、bmp、tga、psd、svg、svgz、pdf、eps、ai 或 vectorcraft
+@msg Flare click coordinates must be finite numbers 光暈的點按座標必須是有限的數字
+@msg HTML Text applies to object slices of type only HTML 文字只適用於文字的物件切片
+@msg Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces) 「識別物件依據」為 XML ID:名稱必須是有效的 XML ID (以字母、底線或冒號開頭;不含空格)
+@msg Image Trace failed: internal error: its thread stopped (please report this bug) 影像描圖失敗:內部錯誤:其執行緒已停止 (請回報此錯誤)
+@msg Image Trace failed: {e} 影像描圖失敗:{e}
+@msg Image Trace finished, but its document was closed 影像描圖已完成,但其文件已關閉
+@msg Image Trace finished, but the image or its settings changed meanwhile: trace it again 影像描圖已完成,但影像或其設定在此期間已變更:請重新描圖
+@msg Invalid dimension offset 尺寸標註位移無效
+@msg Invalid point 點無效
+@msg Join: a subpath went missing 連接:有一條子路徑不見了
+@msg Lock Workspace is not available in VectorCraft VectorCraft 不提供「鎖定工作區」
+@msg Move Panel to New Window is not available: floating panels stay in the main window 無法使用「將面板移至新視窗」:浮動面板會留在主視窗中
+@msg Offset Path: the path is too complex to offset 位移複製:路徑過於複雜,無法位移
+@msg Offset Path: {e} 位移複製:{e}
+@msg PDF content is incomplete or exceeds the reader's limits; no document was imported PDF 內容不完整或超出讀取器的限制;未匯入任何文件
+@msg PDF flattener: {e} PDF 平面化:{e}
+@msg PDF options: {e} PDF 選項:{e}
+@msg Recolor Artwork is no longer open 「重新上色圖稿」已不再開啟
+@msg SVG is not UTF-8, UTF-16 or ISO 8859-1 SVG 不是 UTF-8、UTF-16 或 ISO 8859-1
+@msg Save writes one artboard: name one, or export several with document.export 「儲存」只寫入一個工作區域:請指定一個,或以 document.export 匯出多個
+@msg Select a dimension first 請先選取尺寸標註
+@msg Select dimensions to update 請選取要更新的尺寸標註
+@msg Select only dimensions 請只選取尺寸標註
+@msg Select two or more objects, or a group 請選取兩個以上的物件,或一個群組
+@msg Shaper needs 1–64 source paths Shaper 需要 1–64 條來源路徑
+@msg Switch x/y is for scatter graphs 「切換 x/y」適用於散佈圖
+@msg Text options: {e} 文字選項:{e}
+@msg `align` is start, center or end `align` 是 start、center 或 end
+@msg `anchors` must be an array of [subpath, anchor] pairs `anchors` 必須是 [subpath, anchor] 配對的陣列
+@msg `anchors` must list anchor indices (0, 1, 2…), not {k} `anchors` 必須列出錨點索引 (0, 1, 2…),而非 {k}
+@msg `anchors` must list anchor indices (0, 1, 2…), not {v} `anchors` 必須列出錨點索引 (0, 1, 2…),而非 {v}
+@msg `artboards` must be a list of artboard indices `artboards` 必須是工作區域索引的清單
+@msg `asset` is the asset id (see assets.list) `asset` 是資產 ID (請參閱 assets.list)
+@msg `at` must be a byte offset `at` 必須是位元組偏移
+@msg `autoLeading` must be a number `autoLeading` 必須是數字
+@msg `background` must be "", "matte" or #rrggbb `background` 必須是 ""、"matte" 或 #rrggbb
+@msg `burasagari` must be "none", "standard" or "forced" `burasagari` 必須是 "none"、"standard" 或 "forced"
+@msg `center` must be [x, y] `center` 必須是 [x, y]
+@msg `charAlign` must be "romanBaseline", "emBoxTop", "emBoxCenter", "emBoxBottom", "icfTop" or "icfBottom" `charAlign` 必須是 "romanBaseline"、"emBoxTop"、"emBoxCenter"、"emBoxBottom"、"icfTop" 或 "icfBottom"
+@msg `charDirection` must be "default", "leftToRight" or "rightToLeft" `charDirection` 必須是 "default"、"leftToRight" 或 "rightToLeft"
+@msg `clearStroke` requires `stroke: false`, `ids: []` and no `stop` `clearStroke` 需要 `stroke: false`、`ids: []`,且不能有 `stop`
+@msg `clear` must be a list of type object ids, not {v} `clear` 必須是文字物件 ID 的清單,而非 {v}
+@msg `clear` must list type objects; {_1} is not one `clear` 必須列出文字物件;{_1} 不是文字物件
+@msg `clear` must not list `id` `clear` 不能列出 `id`
+@msg `color` needs a `rule` to make the theme from `color` 需要 `rule` 才能建立主題
+@msg `colors` is empty `colors` 為空
+@msg `columnType` is vertical, uniform, repeating or sliding `columnType` 是 vertical、uniform、repeating 或 sliding
+@msg `columns` must be a whole number 1..50 `columns` 必須是 1..50 的整數
+@msg `commands` must not be empty `commands` 不能為空
+@msg `contents` goes with `to` `contents` 要與 `to` 一起使用
+@msg `corners` must be sharp, round or square, not `{other}` `corners` 必須是 sharp、round 或 square,而非 `{other}`
+@msg `corners` must list anchor indices (0, 1, 2…), not {v} `corners` 必須列出錨點索引 (0, 1, 2…),而非 {v}
+@msg `count` must be a whole number from 1 to 100 `count` 必須是 1 到 100 之間的整數
+@msg `dataBase64` is larger than the module limit `dataBase64` 大於模組上限
+@msg `dataBase64` is not valid base64 `dataBase64` 不是有效的 base64
+@msg `defaultsOnly` cannot be combined with `ids`, `reverse` or `append` `defaultsOnly` 不能與 `ids`、`reverse` 或 `append` 一起使用
+@msg `depth` and `childLimit` slice the summary: pass `summary: true` `depth` 和 `childLimit` 用於裁切摘要:請傳入 `summary: true`
+@msg `design` is a design name, or null for plain columns `design` 是設計名稱,或以 null 表示一般直條
+@msg `design` is a design name, or null for the default marker `design` 是設計名稱,或以 null 表示預設標記
+@msg `destinationFolder` is required when destination is `folder` destination 為 `folder` 時必須提供 `destinationFolder`
+@msg `destination` must be `none`, `saveAndClose` or `folder` `destination` 必須是 `none`、`saveAndClose` 或 `folder`
+@msg `diameter` must be from 0.1 to 2500 pt `diameter` 必須介於 0.1 到 2500 pt 之間
+@msg `digits` must be one of {_1} `digits` 必須是 {_1} 其中之一
+@msg `dimImages` is a percentage or false `dimImages` 是百分比或 false
+@msg `direction` must be "auto", "leftToRight" or "rightToLeft" `direction` 必須是 "auto"、"leftToRight" 或 "rightToLeft"
+@msg `each` is the value one design stands for, more than 0 `each` 是一個設計所代表的值,必須大於 0
+@msg `end` must be "start" or "end" `end` 必須是 "start" 或 "end"
+@msg `ends` applies to joining exactly two paths `ends` 只適用於連接剛好兩條路徑
+@msg `event` must be one of {_1} `event` 必須是 {_1} 其中之一
+@msg `every` must be "all", "odd" or "even", not {v} `every` 必須是 "all"、"odd" 或 "even",而非 {v}
+@msg `expand` must be true or false `expand` 必須是 true 或 false
+@msg `files` must be 1 to 1000 paths of font files the last text.findFontFiles search found `files` 必須是 1 到 1000 個字體檔案路徑,且出自最近一次 text.findFontFiles 搜尋的結果
+@msg `folder` must be an absolute folder path `folder` 必須是絕對資料夾路徑
+@msg `fontAxes` must be an object of axis tags and values, such as {"wght"} `fontAxes` 必須是由軸標籤和值組成的物件,例如 {"wght"}
+@msg `format` must be a string `format` 必須是字串
+@msg `format` must be one of {_1} `format` 必須是 {_1} 其中之一
+@msg `fractions` is chop or scale `fractions` 是 chop 或 scale
+@msg `from` and `to` must differ `from` 和 `to` 必須不同
+@msg `from` has an empty frame `from` 中有空的文字框
+@msg `from` must be a non-negative integer `from` 必須是非負整數
+@msg `from` must be area type or type on a path `from` 必須是區域文字或路徑文字
+@msg `gid` must be a glyph id, not {v} `gid` 必須是字符 ID,而非 {v}
+@msg `glyphAlternate` belongs to one character, not to a character style (text.setGlyphAlternate sets it) `glyphAlternate` 屬於單一字元,不屬於字元樣式 (由 text.setGlyphAlternate 設定)
+@msg `guide` must be a guide index (guide.list) `guide` 必須是參考線索引 (guide.list)
+@msg `hAlign` must be default, left, center or right `hAlign` 必須是 default、left、center 或 right
+@msg `id` must be a node id `id` 必須是節點 ID
+@msg `id` must be an object id, not {v} `id` 必須是物件 ID,而非 {v}
+@msg `ids` must name Objects on Path groups or objects in them `ids` 必須指定「路徑上的物件」群組或其中的物件
+@msg `ids` takes at most {MAX_INSPECT_IDS} ids `ids` 最多接受 {MAX_INSPECT_IDS} 個 ID
+@msg `imageMap` must be none, rectangle or polygon `imageMap` 必須是 none、rectangle 或 polygon
+@msg `includeArtboardFill` must be true or false `includeArtboardFill` 必須是 true 或 false
+@msg `includeSubdirs` must be true or false `includeSubdirs` 必須是 true 或 false
+@msg `index` must be a point index `index` 必須是點的索引
+@msg `index` must be a whole number or null `index` 必須是整數或 null
+@msg `indices` must be a non-empty array of item indices `indices` 必須是項目索引的非空陣列
+@msg `items` is a list of {kind, item} `items` 是 {kind, item} 的清單
+@msg `items` is empty `items` 為空
+@msg `items` must be a list of {id, points} `items` 必須是 {id, points} 的清單
+@msg `items` must be a list of {{id, anchors?}}, not {v} `items` 必須是 {{id, anchors?}} 的清單,而非 {v}
+@msg `items` must be an array of {id, anchors} `items` 必須是 {id, anchors} 的陣列
+@msg `items` must be an array of {{id, corners?}}, not {v} `items` 必須是 {{id, corners?}} 的陣列,而非 {v}
+@msg `kashida` must be "none", "short", "medium", "long" or "stylistic" `kashida` 必須是 "none"、"short"、"medium"、"long" 或 "stylistic"
+@msg `kind` is one of {_1} `kind` 是 {_1} 其中之一
+@msg `kind` must be "round", "invertedRound" or "chamfer", not {v} `kind` 必須是 "round"、"invertedRound" 或 "chamfer",而非 {v}
+@msg `kind` must be `text`, `visibility`, `image` or `graph` `kind` 必須是 `text`、`visibility`、`image` 或 `graph`
+@msg `kind` must be a string `kind` 必須是字串
+@msg `kind` must be image, noImage or htmlText `kind` 必須是 image、noImage 或 htmlText
+@msg `kinsoku` must be "none", "hard" or "soft" `kinsoku` 必須是 "none"、"hard" 或 "soft"
+@msg `label` must be a non-empty name of at most 200 characters `label` 必須是不超過 200 個字元的非空名稱
+@msg `language` must be a language name or tag, such as "Turkish" or "tr" `language` 必須是語言名稱或標籤,例如 "Turkish" 或 "tr"
+@msg `leadingModel` must be "romanBaseline" or "emBoxTop" `leadingModel` 必須是 "romanBaseline" 或 "emBoxTop"
+@msg `leading` must be from 0.1 to 5000, or null for Auto `leading` 必須介於 0.1 到 5000 之間,或以 null 表示自動
+@msg `length` must be a number of at most 1000000 pt either way `length` 必須是正負都不超過 1000000 pt 的數字
+@msg `line` must be true or false `line` 必須是 true 或 false
+@msg `minAngle` ({min_angle}°) can't be more than `maxAngle` ({max_angle}°) `minAngle` ({min_angle}°) 不能大於 `maxAngle` ({max_angle}°)
+@msg `mode` must be "handles", "straight" or "smooth" `mode` 必須是 "handles"、"straight" 或 "smooth"
+@msg `mode` must be "straight", "bezier" or "arc" `mode` 必須是 "straight"、"bezier" 或 "arc"
+@msg `mode` must be one of {_1} `mode` 必須是 {_1} 其中之一
+@msg `mojikumi` must be "none", "yakumonoHankaku", "gyomatsuYakumonoHankaku", "gyomatsuYakumonoZenkaku", "yakumonoZenkaku" or "lineEndHalf" `mojikumi` 必須是 "none"、"yakumonoHankaku"、"gyomatsuYakumonoHankaku"、"gyomatsuYakumonoZenkaku"、"yakumonoZenkaku" 或 "lineEndHalf"
+@msg `names` is a list of preset names `names` 是預設集名稱的清單
+@msg `names` lists preset names `names` 用來列出預設集名稱
+@msg `names` must be a list of names `names` 必須是名稱清單
+@msg `names` must be a list of swatch names `names` 必須是色票名稱的清單
+@msg `names` must be an array of style names `names` 必須是樣式名稱的陣列
+@msg `new` is a design name, or true `new` 是設計名稱,或 true
+@msg `path` must be a folder `path` 必須是資料夾
+@msg `path` must be a path object `path` 必須是路徑物件
+@msg `pivot` must be a whole number from 0 to 8 `pivot` 必須是 0 到 8 之間的整數
+@msg `points` must be a list of [x, y] points `points` 必須是 [x, y] 點的清單
+@msg `points` must be a list of point indexes `points` 必須是點索引的清單
+@msg `points` must be an array of point indices `points` 必須是點索引的陣列
+@msg `points` must be point indexes `points` 必須是點的索引
+@msg `port` must be "in" or "out" `port` 必須是 "in" 或 "out"
+@msg `position` is above or below `position` 是 above 或 below
+@msg `preset` must be a string `preset` 必須是字串
+@msg `ratio` must be from 0.1 to 0.5 `ratio` 必須介於 0.1 到 0.5 之間
+@msg `rect` must be [x, y, width, height] with a positive size `rect` 必須是大小為正值的 [x, y, width, height]
+@msg `relation` must be overlap, inside or outside `relation` 必須是 overlap、inside 或 outside
+@msg `reversed` must be true or false `reversed` 必須是 true 或 false
+@msg `rotate` must be a finite number `rotate` 必須是有限的數字
+@msg `rule` must be nonZero or evenOdd `rule` 必須是 nonZero 或 evenOdd
+@msg `scale` must be a number in (0, 100] `scale` 必須是 (0, 100] 範圍內的數字
+@msg `scope` must be "selection" or "document", not {other} `scope` 必須是 "selection" 或 "document",而非 {other}
+@msg `script` must be a string `script` 必須是字串
+@msg `seriesIndexes` is a non-empty list of series indexes `seriesIndexes` 是數列索引的非空清單
+@msg `seriesIndexes` needs a `type` or a `valueAxis` `seriesIndexes` 需要 `type` 或 `valueAxis`
+@msg `shift` must be a number of points `shift` 必須是以點為單位的數字
+@msg `showCenter` must be true or false `showCenter` 必須是 true 或 false
+@msg `singleWordJustification` must be "fullJustify", "alignLeft", "alignCenter" or "alignRight" `singleWordJustification` 必須是 "fullJustify"、"alignLeft"、"alignCenter" 或 "alignRight"
+@msg `size` is a percentage of the base text's size, 1 to 100 `size` 是基底文字大小的百分比,1 到 100
+@msg `source` and `target` are the same object `source` 和 `target` 是同一個物件
+@msg `source` must be `folder` or `datasets` `source` 必須是 `folder` 或 `datasets`
+@msg `spacing` must be a number `spacing` 必須是數字
+@msg `spacing` must be a number of points, 0 or more, or "auto" `spacing` 必須是以點為單位且大於或等於 0 的數字,或 "auto"
+@msg `start` must be a finite number `start` 必須是有限的數字
+@msg `starts` must be an array of anchor indices (or nulls) `starts` 必須是錨點索引 (或 null) 的陣列
+@msg `status` must be linked, embedded, missing or modified, or a list of them `status` 必須是 linked、embedded、missing 或 modified,或由它們組成的清單
+@msg `stop` must be a stop index `stop` 必須是色標索引
+@msg `story` styles the whole story: give no `start` or `end` `story` 會設定整篇文章的樣式:請不要提供 `start` 或 `end`
+@msg `story` takes the whole story: give no `start` or `end` `story` 會涵蓋整篇文章:請不要提供 `start` 或 `end`
+@msg `strokeMode` applies to strokes (stroke: true) `strokeMode` 適用於筆畫 (stroke: true)
+@msg `style` must be roman or japanese `style` 必須是 roman 或 japanese
+@msg `subpaths` must be two integer indices `subpaths` 必須是兩個整數索引
+@msg `symbol` must be a symbol name `symbol` 必須是符號名稱
+@msg `target` must be "object" or "contents", not {v} `target` 必須是 "object" 或 "contents",而非 {v}
+@msg `tint` applies to global and spot colours `tint` 適用於全域色和特別色
+@msg `tint` goes with a global or spot `swatch` `tint` 要與全域色或特別色的 `swatch` 一起使用
+@msg `tint` must be a number (%) `tint` 必須是數字 (%)
+@msg `tint` needs a global or spot colour `tint` 需要全域色或特別色
+@msg `to` is already threaded `to` 已經串連
+@msg `to` is the design's new name `to` 是設計的新名稱
+@msg `to` must be [x, y] `to` 必須是 [x, y]
+@msg `to` must be a closed path, area type or type on a path other than `from` `to` 必須是 `from` 以外的封閉路徑、區域文字或路徑文字
+@msg `to` must be a whole number `to` 必須是整數
+@msg `to` takes the rows of one object's stack, not aligned rows `to` 接受單一物件堆疊中的列,而非已對齊的列
+@msg `useLab` must be true or false `useLab` 必須是 true 或 false
+@msg `vAlign` must be default, top, middle, baseline or bottom `vAlign` 必須是 default、top、middle、baseline 或 bottom
+@msg `values` must be a list `values` 必須是清單
+@msg `values` must be an object `values` 必須是物件
+@msg `values` must name hex, rgb, cmyk, lab or gray `values` 必須指定 hex、rgb、cmyk、lab 或 gray
+@msg `values` names a kind twice `values` 將同一種類指定了兩次
+@msg `{_1}` can't be edited or removed `{_1}` 無法編輯或移除
+@msg `{_1}` is built in and always listed `{_1}` 是內建項目,一律會列出
+@msg `{_1}` is not a font file the last text.findFontFiles search found `{_1}` 不是最近一次 text.findFontFiles 搜尋找到的字體檔案
+@msg `{_1}` would be overwritten by the package: choose a different folder or name `{_1}` 會被封裝覆寫:請選擇其他資料夾或名稱
+@msg `{file}` has no art to place `{file}` 沒有可置入的圖稿
+@msg `{file}` is over {_1} MB `{file}` 超過 {_1} MB
+@msg `{f}` belongs to a VectorCraft that is running `{f}` 屬於執行中的 VectorCraft
+@msg `{f}` is the copy of a document open now `{f}` 是目前開啟文件的副本
+@msg `{id}` is no writable format (see document.formats) `{id}` 不是可寫入的格式 (請參閱 document.formats)
+@msg `{id}` isn't a loaded library `{id}` 不是已載入的資料庫
+@msg `{key}` has no colours `{key}` 沒有顏色
+@msg `{key}` is a design name `{key}` 是設計名稱
+@msg `{key}` is a design name or a list of them `{key}` 是設計名稱或設計名稱的清單
+@msg `{key}` is longer than {MAX_NAME} characters `{key}` 超過 {MAX_NAME} 個字元
+@msg `{key}` is out of range `{key}` 超出範圍
+@msg `{key}` must be [min, desired, max] or {{min?, desired?, max?}}, in percent `{key}` 必須是 [min, desired, max] 或 {{min?, desired?, max?}},以百分比表示
+@msg `{key}` must be a non-negative integer `{key}` 必須是非負整數
+@msg `{key}` must be a number {_1}..{_2} `{key}` 必須是 {_1}..{_2} 的數字
+@msg `{key}` must be a whole number of at least 0, not {n} `{key}` 必須是至少為 0 的整數,而非 {n}
+@msg `{key}` must be an appearance item index (paint order) or null `{key}` 必須是外觀項目索引 (繪製順序) 或 null
+@msg `{key}` must be an array of asset ids (see assets.list) `{key}` 必須是資產 ID 的陣列 (請參閱 assets.list)
+@msg `{key}` must be from 0 to 180 degrees, not {a} `{key}` 必須介於 0 到 180 度之間,而非 {a}
+@msg `{key}` must be from {_1} to {shift_max} `{key}` 必須介於 {_1} 到 {shift_max} 之間
+@msg `{key}`: not a colour: {v} `{key}`:不是顏色:{v}
+@msg `{k}.family` is required 必須提供 `{k}.family`
+@msg `{k}` is a number 0..1 `{k}` 是 0..1 的數字
+@msg `{k}` is true or false `{k}` 是 true 或 false
+@msg `{k}` isn't an export setting: {_1} `{k}` 不是匯出設定:{_1}
+@msg `{k}` must be a byte offset `{k}` 必須是位元組偏移
+@msg `{k}` must be a number `{k}` 必須是數字
+@msg `{k}` must be a number of points `{k}` 必須是以點為單位的數字
+@msg `{k}` must be a string `{k}` 必須是字串
+@msg `{k}` must be a whole number `{k}` 必須是整數
+@msg `{k}` must be from {lo} to {hi} for {key} 對 {key} 而言,`{k}` 必須介於 {lo} 到 {hi} 之間
+@msg `{k}` must be {what}, not {v} `{k}` 必須是 {what},而非 {v}
+@msg `{l}` isn't a global or spot colour `{l}` 不是全域色或特別色
+@msg `{m}` isn't a bitmap: use clipboard.importSvg, importPdf or importText `{m}` 不是點陣圖:請使用 clipboard.importSvg、importPdf 或 importText
+@msg `{name}` can't be deleted `{name}` 無法刪除
+@msg `{name}` can't be duplicated `{name}` 無法製作複本
+@msg `{name}` can't be edited `{name}` 無法編輯
+@msg `{name}` can't be edited this way `{name}` 無法以這種方式編輯
+@msg `{name}` can't be renamed `{name}` 無法重新命名
+@msg `{name}` can't name a folder `{name}` 不能作為資料夾名稱
+@msg `{name}` has no art to place `{name}` 沒有可置入的圖稿
+@msg `{name}` has no text to place `{name}` 沒有可置入的文字
+@msg `{name}` in the Word document is not UTF-8 or UTF-16 text Word 文件中的 `{name}` 不是 UTF-8 或 UTF-16 文字
+@msg `{name}` in the Word document is not well-formed XML: {e} Word 文件中的 `{name}` 不是格式正確的 XML:{e}
+@msg `{name}` in the Word document nests elements more than {MAX_NESTING} deep Word 文件中的 `{name}` 的元素巢狀層次超過 {MAX_NESTING} 層
+@msg `{name}` is a built-in preset and is read-only: save it under another name `{name}` 是內建預設集,為唯讀:請以其他名稱儲存
+@msg `{name}` is a built-in preset: save under another name `{name}` 是內建預設集:請以其他名稱儲存
+@msg `{name}` is a built-in profile and stays `{name}` 是內建描述檔,會保留
+@msg `{name}` is a tint of `{base}`: edit `{base}` (a tint swatch only takes a new name) `{name}` 是 `{base}` 的色調:請編輯 `{base}` (色調色票只能變更名稱)
+@msg `{name}` is a working space: choose another with edit.colorSettings first `{name}` 是工作色域:請先以 edit.colorSettings 選擇其他色域
+@msg `{name}` is built in and stays `{name}` 是內建項目,會保留
+@msg `{name}` takes a paint of its own kind (colour, gradient or pattern) `{name}` 只接受同類型的上色 (顏色、漸層或圖樣)
+@msg `{n}` can't be deleted `{n}` 無法刪除
+@msg `{n}` can't be merged `{n}` 無法合併
+@msg `{n}` can't be moved `{n}` 無法移動
+@msg `{n}` is not a colour swatch `{n}` 不是顏色色票
+@msg `{n}` isn't a solid colour `{n}` 不是純色
+@msg `{n}`: colour groups hold solid colours only `{n}`:顏色群組只能包含純色
+@msg `{other}` doesn't trace an image `{other}` 不會描圖影像
+@msg `{s}`: the step's references copy in more than {_1} MB of values `{s}`:此步驟的參照複製了超過 {_1} MB 的值
+@msg `{tag}` is not an axis tag: a tag is four characters, such as "wght" `{tag}` 不是軸標籤:標籤為四個字元,例如 "wght"
+@msg `{to}` is empty or already used `{to}` 為空或已被使用
+@msg `{what}` is longer than {MAX_TEXT} bytes `{what}` 超過 {MAX_TEXT} 位元組
+@msg `{what}` must be text `{what}` 必須是文字
+@msg a DXF drawing has one origin: pass useArtboards: true for one drawing per artboard DXF 圖面只有一個原點:若要每個工作區域各一份圖面,請傳入 useArtboards: true
+@msg a PDF presets file with over {_1} presets PDF 預設集檔案包含超過 {_1} 個預設集
+@msg a batch preview cannot switch documents 批次預覽無法切換文件
+@msg a brush named `{nn}` exists 已有名為 `{nn}` 的筆刷
+@msg a container of the listed objects is gone 所列物件的某個容器已不存在
+@msg a content stream can't be read 有一個內容資料流無法讀取
+@msg a content stream isn't a stream 有一個內容資料流不是資料流
+@msg a design holds at most {_1} objects 一個設計最多包含 {_1} 個物件
+@msg a design needs a name 設計需要名稱
+@msg a dictionary grew too large 有一個字典變得過大
+@msg a document keeps at most {_1} graph designs 一份文件最多保留 {_1} 個圖表設計
+@msg a document keeps at most {_1} saved selections 一份文件最多保留 {_1} 個已儲存的選取範圍
+@msg a document keeps at most {_1} views 一份文件最多保留 {_1} 個檢視
+@msg a document links at most {MAX_SVG_SCRIPTS} JavaScript files 一份文件最多連結 {MAX_SVG_SCRIPTS} 個 JavaScript 檔案
+@msg a feConvolveMatrix filter's order is {x}×{y}, which overflows the kernel-size multiply usvg makes feConvolveMatrix 濾鏡的階數為 {x}×{y},會使 usvg 計算核心大小的乘法溢位
+@msg a gradient preset is already called {n} 已有名為 {n} 的漸層預設集
+@msg a graph has at most {_1} series, so it transposes with at most that many categories 一個圖表最多有 {_1} 個數列,因此轉置時最多只能有同樣數量的類別
+@msg a layer can't be merged into its own sublayer 圖層無法合併到自己的子圖層
+@msg a layer's form box isn't an array 有一個圖層的表單方框不是陣列
+@msg a layer's form can't be read 有一個圖層的表單無法讀取
+@msg a layer's form has no box 有一個圖層的表單沒有方框
+@msg a layer's form has no dictionary 有一個圖層的表單沒有字典
+@msg a legend draws at most {MAX_CHIPS} swatches 圖例最多繪製 {MAX_CHIPS} 個色票
+@msg a name names one asset: give multiple: false, or one object 一個名稱只代表一個資產:請指定 multiple: false,或提供一個物件
+@msg a page can't be read 有一個頁面無法讀取
+@msg a page object is used by something else 有一個頁面物件已被其他內容使用
+@msg a page thumbnail must be RGB pixels, 1 to {THUMBNAIL_SIZE} a side (got {w} × {h}) 頁面縮圖必須是 RGB 像素,每邊 1 到 {THUMBNAIL_SIZE} (收到 {w} × {h})
+@msg a pattern layer doesn't end what it starts 有一個圖樣圖層沒有結束它所開始的內容
+@msg a pattern layer's colors hold something else 有一個圖樣圖層的顏色包含其他內容
+@msg a pattern named `{n}` exists 已有名為 `{n}` 的圖樣
+@msg a plug-in with id {_1} is already installed 已安裝 ID 為 {_1} 的增效模組
+@msg a preset named `{n}` exists 已有名為 `{n}` 的預設集
+@msg a print presets file with over {_1} presets 列印預設集檔案包含超過 {_1} 個預設集
+@msg a profile is already called {n} 已有名為 {n} 的描述檔
+@msg a row can't go into itself 列不能移入其本身
+@msg a series goes on the left or the right value axis 數列只能放在左側或右側的數值軸上
+@msg a start must be an anchor index ≥ 0 or null 起點必須是 ≥ 0 的錨點索引或 null
+@msg a style named `{name}` already exists 已有名為 `{name}` 的樣式
+@msg a style named `{to}` already exists 已有名為 `{to}` 的樣式
+@msg a swatch needs a colour, gradient or pattern 色票需要顏色、漸層或圖樣
+@msg a symbol named `{n}` already exists 已有名為 `{n}` 的符號
+@msg a theme has 1 to {THEME_SIZE} colours 一個主題有 1 到 {THEME_SIZE} 種顏色
+@msg a theme is already called {name} (replace: "{name}" overwrites it) 已有名為 {name} 的主題 (replace: "{name}" 會覆寫它)
+@msg a toolbar name has at most {MAX_NAME} characters 工具列名稱最多 {MAX_NAME} 個字元
+@msg a view named `{_1}` exists 已有名為 `{_1}` 的檢視
+@msg a view named `{name}` exists 已有名為 `{name}` 的檢視
+@msg a workspace keeps at most {MAX} toolbars 一個工作區最多保留 {MAX} 個工具列
+@msg a {_1} series can't go in a {_2} graph {_1} 數列不能放在 {_2} 圖表中
+@msg aiVersion {v}: "pdf" (PDF-compatible, the default) or "7" (legacy .ai, version 7) aiVersion {v}:"pdf" (PDF 相容,預設值) 或 "7" (舊版 .ai,第 7 版)
+@msg already editing an opacity mask 已在編輯不透明度遮色片
+@msg already editing another object's opacity mask 已在編輯另一個物件的不透明度遮色片
+@msg already in pattern editing mode 已在圖樣編輯模式中
+@msg an artboard is listed twice 有一個工作區域列出了兩次
+@msg an empty artboard has no width or height to scale to 空的工作區域沒有可供縮放的寬度或高度
+@msg an image is too large for DXF and was left out 有一個影像對 DXF 而言過大,已略過
+@msg an image of {_1} MB, at most {_2} MB 影像為 {_1} MB,上限為 {_2} MB
+@msg an image of {declared_w} × {declared_h} pixels is too large to draw {declared_w} × {declared_h} 像素的影像過大,無法繪製
+@msg an image of {width} × {height} pixels is too large to draw {width} × {height} 像素的影像過大,無法繪製
+@msg an index must be a whole number 索引必須是整數
+@msg an inline graphic has no glyphs 行內圖形沒有字符
+@msg an invisible text form can't be read 有一個不可見文字表單無法讀取
+@msg an object is missing 有一個物件遺失
+@msg an object's header is missing 有一個物件的標頭遺失
+@msg an overprinting form can't be read 有一個疊印表單無法讀取
+@msg an unread object has no readable bounds 有一個未讀取的物件沒有可讀取的邊界
+@msg an unread object holds a frame of threaded type 有一個未讀取的物件包含串連文字的文字框
+@msg an unread object is missing from its layer 有一個未讀取的物件不在其圖層中
+@msg anchor `smooth` must be true or false 錨點的 `smooth` 必須是 true 或 false
+@msg anchor `{key}` must be [x, y] 錨點的 `{key}` 必須是 [x, y]
+@msg anchor `{key}` must be a number 錨點的 `{key}` 必須是數字
+@msg angle and roundness must be finite angle 和 roundness 必須是有限值
+@msg angles must be a list as long as pins (degrees or null) angles 必須是與 pins 等長的清單 (度數或 null)
+@msg appearance must be flat|center|edge appearance 必須是 flat|center|edge
+@msg apply is "fill" or "stroke", not `{other}` apply 是 "fill" 或 "stroke",而非 `{other}`
+@msg area must be a closed polygon of 3 to 100000 [x, y] points area 必須是由 3 到 100000 個 [x, y] 點組成的封閉多邊形
+@msg area type needs a path that encloses an area 區域文字需要圍出區域的路徑
+@msg area type {_1} is named twice 區域文字 {_1} 指定了兩次
+@msg arrowAlign must be extend|tip, got {o} arrowAlign 必須是 extend|tip,收到 {o}
+@msg arrowScale must be [start, end] arrowScale 必須是 [start, end]
+@msg artboard is a 0-based index artboard 是從 0 起算的索引
+@msg artboard must be a 0-based index artboard 必須是從 0 起算的索引
+@msg artboard {i} doesn't print 工作區域 {i} 不會列印
+@msg artboardLayout must be {layout?, columns?, spacing?, rightToLeft?} artboardLayout 必須是 {layout?, columns?, spacing?, rightToLeft?}
+@msg artboardLayout.columns must be 1 or more artboardLayout.columns 必須大於或等於 1
+@msg artboardLayout.layout must be gridByRow, gridByColumn, row or column artboardLayout.layout 必須是 gridByRow、gridByColumn、row 或 column
+@msg artboardLayout.rightToLeft must be true or false artboardLayout.rightToLeft 必須是 true 或 false
+@msg artboardLayout.spacing must be 0–{MAX_SPACING} pt artboardLayout.spacing 必須是 0–{MAX_SPACING} pt
+@msg artboards must be a non-empty list artboards 必須是非空清單
+@msg artboards must be a number artboards 必須是數字
+@msg asset `{_1}` has no art to export 資產 `{_1}` 沒有可匯出的圖稿
+@msg at most {MAX_KEYWORDS} keywords 最多 {MAX_KEYWORDS} 個關鍵字
+@msg at most {MAX_MATCHES} matches are kept 最多保留 {MAX_MATCHES} 個相符項目
+@msg at most {MAX_POINTS} points 最多 {MAX_POINTS} 個點
+@msg at most {MAX_QUEUE} files 最多 {MAX_QUEUE} 個檔案
+@msg at must be [x, y] at 必須是 [x, y]
+@msg at/rect lie outside the canvas at/rect 位於畫布之外
+@msg attribute must be size|leading|tracking|kerning|baselineShift attribute 必須是 size|leading|tracking|kerning|baselineShift
+@msg attrs must be an object attrs 必須是物件
+@msg automation path rejected: the folder's name isn't valid text 已拒絕自動化路徑:資料夾名稱不是有效的文字
+@msg automation path rejected: {_1} isn't valid text 已拒絕自動化路徑:{_1} 不是有效的文字
+@msg automation path rejected: {why}: {path} 已拒絕自動化路徑:{why}:{path}
+@msg axis must be "vertical", "horizontal" or an angle in degrees, not `{given}` axis 必須是 "vertical"、"horizontal" 或以度為單位的角度,而非 `{given}`
+@msg backgroundContents must be transparent or white backgroundContents 必須是 transparent 或 white
+@msg bad AES key AES 金鑰無效
+@msg bad `runs`: {e} `runs` 無效:{e}
+@msg bad arrowScale arrowScale 無效
+@msg bad base64 base64 無效
+@msg bad color {c} 顏色 {c} 無效
+@msg bad colour 顏色無效
+@msg bad colour in `colors` `colors` 中有無效的顏色
+@msg bad colour {v} 顏色 {v} 無效
+@msg bad colour {v} in groupColors groupColors 中的顏色 {v} 無效
+@msg bad cross-reference entry 交互參照項目無效
+@msg bad cross-reference table 交互參照表無效
+@msg bad current colour {v} 目前的顏色 {v} 無效
+@msg bad dataBase64 dataBase64 無效
+@msg bad edit 編輯無效
+@msg bad fill {v} 填色 {v} 無效
+@msg bad fit: {e} fit 無效:{e}
+@msg bad model {other} (keep, new or document) model {other} 無效 (keep、new 或 document)
+@msg bad new colour {v} 新的顏色 {v} 無效
+@msg bad object 物件無效
+@msg bad object number 物件編號無效
+@msg bad offset 偏移無效
+@msg bad path data: {e} 路徑資料無效:{e}
+@msg bad point {v}: expected [x, y] or [x, y, pressure] 無效的點 {v}:應為 [x, y] 或 [x, y, pressure]
+@msg bad reference 參照無效
+@msg bad trailer size 檔尾大小無效
+@msg bad {k} colour {k} 顏色無效
+@msg batches cannot nest 批次不能巢狀
+@msg bind something to a variable first 請先將某個項目綁定到變數
+@msg bleed must be 0–{MAX_BLEED} pt: a number, [top, bottom, left, right] or {{top, bottom, left, right}} bleed 必須是 0–{MAX_BLEED} pt:一個數字、[top, bottom, left, right] 或 {{top, bottom, left, right}}
+@msg brush must be a name or null brush 必須是名稱或 null
+@msg by must be a number by 必須是數字
+@msg can't add page thumbnails: {why} 無法加入頁面縮圖:{why}
+@msg can't add the editing data for other apps: {why} 無法加入供其他應用程式使用的編輯資料:{why}
+@msg can't edit the written PDF: {why} 無法編輯已寫入的 PDF:{why}
+@msg can't encrypt the PDF: {why} 無法加密 PDF:{why}
+@msg can't linearise the PDF: {why} 無法將 PDF 線性化:{why}
+@msg can't open `{_1}`: {_2} 無法開啟 `{_1}`:{_2}
+@msg can't open `{_1}`: {e} 無法開啟 `{_1}`:{e}
+@msg can't open `{name}`: not a format VectorCraft reads (see document.formats) 無法開啟 `{name}`:不是 VectorCraft 可讀取的格式 (請參閱 document.formats)
+@msg can't place `{name}`: not a format VectorCraft reads (see document.formats) 無法置入 `{name}`:不是 VectorCraft 可讀取的格式 (請參閱 document.formats)
+@msg can't read back the written PDF: {e} 無法讀回已寫入的 PDF:{e}
+@msg can't read this Word document: {why} 無法讀取此 Word 文件:{why}
+@msg can't write .{e} images: use .png, .jpg, .gif, .webp, .tif or .bmp 無法寫入 .{e} 影像:請使用 .png、.jpg、.gif、.webp、.tif 或 .bmp
+@msg can't write PDF layers, overprint or invisible text: {why} 無法寫入 PDF 圖層、疊印或不可見文字:{why}
+@msg can't write the file's copyright: {why} 無法寫入檔案的著作權資訊:{why}
+@msg can't write the file's modification date: {why} 無法寫入檔案的修改日期:{why}
+@msg can't write the pages' user units: {why} 無法寫入頁面的使用者單位:{why}
+@msg cannot distort a zero-size or non-finite bounding box 無法扭曲大小為零或非有限的邊框
+@msg cannot move an object into itself 無法將物件移入其本身
+@msg case must be upper|lower|title|sentence case 必須是 upper|lower|title|sentence
+@msg category `{c}`: one of {_1} 類別 `{c}`:應為 {_1} 其中之一
+@msg center must be [x, y] center 必須是 [x, y]
+@msg choose at least one artboard 請至少選擇一個工作區域
+@msg choose at least one status 請至少選擇一種狀態
+@msg clip must be true or false clip 必須是 true 或 false
+@msg clippingMask must be true or false clippingMask 必須是 true 或 false
+@msg closed must be a boolean closed 必須是布林值
+@msg collapsed must be true or false collapsed 必須是 true 或 false
+@msg color.profile: no RGB or CMYK profile is called “{profile}” color.profile:沒有名為「{profile}」的 RGB 或 CMYK 描述檔
+@msg colorMode must be rgb or cmyk, not `{m}` colorMode 必須是 rgb 或 cmyk,而非 `{m}`
+@msg colorModel must be {own} (the document's mode), grayscale or bitmap colorModel 必須是 {own} (文件的模式)、grayscale 或 bitmap
+@msg colour groups can't go into colour groups 顏色群組不能放入顏色群組
+@msg colour, mode and spot apply to solid-colour swatches only colour、mode 和 spot 只適用於純色色票
+@msg column {column}: `{cell}` is a `%` column of graph data files, which are not read 第 {column} 欄:`{cell}` 是圖表資料檔案的 `%` 欄,不會讀取
+@msg column {column}: `{cell}` names `{name}`, which is a {_1} variable 第 {column} 欄:`{cell}` 指定了 `{name}`,而它是 {_1} 變數
+@msg column {column}: `{cell}` names no variable 第 {column} 欄:`{cell}` 未指定任何變數
+@msg column {column}: the variable name is longer than {MAX_NAME} characters 第 {column} 欄:變數名稱超過 {MAX_NAME} 個字元
+@msg column: true floats the whole column and takes no onto, above or below column: true 會讓整欄浮動,不接受 onto、above 或 below
+@msg command must be a shape.* command command 必須是 shape.* 指令
+@msg commands[{i}]: missing `command` commands[{i}]:未提供 `command`
+@msg composer must be "singleLine" or "everyLine" composer 必須是 "singleLine" 或 "everyLine"
+@msg compound shapes take paths, compound paths, groups, compound shapes, type, blends and envelopes 複合形狀接受路徑、複合路徑、群組、複合形狀、文字、漸變物件和封套
+@msg compound {_1} must contain exactly one open path to join 複合路徑 {_1} 必須剛好包含一條可連接的開放路徑
+@msg connect needs a path other than the one extended connect 需要延伸路徑以外的另一條路徑
+@msg connect needs id connect 需要 id
+@msg connect path has no open end connect 的路徑沒有開放的端點
+@msg copy needs path: the file to copy into the document's folder copy 需要 path:要複製到文件資料夾的檔案
+@msg copy takes images only: relink placed documents where their files are copy 只接受影像:請在置入文件的檔案所在位置重新連結
+@msg copying a file isn't available on the web 網頁版無法複製檔案
+@msg copyrightStatus must be unknown, copyrighted or publicDomain copyrightStatus 必須是 unknown、copyrighted 或 publicDomain
+@msg copyrightUrl must be a URL (no spaces, at most 2048 characters) copyrightUrl 必須是 URL (不含空格,最多 2048 個字元)
+@msg corners must be 4 [x, y] points (TL, TR, BR, BL) corners 必須是 4 個 [x, y] 點 (TL、TR、BR、BL)
+@msg counterPer `{c}`: item or file counterPer `{c}`:應為 item 或 file
+@msg crop `{_1}`: only PDF pages have art, trim, bleed and media boxes; use "crop" or "bounding" crop `{_1}`:只有 PDF 頁面有 art、trim、bleed 和 media 方框;請使用 "crop" 或 "bounding"
+@msg crop rectangle doesn't overlap the image 裁切矩形與影像沒有重疊
+@msg cross-reference and object streams aren't supported 不支援交互參照資料流和物件資料流
+@msg damaged compressed data: {e} 壓縮資料已損毀:{e}
+@msg dash lengths must be numbers, got {v} 虛線長度必須是數字,收到 {v}
+@msg dash must be a list of lengths or null, got {v} dash 必須是長度清單或 null,收到 {v}
+@msg data key `{k}`: letters, digits, -, _ and . (not name or vc-…) 資料鍵 `{k}`:只能使用字母、數字、-、_ 和 . (不能是 name 或 vc-…)
+@msg data must be an object of key: value, not {v} data 必須是 key: value 形式的物件,而非 {v}
+@msg data set `{name}` holds a value for `{var}`, which the library doesn't declare 資料集 `{name}` 包含 `{var}` 的值,但資料庫未宣告該變數
+@msg data set `{name}` holds two values for `{var}` 資料集 `{name}` 包含 `{var}` 的兩個值
+@msg data set `{name}` is declared twice 資料集 `{name}` 宣告了兩次
+@msg data takes at most {MAX_DATA} keys at once data 一次最多接受 {MAX_DATA} 個鍵
+@msg dataset `{name}` already exists 資料集 `{name}` 已存在
+@msg dataset `{to}` already exists 資料集 `{to}` 已存在
+@msg date `{d}`: YYYY-MM-DD 日期 `{d}`:應為 YYYY-MM-DD
+@msg decoded data is too large 解碼後的資料過大
+@msg define a variable first 請先定義變數
+@msg detect must be true or false detect 必須是 true 或 false
+@msg direct-select anchor points of a path 請以直接選取方式選取路徑的錨點
+@msg distance must be > 0 distance 必須 > 0
+@msg dx and dy give the arrow's direction dx 和 dy 用來指定箭頭的方向
+@msg dx and dy must be numbers dx 和 dy 必須是數字
+@msg dxf fitTo must be [width, height], positive dxf fitTo 必須是正值的 [width, height]
+@msg dxf must be an object of DXF import options dxf 必須是 DXF 匯入選項的物件
+@msg dxf scale must be a positive number up to {_1}, not {s} dxf scale 必須是不超過 {_1} 的正數,而非 {s}
+@msg dxf unit `{u}`: a unit such as mm, cm, in or pt dxf unit `{u}`:應為 mm、cm、in 或 pt 這類單位
+@msg dxf: {e} dxf:{e}
+@msg each format is an object {format, scale?, suffix?} 每個格式都是物件 {format, scale?, suffix?}
+@msg each item needs an id 每個項目都需要 id
+@msg each match needs `kind` 每個相符項目都需要 `kind`
+@msg each match needs a `name` 每個相符項目都需要 `name`
+@msg each of `fonts` is {family, style?}; 1 to 1000 of them `fonts` 的每一項都是 {family, style?};共 1 到 1000 項
+@msg each of `items` is {kind, item} `items` 的每一項都是 {kind, item}
+@msg each of `items` is {{id, corners?}} with an object id, not {it} `items` 的每一項都是帶有物件 ID 的 {{id, corners?}},而非 {it}
+@msg each row needs `from` 每一列都需要 `from`
+@msg each stop needs a position 每個色標都需要位置
+@msg each subpath needs an anchors array 每條子路徑都需要 anchors 陣列
+@msg easingType must be easeIn, easeOut, easeInOut or easeOutIn easingType 必須是 easeIn、easeOut、easeInOut 或 easeOutIn
+@msg editingData must be true or false editingData 必須是 true 或 false
+@msg editingData must be true, false or "always" editingData 必須是 true、false 或 "always"
+@msg edits overlap 編輯範圍重疊
+@msg effects of objects whose appearances differ can't be moved together 外觀不同的物件,其效果無法一起移動
+@msg empty header in column {_1} 第 {_1} 欄的標題為空
+@msg encoding `{_1}`: utf8 or utf16 編碼 `{_1}`:應為 utf8 或 utf16
+@msg ends must be two of "first" and "last" ends 必須是兩個值,各為 "first" 或 "last"
+@msg every numbered name for the result is taken 結果可用的編號名稱都已被使用
+@msg expanded SVG styles exceed the import size limit 展開後的 SVG 樣式超過匯入大小上限
+@msg export presets file is larger than {MAX_PRESET_FILE_BYTES} bytes 匯出預設集檔案大於 {MAX_PRESET_FILE_BYTES} 位元組
+@msg exportText must be editable or appearance exportText 必須是 editable 或 appearance
+@msg exportType must be movieClip or graphic, not `{s}` exportType 必須是 movieClip 或 graphic,而非 `{s}`
+@msg extend needs id extend 需要 id
+@msg featureChanges lists at most {MAX_FEATURE_CHANGES} tags featureChanges 最多列出 {MAX_FEATURE_CHANGES} 個標籤
+@msg featureChanges must be a list of tags featureChanges 必須是標籤清單
+@msg features must be a list of tags features 必須是標籤清單
+@msg fidelity must be 0..100 fidelity 必須是 0..100
+@msg files must be {name, dataBase64} files 必須是 {name, dataBase64}
+@msg fit must be none|autoHeight|shrinkText|block, got {id} fit 必須是 none|autoHeight|shrinkText|block,收到 {id}
+@msg flattener: {e} 平面化:{e}
+@msg flattenerPreset `{name}`: high, medium, low or a saved one (see flattener.presets.list) flattenerPreset `{name}`:應為 high、medium、low 或已儲存的預設集 (請參閱 flattener.presets.list)
+@msg flipProfile must be along|across, got {o} flipProfile 必須是 along|across,收到 {o}
+@msg folders `{f}`: none, rows, format, item or itemFormat folders `{f}`:應為 none、rows、format、item 或 itemFormat
+@msg folders deeper than {MAX_BATCH_DEPTH} levels 資料夾深度超過 {MAX_BATCH_DEPTH} 層
+@msg folders deeper than {MAX_DEPTH} levels 資料夾深度超過 {MAX_DEPTH} 層
+@msg font family `{family}` is not available 無法使用字體系列 `{family}`
+@msg font family `{tf}` is not available 無法使用字體系列 `{tf}`
+@msg format `{f}`: pdf or postscript format `{f}`:應為 pdf 或 postscript
+@msg format `{f}`: text or json format `{f}`:應為 text 或 json
+@msg format is `{format}` format 為 `{format}`
+@msg formats must be an array of rows, not {v} formats 必須是列的陣列,而非 {v}
+@msg frequency must be between 1 and 300 lines per inch frequency 必須介於每英寸 1 到 300 線之間
+@msg from and to must be finite from 和 to 必須是有限值
+@msg from must be [x0, y0, x1, y1] from 必須是 [x0, y0, x1, y1]
+@msg give `before` or `after`, not both 請提供 `before` 或 `after`,不要兩者都提供
+@msg give `colors`, or `color` and `rule` 請提供 `colors`,或 `color` 和 `rule`
+@msg give `event`, such as onclick 請提供 `event`,例如 onclick
+@msg give `file`, the JavaScript file's path or URL 請提供 `file`,即 JavaScript 檔案的路徑或 URL
+@msg give `fill` and/or `stroke` 請提供 `fill` 和/或 `stroke`
+@msg give `fill`, `stroke` or both 請提供 `fill`、`stroke` 或兩者
+@msg give `folder` 請提供 `folder`
+@msg give `fonts`, or open a document 請提供 `fonts`,或開啟文件
+@msg give `from` or `indices`, not both 請提供 `from` 或 `indices`,不要兩者都提供
+@msg give `id` (no text is being edited) 請提供 `id` (目前沒有正在編輯的文字)
+@msg give `id` and `anchors`, or `items`, not both 請提供 `id` 和 `anchors`,或提供 `items`,不要兩者都提供
+@msg give `id` or `guide`, not both 請提供 `id` 或 `guide`,不要兩者都提供
+@msg give `length` or `to` 請提供 `length` 或 `to`
+@msg give `matches`: [{kind, name, target}] 請提供 `matches`: [{kind, name, target}]
+@msg give `name` (or edit a pattern) 請提供 `name` (或編輯圖樣)
+@msg give `name` or `names` 請提供 `name` 或 `names`
+@msg give `path` (a .wasm file) or `dataBase64` (the module) 請提供 `path` (.wasm 檔案) 或 `dataBase64` (模組)
+@msg give `path`, `data` or `dataBase64` 請提供 `path`、`data` 或 `dataBase64`
+@msg give `path`, `data`, `dataBase64` or `csv` 請提供 `path`、`data`、`dataBase64` 或 `csv`
+@msg give `path`, `xml` or `dataBase64` 請提供 `path`、`xml` 或 `dataBase64`
+@msg give `script`, the JavaScript the event runs ("" removes the event) 請提供 `script`,即事件執行的 JavaScript ("" 會移除事件)
+@msg give `text` (the reading) or `remove` 請提供 `text` (讀音) 或 `remove`
+@msg give `x` and `y` (and a `tolerance` of 0 or more) 請提供 `x` 和 `y` (以及大於或等於 0 的 `tolerance`)
+@msg give a mode or r/g/b, c/m/y/k, gray or tint adjustments 請提供模式,或 r/g/b、c/m/y/k、gray 或 tint 調整值
+@msg give a name, or paint the active proxy with a saved preset 請提供名稱,或以已儲存的預設集為作用中的代理上色
+@msg give a name, or select a stroke with a saved profile 請提供名稱,或選取使用已儲存描述檔的筆畫
+@msg give a pattern name or select an object painted with a pattern 請提供圖樣名稱,或選取以圖樣上色的物件
+@msg give at least one of clip, invert, disabled, linked clip、invert、disabled、linked 請至少提供一個
+@msg give color, none, swatch or gradient 請提供 color、none、swatch 或 gradient
+@msg give dataBase64 請提供 dataBase64
+@msg give either at (a click) or area (a path drawn around the overlaps) 請提供 at (一次點按) 或 area (圍繞重疊處繪製的路徑) 其中之一
+@msg give formats or a preset, not both 請提供 formats 或預設集,不要兩者都提供
+@msg give newName or delete 請提供 newName 或 delete
+@msg give offset or by 請提供 offset 或 by
+@msg give one of `to`, `rect` or `at` 請提供 `to`、`rect` 或 `at` 其中一個
+@msg give one of assets, layers, selection or fullDocument 請提供 assets、layers、selection 或 fullDocument 其中一個
+@msg give only one of onto, above, below, icon and a column place onto、above、below、icon 和欄位置只能提供其中一個
+@msg give origin: [x, y], or reset: true 請提供 origin: [x, y],或 reset: true
+@msg give overprintFill, overprintStroke, showCenter, imageMap, url or note 請提供 overprintFill、overprintStroke、showCenter、imageMap、url 或 note
+@msg give path (a file) or folder 請提供 path (檔案) 或 folder
+@msg give path, or name and dataBase64 請提供 path,或 name 和 dataBase64
+@msg give paths or files 請提供 paths 或 files
+@msg give rows, rowHeight, columns or columnWidth 請提供 rows、rowHeight、columns 或 columnWidth
+@msg give svg, or dataBase64 of an SVG or SVGZ file 請提供 svg,或 SVG 或 SVGZ 檔案的 dataBase64
+@msg give text 請提供 text
+@msg give the asset a name 請為資產命名
+@msg give the preset a `name` 請為預設集提供 `name`
+@msg give the preset a name 請為預設集命名
+@msg give the preset a name (1–64 characters) 請為預設集命名 (1–64 個字元)
+@msg give the settings to change, e.g. {"formats"}]} 請提供要變更的設定,例如 {"formats"}]}
+@msg give the style a name 請為樣式命名
+@msg give two or more style `names` 請在 `names` 提供兩個以上的樣式名稱
+@msg give two or more swatches 請提供兩個以上的色票
+@msg give x, y, or a handle's length or angle 請提供 x、y,或控制把手的長度或角度
+@msg give {key, value} or {values} 請提供 {key, value} 或 {values}
+@msg global must be true or false global 必須是 true 或 false
+@msg glyph {_1} is not drawn for {ch} with the character's style (such as All Caps, Small Caps or a numeral system) 以該字元的樣式 (例如全部大寫、小型大寫或數字系統) 繪製 {ch} 時不會使用字符 {_1}
+@msg glyph {g} is not an alternate of {ch} in {_1} {_2} 字符 {g} 不是 {_1} {_2} 中 {ch} 的替代字
+@msg gradient must be "objects" or "mesh" gradient 必須是 "objects" 或 "mesh"
+@msg grays must be "profile" or "black", not `{g}` grays 必須是 "profile" 或 "black",而非 `{g}`
+@msg gridColors must be a preset name or two colours gridColors 必須是預設集名稱或兩種顏色
+@msg gridSize must be small, medium or large gridSize 必須是 small、medium 或 large
+@msg guide {g} is not selected 參考線 {g} 未選取
+@msg handle must be 0..3 handle 必須是 0..3
+@msg hiddenObjects {v}: "layer" (the default), "omit" or "visible" hiddenObjects {v}:應為 "layer" (預設值)、"omit" 或 "visible"
+@msg highlight a layer to merge into 請反白標示要合併到的圖層
+@msg highlight two or more rows in one layer or group 請在同一個圖層或群組中反白標示兩列以上
+@msg highlight two or more rows to merge 請反白標示兩列以上以進行合併
+@msg icon docks a panel in the icon column: use window.panel.dock icon 會將面板停駐在圖示欄:請使用 window.panel.dock
+@msg id must be a dimension id id 必須是尺寸標註 ID
+@msg image has no bounds 影像沒有邊界
+@msg image {_1} is linked already 影像 {_1} 已經是連結影像
+@msg imageMap `{m}`: none, client or server imageMap `{m}`:應為 none、client 或 server
+@msg index out of range 索引超出範圍
+@msg instances must be 1..1000 instances 必須是 1..1000
+@msg invalid `color` `color` 無效
+@msg invalid anchor index {v}: expected a non-negative integer 錨點索引 {v} 無效:應為非負整數
+@msg invalid anchor reference {v}: expected [subpath, anchor] 錨點參照 {v} 無效:應為 [subpath, anchor]
+@msg invalid angle 角度無效
+@msg invalid brush: {e} 筆刷無效:{e}
+@msg invalid color 顏色無效
+@msg invalid colour {c} 顏色 {c} 無效
+@msg invalid delta 位移量無效
+@msg invalid point 點無效
+@msg invalid size 大小無效
+@msg invalid t t 無效
+@msg invalid tolerance 容許度無效
+@msg its artboards all lie beyond the canvas (more than {_1} pt from the origin) 其工作區域全都位於畫布之外 (距離原點超過 {_1} pt)
+@msg its clipping masks are nested too deeply 其剪裁遮色片的巢狀層次過深
+@msg its metadata has no description 其中繼資料沒有描述
+@msg its metadata has no direct length 其中繼資料沒有直接長度
+@msg join two different paths 請連接兩條不同的路徑
+@msg justify must be auto|left|center|right|justifyAll justify 必須是 auto|left|center|right|justifyAll
+@msg kbps must be a positive number, not {kbps} kbps 必須是正數,而非 {kbps}
+@msg keywords must be a list of words or a comma-separated string keywords 必須是單字清單或以逗號分隔的字串
+@msg keywords must be at most {MAX_KEYWORD} characters, without control characters keywords 最多 {MAX_KEYWORD} 個字元,且不含控制字元
+@msg kind must be 1, 2 or 3, or give a preset `name` (see perspective.presets.list) kind 必須是 1、2 或 3,或提供預設集的 `name` (請參閱 perspective.presets.list)
+@msg kind must be graphic, fillColor, strokeColor, fillGradient, strokeGradient, fillPattern, strokePattern, charStyle or paraStyle kind 必須是 graphic、fillColor、strokeColor、fillGradient、strokeGradient、fillPattern、strokePattern、charStyle 或 paraStyle
+@msg knockout must be "on", "off", "neutral" or a bool, not {v} knockout 必須是 "on"、"off"、"neutral" 或布林值,而非 {v}
+@msg layer `{name}` has no art to export 圖層 `{name}` 沒有可匯出的圖稿
+@msg layer must name a layer or sublayer layer 必須指定圖層或子圖層
+@msg layers can't be intertwined: select the objects in them 圖層無法交織:請選取其中的物件
+@msg layers go only at the top level or in layers, objects in layers and groups 圖層只能放在最上層或圖層中,物件只能放在圖層和群組中
+@msg layers must be true or false layers 必須是 true 或 false
+@msg layout must be gridByRow, gridByColumn, row or column layout 必須是 gridByRow、gridByColumn、row 或 column
+@msg leading must be a number or "auto" leading 必須是數字或 "auto"
+@msg left/right must be non-negative widths left/right 必須是非負的寬度
+@msg level `{t}`: 2 or 3 level `{t}`:應為 2 或 3
+@msg line {line}: more than {MAX_CSV_COLS} columns 第 {line} 行:超過 {MAX_CSV_COLS} 欄
+@msg line {line}: unclosed quote 第 {line} 行:引號未結束
+@msg line {line}: value for `{var}` must match its variable kind 第 {line} 行:`{var}` 的值必須符合其變數種類
+@msg lineEndings `{other}`: lf or crlf lineEndings `{other}`:應為 lf 或 crlf
+@msg loading profiles from disk isn't available in the browser 瀏覽器中無法從磁碟載入描述檔
+@msg margin: a {m} pt margin leaves no imageable area on the paper margin:{m} pt 的邊界使紙張上沒有可列印區域
+@msg matrix must be finite and invertible, depth finite matrix 必須是有限且可逆的矩陣,depth 必須是有限值
+@msg matte: {e} matte:{e}
+@msg member is for order front, forward, backward or back member 用於 order 為 front、forward、backward 或 back 的情況
+@msg missing `artboards` 未提供 `artboards`
+@msg missing `at` [x, y] 未提供 `at` [x, y]
+@msg missing `attrs` 未提供 `attrs`
+@msg missing `color` or `opacity` 未提供 `color` 或 `opacity`
+@msg missing `colors` 未提供 `colors`
+@msg missing `commands` 未提供 `commands`
+@msg missing `destinationFolder` 未提供 `destinationFolder`
+@msg missing `effect` id 未提供 `effect` ID
+@msg missing `family` 未提供 `family`
+@msg missing `find` 未提供 `find`
+@msg missing `gid` (a glyph id, or null for the default glyph) 未提供 `gid` (字符 ID,或以 null 表示預設字符)
+@msg missing `group` 未提供 `group`
+@msg missing `id` 未提供 `id`
+@msg missing `id`: give one, or select one type object 未提供 `id`:請提供 ID,或選取一個文字物件
+@msg missing `index` (a point index, or null to clear) 未提供 `index` (點的索引,或以 null 清除)
+@msg missing `index` (a stop index, or null to clear) 未提供 `index` (色標索引,或以 null 清除)
+@msg missing `index` (an item index or null) 未提供 `index` (項目索引或 null)
+@msg missing `item` 未提供 `item`
+@msg missing `library` 未提供 `library`
+@msg missing `library` (see graphicStyle.libraries) 未提供 `library` (請參閱 graphicStyle.libraries)
+@msg missing `library` (see swatch.library.list) 未提供 `library` (請參閱 swatch.library.list)
+@msg missing `map` (rows [{from, to}] or {current}) 未提供 `map` (列 [{from, to}] 或 {current})
+@msg missing `matrix` [a,b,c,d,e,f] 未提供 `matrix` [a,b,c,d,e,f]
+@msg missing `name` (see gradient.presets.list) 未提供 `name` (請參閱 gradient.presets.list)
+@msg missing `offset` (a distance in points) 未提供 `offset` (以點為單位的距離)
+@msg missing `path` 未提供 `path`
+@msg missing `path` (a path object id) 未提供 `path` (路徑物件 ID)
+@msg missing `rule` 未提供 `rule`
+@msg missing `source` id 未提供 `source` ID
+@msg missing `start` (the byte offset of a character) 未提供 `start` (字元的位元組偏移)
+@msg missing `status`: linked, embedded, missing or modified 未提供 `status`:應為 linked、embedded、missing 或 modified
+@msg missing `target` id 未提供 `target` ID
+@msg missing `text` 未提供 `text`
+@msg missing `to` 未提供 `to`
+@msg missing `to` (an index) 未提供 `to` (索引)
+@msg missing `to`: a non-negative integer 未提供 `to`:應為非負整數
+@msg missing `type` 未提供 `type`
+@msg missing `value` 未提供 `value`
+@msg missing `{key}` 未提供 `{key}`
+@msg missing `{k}` 未提供 `{k}`
+@msg missing anchor 未提供錨點
+@msg missing anchors 未提供 anchors
+@msg missing anchors [[subpath, anchor]…] 未提供 anchors [[subpath, anchor]…]
+@msg missing array `{key}` 未提供陣列 `{key}`
+@msg missing case 未提供 case
+@msg missing color 未提供顏色
+@msg missing commands 未提供 commands
+@msg missing destination folder 未提供目的地資料夾
+@msg missing from 未提供 from
+@msg missing group 未提供 group
+@msg missing id or ids 未提供 id 或 ids
+@msg missing ids 未提供 ids
+@msg missing indexes 未提供 indexes
+@msg missing matrix [a,b,c,d,e,f] 未提供 matrix [a,b,c,d,e,f]
+@msg missing name 未提供名稱
+@msg missing op 未提供 op
+@msg missing or invalid `color` `color` 未提供或無效
+@msg missing or unknown `tool` `tool` 未提供或未知
+@msg missing point 未提供點
+@msg missing point [x,y] 未提供點 [x,y]
+@msg missing point `{key}` ([x, y]) 未提供點 `{key}` ([x, y])
+@msg missing points 未提供 points
+@msg missing points [[x,y]…] 未提供 points [[x,y]…]
+@msg missing segment 未提供線段
+@msg missing stops 未提供色標
+@msg missing subpaths 未提供 subpaths
+@msg missing target 未提供 target
+@msg missing to 未提供 to
+@msg missing x, y 未提供 x、y
+@msg mode is set, add or toggle, not `{other}` mode 是 set、add 或 toggle,而非 `{other}`
+@msg mode must be "cmyk" or "rgb" mode 必須是 "cmyk" 或 "rgb"
+@msg mode must be area|onPath, got {o} mode 必須是 area|onPath,收到 {o}
+@msg more than 10000 linked files: package dependencies in smaller parts 連結的檔案超過 10000 個:請分成較小的部分封裝相依檔案
+@msg more than 256 commands 超過 256 個指令
+@msg more than {MAX_BATCH_FILES} data sets 超過 {MAX_BATCH_FILES} 個資料集
+@msg more than {MAX_BATCH_FILES} files 超過 {MAX_BATCH_FILES} 個檔案
+@msg more than {MAX_CSV_ROWS} data rows 超過 {MAX_CSV_ROWS} 個資料列
+@msg more than {MAX_FILES} images 超過 {MAX_FILES} 個影像
+@msg more than {MAX_IMPORT} presets 超過 {MAX_IMPORT} 個預設集
+@msg moved must be [[x,y]…] moved 必須是 [[x,y]…]
+@msg name (a toolbar's name) is required 必須提供 name (工具列的名稱)
+@msg name (a toolbar's name) must be a string name (工具列的名稱) 必須是字串
+@msg name must be a toolbar's name name 必須是工具列的名稱
+@msg names is empty names 為空
+@msg names must be an array of symbol names names 必須是符號名稱的陣列
+@msg need `anchor` or `segment` + `t` 需要 `anchor`,或 `segment` + `t`
+@msg need at least one anchor 至少需要一個錨點
+@msg need at least one point 至少需要一個點
+@msg need at least two points 至少需要兩個點
+@msg needs points 需要 points
+@msg nineSlice must be true or false nineSlice 必須是 true 或 false
+@msg nineSliceGuides must be [left, top, right, bottom], each -16..16, with left + right < 1 and top + bottom < 1 nineSliceGuides 必須是 [left, top, right, bottom],各為 -16..16,且 left + right < 1、top + bottom < 1
+@msg no PDF preset named `{n}` 沒有名為 `{n}` 的 PDF 預設集
+@msg no action set is current 目前沒有使用中的動作組
+@msg no active data set 沒有使用中的資料集
+@msg no appearance item {index} 沒有外觀項目 {index}
+@msg no artboard {_1} 沒有工作區域 {_1}
+@msg no artboard {artboard} (0-based; the document has {_1}) 沒有工作區域 {artboard} (從 0 起算;文件有 {_1} 個)
+@msg no asset {id} 沒有資產 {id}
+@msg no asset {id} (see assets.list) 沒有資產 {id} (請參閱 assets.list)
+@msg no assets to export 沒有可匯出的資產
+@msg no assets yet: collect art for export first (assets.add) 尚無資產:請先收集要匯出的圖稿 (assets.add)
+@msg no brush named `{n}` 沒有名為 `{n}` 的筆刷
+@msg no character at byte {start} 位元組 {start} 處沒有字元
+@msg no colour 沒有顏色
+@msg no colour group `{group}` 沒有顏色群組 `{group}`
+@msg no colour group `{name}` 沒有顏色群組 `{name}`
+@msg no data sets 沒有資料集
+@msg no dataset named `{from}` 沒有名為 `{from}` 的資料集
+@msg no dataset named `{name}` 沒有名為 `{name}` 的資料集
+@msg no datasets 沒有資料集
+@msg no design named `{name}` 沒有名為 `{name}` 的設計
+@msg no design named `{n}` 沒有名為 `{n}` 的設計
+@msg no design named `{old}` 沒有名為 `{old}` 的設計
+@msg no encoder for {_1} yet 尚無 {_1} 的編碼器
+@msg no export preset `{name}` (see document.exportPresets.list) 沒有匯出預設集 `{name}` (請參閱 document.exportPresets.list)
+@msg no folder: pass `path` or set Preferences › Plug-ins & Scratch Disks › Additional Plug-ins Folder 沒有資料夾:請傳入 `path`,或設定「偏好設定 › 增效模組 & 暫存磁碟 › 額外的增效模組資料夾」
+@msg no fonts to look for: they are all available 沒有需要尋找的字體:所有字體都可使用
+@msg no formats to export 沒有可匯出的格式
+@msg no gradient preset `{name}` (see gradient.presets.list) 沒有漸層預設集 `{name}` (請參閱 gradient.presets.list)
+@msg no graphic style `{n}` in `{_1}` `{_1}` 中沒有繪圖樣式 `{n}`
+@msg no graphic style library `{key}` (see graphicStyle.libraries) 沒有繪圖樣式資料庫 `{key}` (請參閱 graphicStyle.libraries)
+@msg no guide selected 未選取參考線
+@msg no guide selected (or give an index) 未選取參考線 (或請提供索引)
+@msg no guide {i} 沒有參考線 {i}
+@msg no images in the folder 資料夾中沒有影像
+@msg no item at index {_1} 索引 {_1} 處沒有項目
+@msg no item at index {from} 索引 {from} 處沒有項目
+@msg no item at one of the indices {idx} 索引 {idx} 中有一個沒有對應的項目
+@msg no layer 沒有圖層
+@msg no layer {id} (see layer.list) 沒有圖層 {id} (請參閱 layer.list)
+@msg no layers to export 沒有可匯出的圖層
+@msg no library `{key}` 沒有資料庫 `{key}`
+@msg no metafile format 沒有中繼檔格式
+@msg no object {_1} 沒有物件 {_1}
+@msg no objects 沒有物件
+@msg no parent layer 沒有父圖層
+@msg no perspective grid preset named `{b}` (see perspective.presets.list) 沒有名為 `{b}` 的透視格點預設集 (請參閱 perspective.presets.list)
+@msg no perspective grid preset named `{n}` 沒有名為 `{n}` 的透視格點預設集
+@msg no point selected (give `index`) 未選取任何點 (請提供 `index`)
+@msg no preset called `{from}` (see file.newPresets) 沒有名為 `{from}` 的預設集 (請參閱 file.newPresets)
+@msg no preset called `{name}` (see file.newPresets) 沒有名為 `{name}` 的預設集 (請參閱 file.newPresets)
+@msg no preset named `{n}` 沒有名為 `{n}` 的預設集
+@msg no print preset named `{b}` (see print.presets.list) 沒有名為 `{b}` 的列印預設集 (請參閱 print.presets.list)
+@msg no print preset named `{name}` (presets: {_1}) 沒有名為 `{name}` 的列印預設集 (預設集:{_1})
+@msg no print preset named `{n}` 沒有名為 `{n}` 的列印預設集
+@msg no profile named `{name}` 沒有名為 `{name}` 的描述檔
+@msg no readable files in the folder 資料夾中沒有可讀取的檔案
+@msg no recovery copy `{f}` (see file.recovery.list) 沒有復原副本 `{f}` (請參閱 file.recovery.list)
+@msg no rows 沒有列
+@msg no saved PDF preset named `{name}` 沒有名為 `{name}` 的已儲存 PDF 預設集
+@msg no saved export preset `{name}` 沒有已儲存的匯出預設集 `{name}`
+@msg no saved gradient preset called {name} 沒有名為 {name} 的已儲存漸層預設集
+@msg no saved gradient presets 沒有已儲存的漸層預設集
+@msg no saved preset called `{name}` 沒有名為 `{name}` 的已儲存預設集
+@msg no saved presets to export (name [Default] in `names` to export it) 沒有可匯出的已儲存預設集 (若要匯出 [Default],請在 `names` 中指定它)
+@msg no saved presets to export (name built-in ones in `names`) 沒有可匯出的已儲存預設集 (內建預設集請在 `names` 中指定)
+@msg no saved print preset named `{name}` 沒有名為 `{name}` 的已儲存列印預設集
+@msg no saved profile called {name} 沒有名為 {name} 的已儲存描述檔
+@msg no saved selection `{name}` 沒有已儲存的選取範圍 `{name}`
+@msg no saved theme called {name} (see colorTheme.list) 沒有名為 {name} 的已儲存主題 (請參閱 colorTheme.list)
+@msg no saved views 沒有已儲存的檢視
+@msg no series {i} 沒有數列 {i}
+@msg no slice {n} in the image (see slice.list) 影像中沒有切片 {n} (請參閱 slice.list)
+@msg no source art 沒有來源圖稿
+@msg no stop {i} (the gradient has {n}) 沒有色標 {i} (漸層有 {n} 個)
+@msg no style `{name}` 沒有樣式 `{name}`
+@msg no such assets (see assets.list) 沒有這些資產 (請參閱 assets.list)
+@msg no such frame anchor, or the frame would reach past the canvas 沒有這個文字框錨點,或文字框會超出畫布
+@msg no such objects 沒有這些物件
+@msg no such path anchor, or the path would reach past the canvas 沒有這個路徑錨點,或路徑會超出畫布
+@msg no such rows 沒有這些列
+@msg no swatch `{keep}` 沒有色票 `{keep}`
+@msg no swatch `{n}` 沒有色票 `{n}`
+@msg no swatch `{n}` (swatches and colour groups move separately) 沒有色票 `{n}` (色票和顏色群組要分開移動)
+@msg no swatch library `{key}` (see swatch.library.list) 沒有色票資料庫 `{key}` (請參閱 swatch.library.list)
+@msg no swatch library `{key}` (see swatch.library.list; "document": the document's swatches) 沒有色票資料庫 `{key}` (請參閱 swatch.library.list;"document":文件的色票)
+@msg no swatch library `{key}` (see swatch.library.list; "{DOCUMENT_SWATCHES}": the document's swatches) 沒有色票資料庫 `{key}` (請參閱 swatch.library.list;"{DOCUMENT_SWATCHES}":文件的色票)
+@msg no swatch named `{n}` 沒有名為 `{n}` 的色票
+@msg no swatch or colour group `{n}` 沒有色票或顏色群組 `{n}`
+@msg no swatch or colour group `{n}` in `{_1}` `{_1}` 中沒有色票或顏色群組 `{n}`
+@msg no symbol named `{name}` 沒有名為 `{name}` 的符號
+@msg no text objects selected 未選取文字物件
+@msg no toolbar named `{_1}` 沒有名為 `{_1}` 的工具列
+@msg no toolbar named `{name}` 沒有名為 `{name}` 的工具列
+@msg no tracing preset `{preset}` (see imageTrace.presets) 沒有描圖預設集 `{preset}` (請參閱 imageTrace.presets)
+@msg no user library folder here (save with a path, or without one to get the data) 這裡沒有使用者資料庫資料夾 (請以路徑儲存,或不提供路徑以取得資料)
+@msg no variable named `{from}` 沒有名為 `{from}` 的變數
+@msg no variable named `{name}` 沒有名為 `{name}` 的變數
+@msg no variable named `{var}` 沒有名為 `{var}` 的變數
+@msg no variable named `{v}` 沒有名為 `{v}` 的變數
+@msg no view named `{name}` 沒有名為 `{name}` 的檢視
+@msg node {_1} is not a graph 節點 {_1} 不是圖表
+@msg node {_1} is not an image 節點 {_1} 不是影像
+@msg node {_1} is not type 節點 {_1} 不是文字
+@msg not a PDF presets file (format `{_1}`) 不是 PDF 預設集檔案 (格式為 `{_1}`)
+@msg not a PDF presets file: {e} 不是 PDF 預設集檔案:{e}
+@msg not a Word document: its WordDocument stream doesn't start with a file information block 不是 Word 文件:其 WordDocument 資料流的開頭不是檔案資訊區塊
+@msg not a Word document: its compound file has no WordDocument stream 不是 Word 文件:其複合檔案沒有 WordDocument 資料流
+@msg not a Word document: its main part is not a WordprocessingML document 不是 Word 文件:其主要部分不是 WordprocessingML 文件
+@msg not a Word document: its package has no main document part 不是 Word 文件:其封裝沒有主要文件部分
+@msg not a closed path 不是封閉路徑
+@msg not a flattener presets file (format `{_1}`) 不是平面化預設集檔案 (格式為 `{_1}`)
+@msg not a flattener presets file: {e} 不是平面化預設集檔案:{e}
+@msg not a layer or group 不是圖層或群組
+@msg not a library file: {e} 不是資料庫檔案:{e}
+@msg not a mesh envelope point 不是網格封套的點
+@msg not a perspective grid presets file (format `{_1}`) 不是透視格點預設集檔案 (格式為 `{_1}`)
+@msg not a perspective grid presets file: {e} 不是透視格點預設集檔案:{e}
+@msg not a print presets file (format `{_1}`) 不是列印預設集檔案 (格式為 `{_1}`)
+@msg not a print presets file: {e} 不是列印預設集檔案:{e}
+@msg not a stored or deflated central record 不是以儲存或 deflate 方式寫入的中央目錄記錄
+@msg not a valid SVGZ file: {e} 不是有效的 SVGZ 檔案:{e}
+@msg not an RTF file: it doesn't start with {\rtf 不是 RTF 檔案:開頭不是 {\rtf
+@msg not an image VectorCraft reads (see document.formats) 不是 VectorCraft 可讀取的影像 (請參閱 document.formats)
+@msg not area type (text in a frame) outside perspective 不是透視以外的區域文字 (文字框中的文字)
+@msg not editing an opacity mask 目前不在編輯不透明度遮色片
+@msg not type on a path outside perspective 不是透視以外的路徑文字
+@msg nothing prints 沒有可列印的內容
+@msg nothing to bind 沒有可綁定的內容
+@msg nothing to change 沒有要變更的內容
+@msg nothing to change: give dx, dy, hScale, vScale or rotation 沒有要變更的內容:請提供 dx、dy、hScale、vScale 或 rotation
+@msg nothing to crop 沒有可裁切的內容
+@msg nothing to export 沒有可匯出的內容
+@msg nothing to export: the document has no visible art 沒有可匯出的內容:文件中沒有可見的圖稿
+@msg nothing to mark 沒有可標記的內容
+@msg nothing to print: every artboard picked is blank 沒有可列印的內容:選擇的工作區域全都是空白
+@msg nothing to print: the document has no art that prints 沒有可列印的內容:文件中沒有會列印的圖稿
+@msg object is not a path 物件不是路徑
+@msg object transparency cannot be sampled onto highlighted characters; disable Transparency in Eyedropper Options or select the whole object 物件透明度無法取樣到反白標示的字元上;請在「滴管選項」中停用「透明度」,或選取整個物件
+@msg object {_1} can't be set as point type 物件 {_1} 無法設為點狀文字
+@msg object {_1} has lost its file's contents: relink it 物件 {_1} 已遺失其檔案的內容:請重新連結
+@msg object {_1} has no path 物件 {_1} 沒有路徑
+@msg object {_1} has no size 物件 {_1} 沒有大小
+@msg object {_1} is hidden or locked, or on a locked artboard 物件 {_1} 已隱藏或鎖定,或位於鎖定的工作區域上
+@msg object {_1} is not a placed document 物件 {_1} 不是置入的文件
+@msg object {_1} is not an Intertwine group 物件 {_1} 不是交織群組
+@msg object {_1} is not an image 物件 {_1} 不是影像
+@msg object {_1} is not an image or a linked file 物件 {_1} 不是影像或連結的檔案
+@msg object {_1} is not an image or a placed document 物件 {_1} 不是影像或置入的文件
+@msg object {_1} is not in this Intertwine group 物件 {_1} 不在此交織群組中
+@msg object {_1} needs a 3D effect at index {index} 物件 {_1} 在索引 {index} 處需要 3D 效果
+@msg object {id} has anchors 0–{_1} for `corners`, not {past} 物件 {id} 可用於 `corners` 的錨點為 0–{_1},沒有 {past}
+@msg object {id} has {n} anchors, so no anchor {past} 物件 {id} 有 {n} 個錨點,因此沒有錨點 {past}
+@msg objects must be none, move or copy objects 必須是 none、move 或 copy
+@msg objects were left out 有物件已略過
+@msg offset out of range 偏移超出範圍
+@msg on must be true or false on 必須是 true 或 false
+@msg only groups, layers, type and images have a Contents (Characters) row 只有群組、圖層、文字和影像有「內容 (字元)」列
+@msg only interior mesh lines can be deleted 只能刪除內部的網格線
+@msg only solid colours can be added (not gradients or patterns) 只能加入純色 (不能加入漸層或圖樣)
+@msg only solid-colour swatches can be spot colours 只有純色色票可以是特別色
+@msg only this Affinity file's embedded preview could be read: use File › Open to see it with its warning, or export SVG or PDF from Affinity first 只能讀取此 Affinity 檔案的內嵌預視:請使用「檔案 › 開啟」查看它及其警告,或先從 Affinity 匯出 SVG 或 PDF
+@msg only this Affinity file's embedded preview could be read; use File › Open to see it with its warning 只能讀取此 Affinity 檔案的內嵌預視;請使用「檔案 › 開啟」查看它及其警告
+@msg onto, above, below, icon and a column place belong to window.panel.float and window.panel.dock onto、above、below、icon 和欄位置屬於 window.panel.float 和 window.panel.dock
+@msg onto, above, below, icon and a column place take no anchor, side, zone or before onto、above、below、icon 和欄位置不接受 anchor、side、zone 或 before
+@msg onto, above, below, icon and a column place take no x, y, width or height onto、above、below、icon 和欄位置不接受 x、y、width 或 height
+@msg open a document to look for its linked files 請開啟文件以尋找其連結的檔案
+@msg options must be an object (see file.formatOptions) options 必須是物件 (請參閱 file.formatOptions)
+@msg order must be leftToRight or rightToLeft order 必須是 leftToRight 或 rightToLeft
+@msg orientation must be portrait or landscape, not `{o}` orientation 必須是 portrait 或 landscape,而非 `{o}`
+@msg outlineText {v}: true or false outlineText {v}:應為 true 或 false
+@msg output intent profile: {e} 輸出目的描述檔:{e}
+@msg output.destination: no RGB or CMYK profile is called “{name}” output.destination:沒有名為「{name}」的 RGB 或 CMYK 描述檔
+@msg output.destination: {_1} files are CMYK, and “{name}” is an RGB profile output.destination:{_1} 檔案為 CMYK,而「{name}」是 RGB 描述檔
+@msg output.destination: {e} output.destination:{e}
+@msg output.inks: no ink is set to print output.inks:沒有任何油墨設定為列印
+@msg output.outputIntent: no profile is called “{name}”, and a {std} output intent embeds its profile output.outputIntent:沒有名為「{name}」的描述檔,而 {std} 輸出目的會內嵌其描述檔
+@msg output.outputIntent: no profile is called “{name}”: a {std} output intent embeds a profile, or names a registered printing condition (outputConditionId and registry) output.outputIntent:沒有名為「{name}」的描述檔:{std} 輸出目的會內嵌描述檔,或指定已註冊的印刷條件 (outputConditionId 和 registry)
+@msg output.outputIntent: {std} files print in CMYK, and “{name}” is an RGB profile output.outputIntent:{std} 檔案以 CMYK 列印,而「{name}」是 RGB 描述檔
+@msg output.outputIntent: “{name}” is a grey profile, which can't be a {std} output intent output.outputIntent:「{name}」是灰階描述檔,不能作為 {std} 輸出目的
+@msg overlap: {o} pt is half a tile or more overlap:{o} pt 已達拼貼的一半以上
+@msg overlapping Shaper paths must be in the same layer or group 重疊的 Shaper 路徑必須位於同一個圖層或群組
+@msg overlapping edits 編輯範圍重疊
+@msg page art conflicts with image data for {key} 頁面圖稿與 {key} 的影像資料衝突
+@msg page art conflicts with pattern {name} 頁面圖稿與圖樣 {name} 衝突
+@msg page art conflicts with swatch {name} 頁面圖稿與色票 {name} 衝突
+@msg page art is missing image data for {key} 頁面圖稿沒有 {key} 的影像資料
+@msg page art is missing pattern {name} 頁面圖稿沒有圖樣 {name}
+@msg page art is missing swatch {name} 頁面圖稿沒有色票 {name}
+@msg page must be a whole number from 1 page 必須是從 1 起算的整數
+@msg page thumbnail: {e} 頁面縮圖:{e}
+@msg page {page} of the PDF has no objects PDF 的第 {page} 頁沒有物件
+@msg page {page}: `{file}` has {n} {artboards} 第 {page} 頁:`{file}` 有 {n} {artboards}
+@msg page {page}: `{name}` has {n} {pages} 第 {page} 頁:`{name}` 有 {n} {pages}
+@msg pages must be a range such as "2-3" or a page number, not {v} pages 必須是 "2-3" 這類範圍或頁碼,而非 {v}
+@msg pages: {e} pages:{e}
+@msg panel position must be finite 面板位置必須是有限值
+@msg parameters must be an object 參數必須是物件
+@msg params must be an object params 必須是物件
+@msg passwords can have up to {MAX_PASSWORD} bytes 密碼最多 {MAX_PASSWORD} 位元組
+@msg path has no open end 路徑沒有開放的端點
+@msg paths must be strings paths 必須是字串
+@msg pdfCompatible: false needs every artboard to keep the editable artwork pdfCompatible: false 需要所有工作區域才能保留可編輯的圖稿
+@msg pdfs `{m}`: single or multiple pdfs `{m}`:應為 single 或 multiple
+@msg pickColorFrom is "artwork" or "swatches", not {v} pickColorFrom 是 "artwork" 或 "swatches",而非 {v}
+@msg pins and moved must be non-empty lists of the same length pins 和 moved 必須是長度相同的非空清單
+@msg pins must be [[x,y]…] pins 必須是 [[x,y]…]
+@msg pins must be finite pins 必須是有限值
+@msg place is above, below or inside, not `{other}` place 是 above、below 或 inside,而非 `{other}`
+@msg plane must be left|right|ground plane 必須是 left|right|ground
+@msg plane must be left|right|ground|none plane 必須是 left|right|ground|none
+@msg point is outside the mesh 點位於網格之外
+@msg points must be [t, left, right] with non-negative widths points 必須是寬度為非負值的 [t, left, right]
+@msg points must be a non-empty [[x,y,pressure?]…] list points 必須是非空的 [[x,y,pressure?]…] 清單
+@msg points must be a non-empty list of finite [x,y] points 必須是由有限 [x,y] 組成的非空清單
+@msg points must be an array or null points 必須是陣列或 null
+@msg points must be finite points 必須是有限值
+@msg points must be finite [x,y] pairs points 必須是有限的 [x,y] 配對
+@msg points must contain 1–4096 coordinates points 必須包含 1–4096 個座標
+@msg position must be normal, superscript or subscript position 必須是 normal、superscript 或 subscript
+@msg position must be topLeft, topRight, bottomLeft or bottomRight position 必須是 topLeft、topRight、bottomLeft 或 bottomRight
+@msg positions must be between 0 and 100000 pt positions 必須介於 0 到 100000 pt 之間
+@msg ppi must be a positive number, not {ppi} ppi 必須是正數,而非 {ppi}
+@msg ppi must be between 1 and 2400 ppi 必須介於 1 和 2400 之間
+@msg preserve `{v}` is not transforms|bounds|fileDimensions|fit|fill preserve `{v}` 不是 transforms|bounds|fileDimensions|fit|fill
+@msg preserveShape must be clippingMask|transparency preserveShape 必須是 clippingMask|transparency
+@msg preset `{id}`: mobile or density preset `{id}`:應為 mobile 或 density
+@msg preset must be standard, diffuse, topLeft or right preset 必須是 standard、diffuse、topLeft 或 right
+@msg preset must be the name of a print preset preset 必須是列印預設集的名稱
+@msg previewMode must be default, pixel or overprint previewMode 必須是 default、pixel 或 overprint
+@msg print settings: {e} 列印設定:{e}
+@msg procedures nest too deeply 程序的巢狀層次過深
+@msg procedures nested too deeply 程序的巢狀層次過深
+@msg quotes must be {double?, single?} quotes 必須是 {double?, single?}
+@msg quotes.{k} must be an opening and a closing quote, e.g. "“”" quotes.{k} 必須是一個前引號和一個後引號,例如 "“”"
+@msg rasterEffectsPpi must be 1–2400 (72 screen, 150 medium, 300 high) rasterEffectsPpi 必須是 1–2400 (72 螢幕、150 中、300 高)
+@msg rating must be a whole number from 0 to {MAX_RATING} rating 必須是 0 到 {MAX_RATING} 之間的整數
+@msg rect and {k} contradict each other: give the area or the artboards rect 和 {k} 互相矛盾:請提供區域或工作區域
+@msg rect applies to raster formats, not {_1} rect 適用於點陣格式,不適用於 {_1}
+@msg rect is [x, y, width, height] in points rect 是以點為單位的 [x, y, width, height]
+@msg rect must be [x, y, width, height] rect 必須是 [x, y, width, height]
+@msg rect must be [x, y, width, height] with a positive width and height rect 必須是寬度和高度為正值的 [x, y, width, height]
+@msg rect needs finite numbers and a positive width and height rect 需要有限的數字,以及正值的寬度和高度
+@msg redraw needs id redraw 需要 id
+@msg redraw within must be a finite distance of 0 or more redraw 的 within 必須是大於或等於 0 的有限距離
+@msg registration must be 0..8 registration 必須是 0..8
+@msg release a layer or a group 請釋放圖層或群組
+@msg replace keeps the replaced object's place and transform: drop template, at and rect replace 會保留被取代物件的位置和變形:請不要提供 template、at 和 rect
+@msg replace: select the one object to replace replace:請選取要取代的那一個物件
+@msg resolution must be a number or screen|medium|high resolution 必須是數字或 screen|medium|high
+@msg resolution must be between 1 and 2400 ppi resolution 必須介於 1 到 2400 ppi 之間
+@msg rotation must be a number of degrees rotation 必須是以度為單位的數字
+@msg rotation must be finite rotation 必須是有限值
+@msg rows and cols must be 1..50 rows 和 cols 必須是 1..50
+@msg rows and columns must be 1..500 rows 和 columns 必須是 1..500
+@msg rows of objects whose appearances differ can't be reordered together 外觀不同的物件,其列無法一起重新排序
+@msg save the document first: Package collects a saved document 請先儲存文件:「封裝」會收集已儲存的文件
+@msg save the document first: copy puts the file in its folder 請先儲存文件:copy 會將檔案放入文件的資料夾
+@msg savedPreset: {e} savedPreset:{e}
+@msg scale `{s}`: a factor such as 2x, a width (100w), a height (100h) or a resolution (72ppi) scale `{s}`:應為 2x 這類倍數、寬度 (100w)、高度 (100h) 或解析度 (72ppi)
+@msg scale must be a number above 0, at most 64 scale 必須是大於 0 且不超過 64 的數字
+@msg scale must be non-zero scale 不能為零
+@msg scope must be selection or all (got {other}) scope 必須是 selection 或 all (收到 {other})
+@msg scribble is too long 塗抹筆跡過長
+@msg segment does not exist 線段不存在
+@msg select a Shaper group 請選取 Shaper 群組
+@msg select a blend to set easing or color shift 請選取漸變物件以設定緩動或顏色偏移
+@msg select a compound shape 請選取複合形狀
+@msg select a graph 請選取圖表
+@msg select a group or layer with visible, unlocked members 請選取含有可見且未鎖定成員的群組或圖層
+@msg select a layer 請選取圖層
+@msg select a text object 請選取文字物件
+@msg select an image (or an Image Trace group) 請選取影像 (或影像描圖群組)
+@msg select an image, or pass id 請選取影像,或傳入 id
+@msg select an object (or pass `id`) 請選取物件 (或傳入 `id`)
+@msg select an object with a freeform gradient 請選取具有任意形狀漸層的物件
+@msg select an object with an opacity mask 請選取具有不透明度遮色片的物件
+@msg select area type 請選取區域文字
+@msg select area type (text in a frame) 請選取區域文字 (文字框中的文字)
+@msg select area type and closed paths (at least one area type object) 請選取區域文字和封閉路徑 (至少一個區域文字物件)
+@msg select area type whose first line can fit its frame 請選取第一行可容納於文字框的區域文字
+@msg select artwork to add its colours 請選取圖稿以加入其顏色
+@msg select artwork to make a group of its colours 請選取圖稿以將其顏色建立為群組
+@msg select artwork to recolour, or give a colour `group` 請選取要重新上色的圖稿,或提供顏色 `group`
+@msg select exactly two path ends to join 請剛好選取兩個要連接的路徑端點
+@msg select members of a compound shape 請選取複合形狀的成員
+@msg select objects or give ids 請選取物件或提供 ids
+@msg select objects to align to the guide 請選取要對齊參考線的物件
+@msg select one Intertwine group 請選取一個交織群組
+@msg select one image or pass id 請選取一個影像或傳入 id
+@msg select paths 請選取路徑
+@msg select paths or compound paths 請選取路徑或複合路徑
+@msg select rows or objects to collect 請選取要收集的列或物件
+@msg select something to export 請選取要匯出的內容
+@msg select something to export (or turn off selectedOnly) 請選取要匯出的內容 (或關閉 selectedOnly)
+@msg select text to redefine the style from 請選取用來重新定義樣式的文字
+@msg select the art and, on top of it, the mask object 請選取圖稿,以及位於其上方的遮色片物件
+@msg select the art for the design 請選取設計要使用的圖稿
+@msg select the art for the pattern tile 請選取圖樣拼貼要使用的圖稿
+@msg select the art to add first 請先選取要加入的圖稿
+@msg select the art to collect (or give its ids) 請選取要收集的圖稿 (或提供其 ids)
+@msg select the content and a path on top 請選取內容以及位於上方的路徑
+@msg select the images to relink, or pass ids 請選取要重新連結的影像,或傳入 ids
+@msg select the images, or pass ids 請選取影像,或傳入 ids
+@msg select the linked images or placed documents, or pass ids 請選取連結的影像或置入的文件,或傳入 ids
+@msg select the object to copy from 請選取要從中複製的物件
+@msg select the objects (or give their ids) 請選取物件 (或提供其 ids)
+@msg select the objects to attach to the path 請選取要附加到路徑的物件
+@msg select the objects to bind 請選取要綁定的物件
+@msg select two or more objects to intertwine 請選取兩個以上要交織的物件
+@msg select two or more slices to combine 請選取兩個以上要合併的切片
+@msg select type on a path 請選取路徑文字
+@msg selection `{m}`: one or each selection `{m}`:應為 one 或 each
+@msg settings is an object of document.exportForScreens params settings 是 document.exportForScreens 參數的物件
+@msg settings must be an object settings 必須是物件
+@msg show `{show}`: all, missing, modified or embedded show `{show}`:應為 all、missing、modified 或 embedded
+@msg show must be true or false show 必須是 true 或 false
+@msg size must be "small", "medium", "large" or a number of points over 0 up to {_1} size 必須是 "small"、"medium"、"large",或大於 0 且不超過 {_1} 的點數
+@msg size must be positive size 必須是正值
+@msg sizeMode is "fixed" or "pressure", not `{other}` sizeMode 是 "fixed" 或 "pressure",而非 `{other}`
+@msg sizeMode is "fixed" or "pressure", not {other} sizeMode 是 "fixed" 或 "pressure",而非 {other}
+@msg sizeVariation must be 0 or more sizeVariation 必須大於或等於 0
+@msg smallCaps must be true or false smallCaps 必須是 true 或 false
+@msg sort `{x}`: name, kind or status sort `{x}`:應為 name、kind 或 status
+@msg source paths are too large for Shaper 來源路徑對 Shaper 而言過大
+@msg spot, mode and group apply to solid colours only spot、mode 和 group 只適用於純色
+@msg status `{s}`: linked, embedded, missing or modified status `{s}`:應為 linked、embedded、missing 或 modified
+@msg steps must be 1..1000 steps 必須是 1..1000
+@msg steps must be 1..{MAX_STEPS} steps 必須是 1..{MAX_STEPS}
+@msg stop {i}: `plates` must be four numbers from 0 to 1 (C, M, Y, K) 色標 {i}:`plates` 必須是四個 0 到 1 之間的數字 (C、M、Y、K)
+@msg stopped before the archive was finished 在封存檔完成之前已停止
+@msg strokeMode must be within|along|across, got {m} strokeMode 必須是 within|along|across,收到 {m}
+@msg strokeOptions must be an object of stroke.set options strokeOptions 必須是 stroke.set 選項的物件
+@msg strokeWidth must be a positive number of pixels strokeWidth 必須是正的像素數
+@msg subpath {si} is not an open subpath 子路徑 {si} 不是開放的子路徑
+@msg subpaths required when joining the same path 連接同一條路徑時必須提供 subpaths
+@msg swatchConflict must be "merge" or "add", not {v} swatchConflict 必須是 "merge" 或 "add",而非 {v}
+@msg text must be {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?} text 必須是 {characterSet?, platform?, removeLineReturns?, removeParagraphReturns?, replaceSpaces?}
+@msg text transform is collapsed or unrepresentable 文字變形已塌陷或無法表示
+@msg text.replaceSpaces must be a whole number from 2 to 100 text.replaceSpaces 必須是 2 到 100 之間的整數
+@msg text.{key} `{v}`: use "{_1}" or "{_2}" text.{key} `{v}`:請使用 "{_1}" 或 "{_2}"
+@msg textureImage: give {path} or {name, dataBase64} of an image file, or a texture as effect.list gives it textureImage:請提供影像檔案的 {path} 或 {name, dataBase64},或 effect.list 所提供的紋理
+@msg textureImage: {e} textureImage:{e}
+@msg that makes more than {MAX_PIECES} slices 這會產生超過 {MAX_PIECES} 個切片
+@msg that object is not in the overlap 該物件不在重疊處
+@msg the Affinity document has no page with a size Affinity 文件中沒有具有大小的頁面
+@msg the CMYK image couldn't be written 無法寫入 CMYK 影像
+@msg the PDF security settings were saved without their passwords: give openPassword or permissionsPassword again, or security without passwordRequired to export unprotected PDF 安全性設定儲存時未包含密碼:請再次提供 openPassword 或 permissionsPassword,或提供不含 passwordRequired 的 security 以匯出不受保護的檔案
+@msg the RTF file has too many paragraphs or formatting changes to place as type (at most {MAX_PARAGRAPHS} paragraphs and {MAX_RUNS} runs) RTF 檔案的段落或格式變更過多,無法置入為文字 (最多 {MAX_PARAGRAPHS} 個段落和 {MAX_RUNS} 個連續格式區段)
+@msg the RTF file is too large to place as type (at most {_1} MB) RTF 檔案過大,無法置入為文字 (最多 {_1} MB)
+@msg the SVG expands to more than {0} nested SVG images SVG 展開後超過 {0} 個巢狀 SVG 影像
+@msg the SVG expands to more than {0} objects SVG 展開後超過 {0} 個物件
+@msg the SVG has no drawable objects SVG 中沒有可繪製的物件
+@msg the SVGZ file unpacks to more than {_1} MB SVGZ 檔案解壓縮後超過 {_1} MB
+@msg the Word document has more than {MAX_PAGES} pages of properties, too many to place Word 文件的屬性超過 {MAX_PAGES} 頁,數量過多而無法置入
+@msg the Word document has more than {MAX_RUNS} runs of character or paragraph properties, too many to place Word 文件的字元或段落屬性超過 {MAX_RUNS} 個連續格式區段,數量過多而無法置入
+@msg the Word document has more than {_1} MB of formatting properties, too many to place Word 文件的格式屬性超過 {_1} MB,數量過多而無法置入
+@msg the Word document has too much text or formatting to place as type (at most {MAX_PARAGRAPHS} paragraphs, {MAX_RUNS} runs and {_1} MB of text) Word 文件的文字或格式過多,無法置入為文字 (最多 {MAX_PARAGRAPHS} 個段落、{MAX_RUNS} 個連續格式區段和 {_1} MB 的文字)
+@msg the Word document has too much text to place as type (at most {_1} MB) Word 文件的文字過多,無法置入為文字 (最多 {_1} MB)
+@msg the Word document is encrypted or protected with a password; save it without one to place it Word 文件已加密或受密碼保護;請儲存為沒有密碼的檔案再置入
+@msg the Word document is too large to place (its {name} stream holds more than {_1} MB) Word 文件過大,無法置入 (其 {name} 資料流超過 {_1} MB)
+@msg the Word document was last saved with fast save, which VectorCraft doesn't read; save it again with fast save turned off Word 文件上次是以快速儲存方式儲存,VectorCraft 無法讀取;請關閉快速儲存後再儲存一次
+@msg the Word document was saved by Word 6.0 or Word 95, which VectorCraft doesn't read; save it as a Word 97-2003 document Word 文件是由 Word 6.0 或 Word 95 儲存,VectorCraft 無法讀取;請儲存為 Word 97-2003 文件
+@msg the Word document's text is made of more than {MAX_PIECES} pieces, too many to place Word 文件的文字由超過 {MAX_PIECES} 個片段組成,數量過多而無法置入
+@msg the action set ids are not a list 動作組 ID 不是清單
+@msg the active paint is not a gradient 作用中的上色不是漸層
+@msg the artboards don't fit on the canvas: fewer artboards, less spacing or a smaller size 工作區域無法容納於畫布:請減少工作區域、縮小間距或縮小尺寸
+@msg the character's size can't be measured 無法測量字元的大小
+@msg the compound shape is too complex to expand 複合形狀過於複雜,無法展開
+@msg the container of the listed objects is gone 所列物件的容器已不存在
+@msg the copy `{user_id}` can't be read as a library 複本 `{user_id}` 無法讀取為資料庫
+@msg the copy would leave the canvas 複本會超出畫布
+@msg the cut cannot be made on this art: {e} 無法在此圖稿上進行切割:{e}
+@msg the data isn't an EMF or WMF picture 資料不是 EMF 或 WMF 圖片
+@msg the data isn't an EPS or PostScript picture 資料不是 EPS 或 PostScript 圖片
+@msg the deferred paste is no longer available 延後的貼上已無法使用
+@msg the design is too big to draw at every data point of this graph 設計過大,無法在此圖表的每個資料點繪製
+@msg the design is too big to draw in every column of this graph 設計過大,無法在此圖表的每個直條中繪製
+@msg the dialog it was opened over is no longer open 它開啟時所在的對話框已不再開啟
+@msg the dictionary stack overflowed 字典堆疊溢位
+@msg the editing data's hash has no place 編輯資料的雜湊值沒有存放位置
+@msg the entities the document declares expand to too much text 文件宣告的實體展開後的文字過多
+@msg the envelope's content has no bounds 封套的內容沒有邊界
+@msg the envelope's mesh is malformed 封套的網格格式不正確
+@msg the file holds no `variableSets` element 檔案中沒有 `variableSets` 元素
+@msg the file is larger than {_1} MB 檔案大於 {_1} MB
+@msg the file is too large 檔案過大
+@msg the file was updated or encrypted already 檔案已經更新或加密
+@msg the first character of a line has no character before it to kern: it moves only up and down (dy) 一行的第一個字元前面沒有可調整字距的字元:它只能上下移動 (dy)
+@msg the font {_1} is not available 無法使用字體 {_1}
+@msg the format has no file extension 此格式沒有副檔名
+@msg the graphic can't be read: {e} 無法讀取圖形:{e}
+@msg the graphic is empty 圖形為空
+@msg the graphic's data is damaged 圖形的資料已損毀
+@msg the graphic's document has no artboard 圖形的文件沒有工作區域
+@msg the header `{cell}` for `{name}` would read back as the variable `{other}`: rename one of them `{name}` 的標題 `{cell}` 讀回時會成為變數 `{other}`:請重新命名其中一個
+@msg the image `{path}` isn't followed: {r} 未跟隨影像 `{path}` 的連結:{r}
+@msg the image has no embedded pixels 影像沒有內嵌的像素
+@msg the image is empty 影像為空
+@msg the image is fully transparent 影像完全透明
+@msg the image is not on an artboard; pass rect 影像不在工作區域上;請傳入 rect
+@msg the image's pixels are missing 影像的像素遺失
+@msg the image's pixels aren't in the document 影像的像素不在文件中
+@msg the image's pixels can't be read: {e} 無法讀取影像的像素:{e}
+@msg the image's transparency doesn't fit it 影像的透明度資料與影像不相符
+@msg the job would print more than {MAX_PAGES} pages 此工作會列印超過 {MAX_PAGES} 頁
+@msg the job would print more than {_1} pages 此工作會列印超過 {_1} 頁
+@msg the layers hold no art to mask 圖層中沒有可套用遮色片的圖稿
+@msg the layout moved 版面已移動
+@msg the library has a group `{name}` already 資料庫中已有群組 `{name}`
+@msg the library has no group `{name}` 資料庫中沒有群組 `{name}`
+@msg the library has no item `{item}` 資料庫中沒有項目 `{item}`
+@msg the library has no item `{other}` 資料庫中沒有項目 `{other}`
+@msg the library holds {MAX_GROUPS} groups already 資料庫已有 {MAX_GROUPS} 個群組
+@msg the library holds {MAX_ITEMS} items of this kind already 資料庫已有 {MAX_ITEMS} 個這類項目
+@msg the name is empty 名稱為空
+@msg the object has no fill, stroke or effect 物件沒有填色、筆畫或效果
+@msg the object has no graphic style (pass `name`) 物件沒有繪圖樣式 (請傳入 `name`)
+@msg the objects don't match the plan 物件與計畫不相符
+@msg the open and permissions passwords must differ 開啟密碼和權限密碼必須不同
+@msg the operand stack overflowed 運算元堆疊溢位
+@msg the page has no area 頁面沒有面積
+@msg the page's type in an unread object's box isn't in its page art 頁面中位於未讀取物件方框內的文字不在其頁面圖稿中
+@msg the paint behind the proxy is not a gradient 代理所代表的上色不是漸層
+@msg the parent must be a layer or sublayer 父項必須是圖層或子圖層
+@msg the path can't be extended 路徑無法延伸
+@msg the path can't be inside one of the objects to attach 路徑不能位於要附加的物件之內
+@msg the path has no segments to measure 路徑沒有可測量的線段
+@msg the path is empty 路徑為空
+@msg the path is too short to shorten that much 路徑過短,無法縮短這麼多
+@msg the picture has no objects 圖片中沒有物件
+@msg the program runs too long 程式執行時間過長
+@msg the program uses too much memory 程式使用過多記憶體
+@msg the rectangle must have a width and a height 矩形必須有寬度和高度
+@msg the rectangle needs a size and must lie on the canvas 矩形需要有大小,而且必須位於畫布上
+@msg the selected art has no width 所選圖稿沒有寬度
+@msg the selection draws nothing 選取範圍沒有繪製任何內容
+@msg the selection has no area 選取範圍沒有面積
+@msg the selection has no art to export 選取範圍中沒有可匯出的圖稿
+@msg the selection has no bounds 選取範圍沒有邊界
+@msg the selection has no graphic style 選取範圍沒有繪圖樣式
+@msg the slice would leave the canvas 切片會超出畫布
+@msg the spot colour couldn't be added 無法加入特別色
+@msg the subpath has no ends to extend (it is closed or a single point) 子路徑沒有可延伸的端點 (它是封閉路徑或單一的點)
+@msg the symbols are nested too deeply to expand their instances 符號的巢狀層次過深,無法展開其範例
+@msg the text file is too large to place as type (at most {_1} MB) 文字檔案過大,無法置入為文字 (最多 {_1} MB)
+@msg the text is empty 文字為空
+@msg the text is over {_1} MB 文字超過 {_1} MB
+@msg the top object must be a path 最上方的物件必須是路徑
+@msg the trace was cancelled 描圖已取消
+@msg the trailer has no size 檔尾沒有大小
+@msg the type object is flattened to nothing 文字物件平面化後沒有任何內容
+@msg the value of axis `{tag}` must be a number or null 軸 `{tag}` 的值必須是數字或 null
+@msg the variable library declares more than {MAX_LIBRARY_DATASETS} data sets 變數資料庫宣告了超過 {MAX_LIBRARY_DATASETS} 個資料集
+@msg the variable library declares more than {MAX_LIBRARY_ENTITIES} entities 變數資料庫宣告了超過 {MAX_LIBRARY_ENTITIES} 個實體
+@msg the variable library declares more than {MAX_LIBRARY_VARIABLES} variables 變數資料庫宣告了超過 {MAX_LIBRARY_VARIABLES} 個變數
+@msg the variable library declares no variables 變數資料庫未宣告任何變數
+@msg the variable library is larger than {MAX_LIBRARY_BYTES} bytes 變數資料庫大於 {MAX_LIBRARY_BYTES} 位元組
+@msg the variable library is not UTF-8 變數資料庫不是 UTF-8
+@msg the variable library is not well-formed XML: {e} 變數資料庫不是格式正確的 XML:{e}
+@msg the variable library nests elements more than {MAX_LIBRARY_NESTING} deep 變數資料庫的元素巢狀層次超過 {MAX_LIBRARY_NESTING} 層
+@msg the variable library's document type declaration holds something other than entities of plain text 變數資料庫的文件類型宣告包含純文字實體以外的內容
+@msg the variable library's entities expand to more than {MAX_LIBRARY_BYTES} bytes 變數資料庫的實體展開後超過 {MAX_LIBRARY_BYTES} 位元組
+@msg the window has no dock 視窗沒有停駐區
+@msg the written PDF has no catalog 已寫入的 PDF 沒有目錄
+@msg the written PDF has no cross-reference table 已寫入的 PDF 沒有交互參照表
+@msg the written PDF's metadata can't be edited 無法編輯已寫入 PDF 的中繼資料
+@msg there are no colour swatches to draw 沒有可繪製的顏色色票
+@msg there is already a design named `{name}` 已有名為 `{name}` 的設計
+@msg there is already a design named `{to}` 已有名為 `{to}` 的設計
+@msg there is already a toolbar named `{name}` 已有名為 `{name}` 的工具列
+@msg there is already a toolbar named `{to}` 已有名為 `{to}` 的工具列
+@msg there is no library: create one first 沒有資料庫:請先建立一個
+@msg these paths are too degenerate for Shaper 這些路徑過於退化,Shaper 無法處理
+@msg thumbnail: no page {v} (the {what} has {count}) thumbnail:沒有頁面 {v} ({what} 有 {count})
+@msg tileRange of {what}: {e} {what} 的 tileRange:{e}
+@msg title can't be empty title 不能為空
+@msg to (the new name) is required 必須提供 to (新名稱)
+@msg to must be fill or stroke to 必須是 fill 或 stroke
+@msg to must be smooth or corner to 必須是 smooth 或 corner
+@msg too large for a zip archive: package to a folder 對 zip 封存檔而言過大:請封裝到資料夾
+@msg too many Shaper edits Shaper 編輯過多
+@msg too many `points` (at most 100000) `points` 過多 (最多 100000 個)
+@msg too many characters: at most {MAX_CHARS} 字元過多:最多 {MAX_CHARS} 個
+@msg too many dots for this size: lower the frequency 此尺寸的網點過多:請降低頻率
+@msg too many filters over one file 單一檔案上的濾鏡過多
+@msg too many overlapping paths for Shaper 重疊的路徑過多,Shaper 無法處理
+@msg too many saved clips 已儲存的剪裁過多
+@msg too many saved graphics states 已儲存的圖形狀態過多
+@msg toolbar (a toolbar's name) must be a string toolbar (工具列的名稱) 必須是字串
+@msg toolbar name can't be empty 工具列名稱不能為空
+@msg transparentImages {v}: "flatten" (the default) or "omit" transparentImages {v}:應為 "flatten" (預設值) 或 "omit"
+@msg two saved selections would be named `{_1}` 會有兩個已儲存的選取範圍都名為 `{_1}`
+@msg type dimension cannot be represented with this text transform 文字尺寸無法以此文字變形表示
+@msg type dimension must be finite 文字尺寸必須是有限值
+@msg unexpected resources 非預期的資源
+@msg unknown CMYK profile `{name}` 未知的 CMYK 描述檔 `{name}`
+@msg unknown File Info field `{k}` 未知的「檔案資訊」欄位 `{k}`
+@msg unknown OpenType feature `{t}` in featureChanges (the tags `features` takes) featureChanges 中有未知的 OpenType 功能 `{t}` (應為 `features` 接受的標籤)
+@msg unknown PDF preset `{name}` (presets: {_1}) 未知的 PDF 預設集 `{name}` (預設集:{_1})
+@msg unknown Warp style 未知的彎曲樣式
+@msg unknown alignToPath `{a}` 未知的 alignToPath `{a}`
+@msg unknown arrowhead {v} 未知的箭頭 {v}
+@msg unknown artboardLayout key `{k}` (layout, columns, spacing, rightToLeft) 未知的 artboardLayout 鍵 `{k}` (layout、columns、spacing、rightToLeft)
+@msg unknown attribute `{attribute}` 未知的屬性 `{attribute}`
+@msg unknown attribute `{k}` 未知的屬性 `{k}`
+@msg unknown axis `{other}` (vertical|horizontal|free) 未知的軸 `{other}` (vertical|horizontal|free)
+@msg unknown blend mode `{b}` 未知的混合模式 `{b}`
+@msg unknown blend mode {b} 未知的混合模式 {b}
+@msg unknown brush type `{o}` 未知的筆刷類型 `{o}`
+@msg unknown category `{c}` 未知的類別 `{c}`
+@msg unknown category `{c}` ({_1}) 未知的類別 `{c}` ({_1})
+@msg unknown character `{c}` 未知的字元 `{c}`
+@msg unknown character style number field 未知的字元樣式數字欄位
+@msg unknown effect `{e}` 未知的效果 `{e}`
+@msg unknown end {e} 未知的端點 {e}
+@msg unknown flattenerPreset `{name}` (High Resolution, Medium Resolution, Low Resolution or a saved one: see flattener.presets.list) 未知的 flattenerPreset `{name}` (High Resolution、Medium Resolution、Low Resolution 或已儲存的預設集:請參閱 flattener.presets.list)
+@msg unknown format `{f}` (vcswatches, gpl, ase or css) 未知的格式 `{f}` (vcswatches、gpl、ase 或 css)
+@msg unknown from {f} 未知的 from {f}
+@msg unknown graph type `{t}` 未知的圖表類型 `{t}`
+@msg unknown grid colours `{name}` (one of {_1}) 未知的格點顏色 `{name}` (應為 {_1} 其中之一)
+@msg unknown gridType `{t}` 未知的 gridType `{t}`
+@msg unknown highlight `{v}` 未知的 highlight `{v}`
+@msg unknown intent `{i}` 未知的色彩比對方式 `{i}`
+@msg unknown issue kind {id} 未知的問題種類 {id}
+@msg unknown issue kind {v} 未知的問題種類 {v}
+@msg unknown knockout state {k} 未知的去底狀態 {k}
+@msg unknown language `{language}` (one of {_1}) 未知的語言 `{language}` (應為 {_1} 其中之一)
+@msg unknown language `{l}` (one of {_1}) 未知的語言 `{l}` (應為 {_1} 其中之一)
+@msg unknown method `{m}` ({_1}) 未知的方法 `{m}` ({_1})
+@msg unknown mode `{other}` (gray, rgb, hsb, lab, cmyk or web) 未知的模式 `{other}` (gray、rgb、hsb、lab、cmyk 或 web)
+@msg unknown mode `{other}` (mono|cmyk) 未知的模式 `{other}` (mono|cmyk)
+@msg unknown mode `{other}` (rgb|cmyk|gray|global) 未知的模式 `{other}` (rgb|cmyk|gray|global)
+@msg unknown mode `{other}` (set, add, subtract) 未知的模式 `{other}` (set、add、subtract)
+@msg unknown mode `{o}` 未知的模式 `{o}`
+@msg unknown mode {m} 未知的模式 {m}
+@msg unknown model `{other}` 未知的 model `{other}`
+@msg unknown model `{other}` (keep, new or document) 未知的 model `{other}` (keep、new 或 document)
+@msg unknown op {o} 未知的 op {o}
+@msg unknown order `{o}`: toggle, front, forward, backward or back 未知的 order `{o}`:應為 toggle、front、forward、backward 或 back
+@msg unknown overprints `{v}` (preserve, simulate or discard) 未知的 overprints `{v}` (preserve、simulate 或 discard)
+@msg unknown panel 未知的面板
+@msg unknown pieLegend `{v}` (none, standard or wedges) 未知的 pieLegend `{v}` (none、standard 或 wedges)
+@msg unknown piePosition `{v}` (even, ratio or stacked) 未知的 piePosition `{v}` (even、ratio 或 stacked)
+@msg unknown pieSort `{v}` (none, all or first) 未知的 pieSort `{v}` (none、all 或 first)
+@msg unknown plate `{n}` 未知的色版 `{n}`
+@msg unknown preference `{k}` 未知的偏好設定 `{k}`
+@msg unknown profile {id} (see stroke.widthProfile.list) 未知的描述檔 {id} (請參閱 stroke.widthProfile.list)
+@msg unknown proof target `{t}` 未知的校樣目標 `{t}`
+@msg unknown quotes key `{k}` (double, single) 未知的 quotes 鍵 `{k}` (double、single)
+@msg unknown resolution `{v}` 未知的解析度 `{v}`
+@msg unknown rule `{rule}` ({_1}) 未知的規則 `{rule}` ({_1})
+@msg unknown rule `{r}` (see color.harmony) 未知的規則 `{r}` (請參閱 color.harmony)
+@msg unknown setting `{k}` 未知的設定 `{k}`
+@msg unknown shape `{v}` (circle|ellipse|square|diamond|line) 未知的形狀 `{v}` (circle|ellipse|square|diamond|line)
+@msg unknown spacing `{o}` (smooth|steps|distance) 未知的 spacing `{o}` (smooth|steps|distance)
+@msg unknown tileType `{t}` ({_1}) 未知的 tileType `{t}` ({_1})
+@msg unknown units 未知的單位
+@msg unknown units `{u}` 未知的單位 `{u}`
+@msg unknown value axis `{v}` (left, right or both) 未知的數值軸 `{v}` (left、right 或 both)
+@msg unknown variation `{v}` (tintsShades|warmCool|vividMuted) 未知的 variation `{v}` (tintsShades|warmCool|vividMuted)
+@msg unknown warp style `{s}` 未知的彎曲樣式 `{s}`
+@msg unknown {key} `{v}` (none, short or full) 未知的 {key} `{v}` (none、short 或 full)
+@msg unknown {key} profile `{n}` 未知的 {key} 描述檔 `{n}`
+@msg unknown {k} key `{key}` (size, position) 未知的 {k} 鍵 `{key}` (size、position)
+@msg unreadable object 無法讀取的物件
+@msg unsafe destination file name 目的地檔案名稱不安全
+@msg use paths or compound paths 請使用路徑或複合路徑
+@msg useArtboards must be true or false, not {v} useArtboards 必須是 true 或 false,而非 {v}
+@msg value for `{var}` must match its variable kind `{var}` 的值必須符合其變數種類
+@msg variable `{_1}` is already defined 變數 `{_1}` 已定義
+@msg variable `{name}` has no `trait` 變數 `{name}` 沒有 `trait`
+@msg variable `{name}` is already defined 變數 `{name}` 已定義
+@msg variable `{name}` is declared twice 變數 `{name}` 宣告了兩次
+@msg variable `{to}` already exists 變數 `{to}` 已存在
+@msg view is required 必須提供 view
+@msg view must be one of {_1} view 必須是 {_1} 其中之一
+@msg warpStyle can only edit an applied Warp effect warpStyle 只能編輯已套用的彎曲效果
+@msg width and height must be more than 0 and at most {limit} 寬度和高度必須大於 0 且不超過 {limit}
+@msg width factors must be finite 寬度係數必須是有限值
+@msg width must be positive width 必須是正值
+@msg width out of range 寬度超出範圍
+@msg width/height must be > 0 width/height 必須 > 0
+@msg x and y must be finite numbers x 和 y 必須是有限的數字
+@msg x and y must be given together x 和 y 必須一起提供
+@msg zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End) zone 需要 anchor:請以 anchor 停駐在面板旁,或以 side (Start 或 End) 停駐在新的停駐欄
+@msg zoom must be positive zoom 必須是正值
+@msg {C}: no effect at index {list} {C}:索引 {list} 處沒有效果
+@msg {DEFAULT_PRESET} is protected and stays {DEFAULT_PRESET} 受到保護,會保留
+@msg {DEFAULT_PRESET} is protected: save the settings under another name {DEFAULT_PRESET} 受到保護:請以其他名稱儲存設定
+@msg {LABEL} produced an empty result {LABEL} 產生了空的結果
+@msg {_1} MB of document, at most {_2} MB 文件為 {_1} MB,上限為 {_2} MB
+@msg {_1} MB of images, at most {_2} MB 影像為 {_1} MB,上限為 {_2} MB
+@msg {_1} bytes of RGBA data for a {width} × {height} image {width} × {height} 影像的 RGBA 資料為 {_1} 位元組
+@msg {_1} can't be read, or changed since it was read: relink or update it first {_1} 無法讀取,或在讀取後已變更:請先重新連結或更新
+@msg {_1} can't go into {into} {_1} 不能放入 {into}
+@msg {_1} can't limit printing to low resolution: choose PDF 1.4 or later, or allow high-resolution printing {_1} 無法將列印限制為低解析度:請選擇 PDF 1.4 以上版本,或允許高解析度列印
+@msg {_1} files can't be opened yet 尚無法開啟 {_1} 檔案
+@msg {_1} files can't be password-protected: remove the passwords or choose no standard {_1} 檔案不能以密碼保護:請移除密碼或不要選擇標準
+@msg {_1} files can't be {_2} {_1} 檔案不能 {_2}
+@msg {_1} files can't carry editing data: turn off preserveEditing {_1} 檔案不能包含編輯資料:請關閉 preserveEditing
+@msg {_1} files can't have PDF layers: turn off createLayers {_1} 檔案不能有 PDF 圖層:請關閉 createLayers
+@msg {_1} files need a creation date, and this platform has no clock to give one {_1} 檔案需要建立日期,而此平台沒有可提供日期的時鐘
+@msg {_1} isn't a path {_1} 不是路徑
+@msg {_1} options: {e} {_1} 選項:{e}
+@msg {_1} paints nothing to rasterize {_1} 沒有繪製任何可點陣化的內容
+@msg {_1} passwords can have up to {MAX_LEGACY_PASSWORD} ASCII characters (PDF 1.7 and later take any) {_1} 密碼最多 {MAX_LEGACY_PASSWORD} 個 ASCII 字元 (PDF 1.7 以上版本接受任何字元)
+@msg {_1} with linked images or an image map writes several files: use document.export 含有連結影像或影像地圖的 {_1} 會寫入多個檔案:請使用 document.export
+@msg {_1} writes one file per artboard here: use document.export, or name one artboard {_1} 在這裡會為每個工作區域寫入一個檔案:請使用 document.export,或指定一個工作區域
+@msg {_1} × {_2} pixels is too large for {_3} (at most {max} pixels a side): lower the scale {_1} × {_2} 像素對 {_3} 而言過大 (每邊最多 {max} 像素):請降低縮放比例
+@msg {_1}: belongs to another account {_1}:屬於另一個帳戶
+@msg {_1}: not a folder of its own {_1}:不是獨立的資料夾
+@msg {_1}: not a plain file {_1}:不是一般檔案
+@msg {dest}: {e} {dest}:{e}
+@msg {doc} has no folder {doc} 沒有資料夾
+@msg {file} isn't linked {file} 不是連結的檔案
+@msg {folder} is not a package folder {folder} 不是封裝資料夾
+@msg {folder}: more than {max} files and folders {folder}:超過 {max} 個檔案和資料夾
+@msg {folder}: {e} {folder}:{e}
+@msg {g} is not a Live Paint group {g} 不是即時上色群組
+@msg {id} is not a slice {id} 不是切片
+@msg {key} must be 0–{MAX_ADD_AROUND} pt {key} 必須是 0–{MAX_ADD_AROUND} pt
+@msg {key} must be [x,y] {key} 必須是 [x,y]
+@msg {key} must be a percentage {min}..100 {key} 必須是 {min}..100 的百分比
+@msg {key} must be a positive number, not {n} {key} 必須是正數,而非 {n}
+@msg {key} must be a positive number, not {v} {key} 必須是正數,而非 {v}
+@msg {key} must be one of {_1} {key} 必須是 {_1} 其中之一
+@msg {key} must be {_1}, got {s} {key} 必須是 {_1},收到 {s}
+@msg {key} must name a floating panel {key} 必須指定浮動面板
+@msg {key} must name a panel in another group {key} 必須指定另一個群組中的面板
+@msg {key} must name a panel that shows {key} 必須指定顯示中的面板
+@msg {key} must name a registered panel {key} 必須指定已註冊的面板
+@msg {kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency) {kind} 檔案沒有透明度,而檔案中會有 {what}:請先將它平面化 (object.flattenTransparency)
+@msg {kind} images can't be opened (see document.formats) 無法開啟 {kind} 影像 (請參閱 document.formats)
+@msg {k} `{v}` is not one of the choices {k} `{v}` 不是選項之一
+@msg {k} has control characters{_1} {k} 含有控制字元{_1}
+@msg {k} is too long (at most {MAX_TEXT} characters) {k} 過長 (最多 {MAX_TEXT} 個字元)
+@msg {k} must be > 0 {k} 必須 > 0
+@msg {k} must be a number (pt) or a length such as "210 mm" {k} 必須是數字 (pt) 或 "210 mm" 這類長度
+@msg {k} must be a number from {min} to {max} {k} 必須是 {min} 到 {max} 之間的數字
+@msg {k} must be a number or "auto" {k} 必須是數字或 "auto"
+@msg {k} must be a string {k} 必須是字串
+@msg {k} must be text {k} 必須是文字
+@msg {k} must be true or false {k} 必須是 true 或 false
+@msg {k} must be {{size?, position?}} {k} 必須是 {{size?, position?}}
+@msg {k}: can't read `{s}` as a length {k}:無法將 `{s}` 讀取為長度
+@msg {label} must be a number at most {max} {label} 必須是不超過 {max} 的數字
+@msg {label} must be a number from -1296 to 1296 {label} 必須是 -1296 到 1296 之間的數字
+@msg {label} must be a number from 1 to 10000 {label} 必須是 1 到 10000 之間的數字
+@msg {name} `{s}`: {ids} {name} `{s}`:{ids}
+@msg {name} is built in and can't be deleted {name} 是內建項目,無法刪除
+@msg {name} must be {lo}–{hi}{unit} (got {v}) {name} 必須是 {lo}–{hi}{unit} (收到 {v})
+@msg {name}: CRC or size mismatch {name}:CRC 或大小不相符
+@msg {name}: only this Affinity file's embedded preview could be read; use File › Open to see it with its warning, or export SVG or PDF from Affinity before placing {name}:只能讀取此 Affinity 檔案的內嵌預視;請使用「檔案 › 開啟」查看它及其警告,或在置入前先從 Affinity 匯出 SVG 或 PDF
+@msg {name}: {e} {name}:{e}
+@msg {native}; its embedded preview can't be read either ({e}) {native};其內嵌預視也無法讀取 ({e})
+@msg {std} files are CMYK, and the file would have colours in {_1} {std} 檔案為 CMYK,而檔案中會有 {_1} 的顏色
+@msg {std} files embed their fonts, and the file would have one that isn't {std} 檔案會內嵌字體,而檔案中會有一個未內嵌的字體
+@msg {target} is the file this document was read from, and reading it left things out ({what}): writing over it would lose them for good. Write another file instead, or pass acknowledgeLoss: true to replace it anyway {target} 是讀取此文件的來源檔案,而讀取時略過了部分內容 ({what}):覆寫它會永久失去這些內容。請改為寫入另一個檔案,或傳入 acknowledgeLoss: true 以強制取代
+@msg {ty} brushes need selected art (or params.{k}) {ty} 筆刷需要選取的圖稿 (或 params.{k})
+@msg {v} is not an asset id {v} 不是資產 ID
+@msg {what} can't be intertwined {what} 無法交織
+@msg {what} doesn't fit the paper with its bleed and marks {what} 連同出血和標記無法容納於紙張
+@msg {what} would print on more than {MAX_TILES} tiles: scale it down {what} 會列印在超過 {MAX_TILES} 個拼貼上:請縮小
diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs
index 83f463d668..924950c1cb 100644
--- a/crates/ui-egui/src/io.rs
+++ b/crates/ui-egui/src/io.rs
@@ -26,13 +26,13 @@ pub(crate) use export::{
export_files, exported_with_notes, file_url, open_in_app, open_original, reveal_path, run_to_file, save_command_output_named,
};
pub(crate) use open::report_installed;
-pub(crate) use open::{EXPORT_PRESETS, import_presets, open_with_on, opened, show_opened};
+pub(crate) use open::{EXPORT_PRESETS, import_presets, open_notes, open_with_on, opened, opened_after_notice, show_opened};
pub use open::{
MAX_RECENT_FILES, OpensAs, ask_revert, install_module, install_plugin, new_from_template, note_recent, open_bytes, open_dialog, open_document,
open_path, open_path_with, open_reporting, open_with, opens_as, place_bytes, recent_files, report_open_error,
};
#[cfg(test)]
-pub(crate) use open::{open_file, opened_after_notice, opened_with_notes};
+pub(crate) use open::{open_file, opened_with_notes};
pub(crate) use save::save_current;
pub use save::{ask_format_options, ask_pdf_options, remember_active_artboard, remember_view, save, save_pdf};
pub(crate) use save::{export_path, is_web, notes_of, report_saved, show_with_notes, target_path, write_named, write_reporting, write_to};
diff --git a/crates/ui-egui/src/io/open.rs b/crates/ui-egui/src/io/open.rs
index 1dec3a1fc7..e0e4e4c404 100644
--- a/crates/ui-egui/src/io/open.rs
+++ b/crates/ui-egui/src/io/open.rs
@@ -162,7 +162,7 @@ pub(crate) fn show_opened(app: &mut VectorcraftApp, notice: Option, note
/// The notes of the open `r`, those of what reading the file left out (its document's
/// `import_losses`) first, each group in the order the import gave them.
-fn open_notes(app: &VectorcraftApp, r: &Value) -> Vec {
+pub(crate) fn open_notes(app: &VectorcraftApp, r: &Value) -> Vec {
let notes = notes_of(r);
let index = r["index"].as_u64().and_then(|i| usize::try_from(i).ok());
let losses = index.and_then(|i| app.session.documents().get(i)).map(|st| st.import_losses.as_slice()).unwrap_or_default();
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
index 4ef21414ee..50ed3d8ee5 100644
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -235,6 +235,12 @@ pub struct VectorcraftApp {
/// icon and title. Set by the desktop app when it opens the window; `false` for embedders
/// without a window of their own.
pub desktop_frame: bool,
+ /// Linux and the BSDs: `VECTORCRAFT_TITLEBAR` is unset and the desktop would draw the window's
+ /// frame on this session with Preferences › User Interface › System Title Bar on or off while
+ /// VectorCraft Title Bar on Every Desktop is off, so the dialog says so under System Title Bar.
+ /// It stays `true` while VectorCraft Title Bar on Every Desktop gives the app the frame. Set by
+ /// the desktop app when it opens the window; `false` elsewhere.
+ pub desktop_keeps_frame: bool,
/// Last window title sent to the OS (`ViewportCommand::Title`, see `chrome::sync_window_title`):
/// sent again only when it changes, so idle frames don't spam the backend.
pub(crate) last_window_title: String,
diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs
index 14e5d053ef..5e05732b62 100644
--- a/crates/ui-egui/src/menus/run.rs
+++ b/crates/ui-egui/src/menus/run.rs
@@ -985,6 +985,8 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::panels::actions::new_action(app, p),
crate::panels::actions::ACTION_OPTIONS => crate::panels::actions::action_options(app, p),
crate::panels::actions::PLAY_ACTION => crate::panels::actions::play_action(app, p),
+ crate::panels::actions::PLAY_AT => crate::panels::actions::play_at(app, p),
+ crate::panels::actions::BUTTON_MODE => crate::panels::actions::button_mode(app, p),
crate::panels::actions::PLAYBACK_OPTIONS => crate::panels::actions::playback_options(app, p),
crate::panels::actions::START_RECORDING => crate::panels::actions::record(app, p),
crate::panels::actions::STOP_RECORDING => crate::panels::actions::stop(app, p),
diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs
index cd311d45a8..255d0836b5 100644
--- a/crates/ui-egui/src/menus/ui_commands.rs
+++ b/crates/ui-egui/src/menus/ui_commands.rs
@@ -1466,7 +1466,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
crate::panels::actions::NEW_ACTION,
"New Action",
"",
- "{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), which the panel does not show. Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
+ "{set: index, name, key?: 0-15, cmd?, shift?, color?: red|yellow|green|blue|violet} add an action named name (spaces trimmed from both ends) after the last action of the Actions panel's set at set (0 is the top set) and start recording into it; Stop Playing/Recording (ui.action.stop) ends the recording. key 1 to 15 is the function key F1 to F15 that plays the action (0 or null: none), with cmd (Ctrl, Command on macOS) and shift; color is its Button Mode color (null or \"\": none), the color of its button in Button Mode (ui.action.buttonMode). Action function keys take priority over command shortcuts and preserve the command's binding. On Windows F1, Ctrl+F4 and Ctrl+F6 cannot be used. The set becomes current and the new action is selected. The panel's Create New Action button and menu's New Action… (dialog `actionNew` with set, name, key, cmd, shift and color) run this on Record. While an action is recording, and for an index past the last set, an empty name, a key or color that is not one of these, or an action that would give the set more than 1,000 actions or an action set file larger than 8 MiB, it is an error, nothing is added and no recording starts → {set, action}",
),
(
crate::panels::actions::ACTION_OPTIONS,
@@ -1480,6 +1480,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{action: name, set?: name} play the Actions panel's action named action at the speed ui.action.playbackOptions sets; set names the set to look in (default: every set). Accelerated plays it as one undoable batch → what command.batch returns. Step by Step and Pause For play its first step now and the others one at a time, each its own undo step, until the last, ui.action.stop, or a step that comes due while a different document is active than after the step before → {results: [the first step's result], pending: the steps left}. An action with a stop, at any speed, plays up to the stop and waits until ui.action.continue or ui.action.stop; at Accelerated the steps between stops are one undoable batch each → {results, pending, atStop?: true when it already waits}. A name no action has, or one that more than one action in the sets looked in has, or an action played while another is still playing step by step or with pauses, is an error and nothing plays",
),
+ (
+ crate::panels::actions::PLAY_AT,
+ "Play Action",
+ "",
+ "{set: index, action: index} play the Actions panel's action at action in set (0 is the top set) as ui.action.play plays it, and return what ui.action.play returns. A click on an action's button in Button Mode runs this. A missing index, or one that is not a whole number or is past the last set or action, is an error and nothing plays",
+ ),
+ (
+ crate::panels::actions::BUTTON_MODE,
+ "Button Mode",
+ "",
+ "{enabled?: bool} with true, the Actions panel shows every action of every set, in set order and then action order, as a button with its name in its Action Options color (no color: an ordinary button), without set names, steps or checkmarks, and a click on a button plays its action (ui.action.playAt); with false, it shows its list again; left out or null, it switches to the other view. The panel menu's Button Mode runs this and shows a check mark while it is on. The view is kept with the UI preferences. Turning it on while an action records, or an enabled that is not true, false or null, is an error and the view stays → {enabled}",
+ ),
(
crate::panels::actions::PLAYBACK_OPTIONS,
"Playback Options",
diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs
index 630d81e95f..20a57b7d63 100644
--- a/crates/ui-egui/src/panels/actions.rs
+++ b/crates/ui-egui/src/panels/actions.rs
@@ -32,8 +32,9 @@ pub struct Action {
/// saved value that is not a function key loads as none.
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_key")]
pub key: Option,
- /// The action's Button Mode color, one of [`COLORS`], if one is assigned. The panel does not
- /// show it. A saved value that is not one of them loads as none.
+ /// The action's Button Mode color, one of [`COLORS`], if one is assigned: the color of its
+ /// button while the panel is in Button Mode ([`button_colors`]). A saved value that is not one
+ /// of them loads as none.
#[serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_color")]
pub color: Option,
}
@@ -144,6 +145,40 @@ impl FunctionKey {
/// The Button Mode colors an action can have, as (value, label).
pub const COLORS: [(&str, &str); 5] = [("red", "Red"), ("yellow", "Yellow"), ("green", "Green"), ("blue", "Blue"), ("violet", "Violet")];
+/// The fill of a Button Mode button in color `color`, one of [`COLORS`]: VectorCraft's own shades.
+fn button_fill(color: &str) -> Option {
+ let rgb = egui::Color32::from_rgb;
+ Some(match color {
+ "red" => rgb(0xd4, 0x3f, 0x3f),
+ "yellow" => rgb(0xf0, 0xc8, 0x3c),
+ "green" => rgb(0x3f, 0xa8, 0x5a),
+ "blue" => rgb(0x3d, 0x7b, 0xd6),
+ "violet" => rgb(0x8e, 0x5a, 0xd0),
+ _ => return None,
+ })
+}
+
+/// The relative luminance of `c`, as WCAG 2 defines it.
+fn luminance(c: egui::Color32) -> f32 {
+ let channel = |v: u8| {
+ let v = f32::from(v) / 255.0;
+ if v <= 0.040_45 { v / 12.92 } else { ((v + 0.055) / 1.055).powf(2.4) }
+ };
+ 0.2126 * channel(c.r()) + 0.7152 * channel(c.g()) + 0.0722 * channel(c.b())
+}
+
+/// The (fill, text) colors of the Button Mode button of an action with color `color`: a colored
+/// fill with black or white text, whichever contrasts more with it, or the theme's ordinary button
+/// for an action without a color.
+fn button_colors(color: Option<&str>, t: &Tokens) -> (egui::Color32, egui::Color32) {
+ match color.and_then(button_fill) {
+ // Black text contrasts more than white above this luminance.
+ Some(fill) if luminance(fill) > 0.179 => (fill, egui::Color32::BLACK),
+ Some(fill) => (fill, egui::Color32::WHITE),
+ None => (t.button, t.text_strong),
+ }
+}
+
/// Serde reader for [`Action::key`]: a value that is not a function key is none.
fn deserialize_key<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> {
let v = ::deserialize(d)?;
@@ -888,6 +923,37 @@ pub fn play_action(app: &mut VectorcraftApp, p: &Value) -> Result
}
}
+/// The command that plays an action by its place in the panel ([`play_at`]).
+pub const PLAY_AT: &str = "ui.action.playAt";
+
+/// `ui.action.playAt {set, action}`: plays the action at index `action` of set `set` (0 is the top
+/// set) at the Playback Options speed, as [`play`] does; a click on an action's button in Button
+/// Mode runs this. A missing index, or one that is not a whole number or is past the last set or
+/// action, is an error, and then nothing plays. → what [`play`] returns
+pub fn play_at(app: &mut VectorcraftApp, p: &Value) -> Result {
+ let index = |k: &str| p.get(k).and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok());
+ let (set, at) = index("set").zip(index("action")).ok_or("no such action")?;
+ play(app, set, at)
+}
+
+/// The command behind the panel menu's Button Mode ([`button_mode`]).
+pub const BUTTON_MODE: &str = "ui.action.buttonMode";
+
+/// `ui.action.buttonMode {enabled?}`: with `enabled: true` the Actions panel shows every action of
+/// every set as a button in its color ([`Action::color`]) that plays the action when clicked, and
+/// with `false` it shows its list of sets, actions and steps again; left out or null, it switches
+/// to the other view. The view is kept with the UI preferences. Turning Button Mode on while an
+/// action records is an error, and so is an `enabled` that is not true, false or null; then the
+/// view does not change. → {enabled}
+pub fn button_mode(app: &mut VectorcraftApp, p: &Value) -> Result {
+ let enabled = crate::menus::opt_bool(p, "enabled")?.unwrap_or(!app.ui.action_button_mode);
+ if enabled && !app.ui.action_button_mode && app.ui.recording.is_some() {
+ return Err("an action is already recording".into());
+ }
+ app.ui.action_button_mode = enabled;
+ Ok(json!({ "enabled": enabled }))
+}
+
/// The command that ends a recording ([`stop`]).
pub const STOP_RECORDING: &str = "ui.action.stop";
@@ -2007,9 +2073,18 @@ pub fn duplicate(app: &mut VectorcraftApp, p: &Value) -> Result {
/// The panel's (≡) menu.
pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
+ // Button Mode is off while an action records, so the Stop button the list shows stays in reach.
+ let buttons = app.ui.action_button_mode;
+ if widgets::menu_item(ui, tl!("Button Mode"), buttons || app.ui.recording.is_none(), buttons)
+ && let Err(e) = app.run(BUTTON_MODE, json!({}))
+ {
+ app.status(e);
+ }
+ ui.separator();
// A selected action counts only while its set is the current one, as the panel shows it: a set
- // deleted since leaves the stored pair pointing at another set's action.
- let selected = pstate::>(ui.ctx(), "actions-selected").filter(|&(s, _)| app.ui.action_set == Some(s));
+ // deleted since leaves the stored pair pointing at another set's action. Button Mode shows no
+ // selected action and no current set, so the items that act on them are off there.
+ let selected = pstate:: >(ui.ctx(), "actions-selected").filter(|&(s, _)| !buttons && app.ui.action_set == Some(s));
let has = selected.and_then(|(s, a)| app.ui.action_sets.get(s).and_then(|set| set.actions.get(a))).is_some();
if widgets::menu_item(ui, tl!("New Action…"), crate::dialogs::action_new::can_open(app), false)
&& let Err(e) = crate::dialogs::action_new::open(app)
@@ -2019,11 +2094,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
if widgets::menu_item(ui, tl!("New Set…"), true, false) {
crate::dialogs::action_set_options::open_new(app);
}
- let current = app.ui.action_set.filter(|&s| s < app.ui.action_sets.len());
+ let current = app.ui.action_set.filter(|&s| !buttons && s < app.ui.action_sets.len());
if widgets::menu_item(ui, tl!("Duplicate"), current.is_some(), false) {
duplicate_selection(app, ui.ctx());
}
- if widgets::menu_item(ui, tl!("Delete"), has || !selected_sets(app).is_empty(), false) {
+ if widgets::menu_item(ui, tl!("Delete"), has || (!buttons && !selected_sets(app).is_empty()), false) {
delete_selection(app, ui.ctx());
}
if widgets::menu_item(ui, tl!("Set Options…"), current.is_some(), false)
@@ -2156,8 +2231,78 @@ enum InclusionChange {
Step(usize, usize, usize, bool),
}
-/// Draw action sets, steps and playback controls, keeping the list above a pinned footer.
+/// Button Mode ([`button_mode`]): every action of every set, in set order and then action order,
+/// as a button with its name in its color ([`button_colors`]), with no set names, steps or
+/// checkmarks. A click on a button, or Enter or Space on the focused one, plays the action
+/// ([`PLAY_AT`]). The footer shows only while an action plays or records: Stop Playing/Recording,
+/// and Play Current Selection while a playback waits at a stop.
+fn show_buttons(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
+ let t = Tokens::get(ui.ctx());
+ let mut play_req = None;
+ let list_height = widgets::panel_list_height(ui, 260.0, 0.0).min(260.0);
+ egui::ScrollArea::vertical().id_salt("actions-buttons").max_height(list_height).min_scrolled_height(list_height.min(64.0)).show(ui, |ui| {
+ for (si, set) in app.ui.action_sets.iter().enumerate() {
+ for (ai, a) in set.actions.iter().enumerate() {
+ let (r, resp) = ui.allocate_exact_size(egui::vec2(ui.available_width(), 24.0), egui::Sense::click());
+ resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &a.name));
+ let (fill, text) = button_colors(a.color.as_deref(), &t);
+ let button = r.shrink2(egui::vec2(2.0, 1.0));
+ ui.painter().rect_filled(button, 3.0, fill);
+ // The pointer over a button, or the keyboard focus on it, rings it in its text color.
+ let (width, ring) = if resp.hovered() || resp.has_focus() { (2.0, text) } else { (1.0, t.button_border) };
+ ui.painter().rect_stroke(button, 3.0, egui::Stroke::new(width, ring), egui::StrokeKind::Inside);
+ ui.painter().with_clip_rect(button.shrink2(egui::vec2(6.0, 0.0))).text(
+ button.left_center() + egui::vec2(8.0, 0.0),
+ egui::Align2::LEFT_CENTER,
+ &a.name,
+ egui::FontId::proportional(12.5),
+ text,
+ );
+ if resp.on_hover_text(&a.name).clicked() {
+ play_req = Some((si, ai));
+ }
+ }
+ }
+ });
+ if app.ui.action_sets.iter().all(|s| s.actions.is_empty()) {
+ dim_label(ui, tl!("No actions."));
+ }
+ let waiting = waiting_at_stop(app);
+ if waiting {
+ dim_label(ui, tl!("Press Play Current Selection in the Actions panel to go on."));
+ }
+ let recording = app.ui.recording.is_some();
+ if recording || app.ui.action_playing.is_some() {
+ widgets::bottom_bar(ui, |ui| {
+ if widgets::icon_button(ui, "square", tl!("Stop Playing/Recording"), false, 24.0).clicked()
+ && let Err(e) = app.run(STOP_RECORDING, json!({}))
+ {
+ app.status(e);
+ }
+ if waiting
+ && widgets::icon_button(ui, "dc-actions", tl!("Play Current Selection"), false, 24.0).clicked()
+ && let Err(e) = app.run(CONTINUE_PLAYBACK, json!({}))
+ {
+ app.status(e);
+ }
+ if recording {
+ ui.label(egui::RichText::new(tl!("● Recording")).color(egui::Color32::from_rgb(0xe0, 0x30, 0x30)));
+ }
+ });
+ }
+ if let Some((si, ai)) = play_req
+ && let Err(e) = app.run(PLAY_AT, json!({"set": si, "action": ai}))
+ {
+ app.status(e);
+ }
+}
+
+/// Draw action sets, steps and playback controls, keeping the list above a pinned footer, or in
+/// Button Mode the actions as buttons ([`show_buttons`]).
pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
+ if app.ui.action_button_mode {
+ return show_buttons(app, ui);
+ }
let t = Tokens::get(ui.ctx());
// The selected action as (set, action). Read as the type it is stored as: egui keeps a
// value per type, so reading the bare pair never found it and no action stayed selected (#1629).
@@ -2672,6 +2817,186 @@ mod tests {
assert_eq!(names(&app), before[1..], "nothing selected after a delete: nothing more goes");
}
+ /// The fills of the rectangles drawn in one headless frame of the panel.
+ fn drawn_fills(ctx: &egui::Context, app: &mut VectorcraftApp) -> Vec {
+ fn collect(s: &egui::Shape, out: &mut Vec) {
+ match s {
+ egui::Shape::Rect(r) => out.push(r.fill),
+ egui::Shape::Vec(v) => v.iter().for_each(|s| collect(s, out)),
+ _ => {}
+ }
+ }
+ let mut output = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui));
+ output.textures_delta.clear();
+ let mut fills = vec![];
+ output.shapes.iter().for_each(|c| collect(&c.shape, &mut fills));
+ fills
+ }
+
+ /// The panel menu's Button Mode switches the panel to buttons and back, with a check mark while
+ /// it is on; `ui.action.buttonMode` does the same, and the view is kept with the UI preferences.
+ /// Turning it on while an action records is refused, so Stop stays in reach.
+ #[test]
+ fn button_mode_switches_the_view_and_is_kept_with_the_preferences() {
+ use super::super::tests_appearance::{click, frame_events};
+ let mut app = app_with_sets();
+ assert!(!app.ui.action_button_mode, "the list is the default");
+ let ctx = egui::Context::default();
+ let item = |ctx: &egui::Context, app: &mut VectorcraftApp| {
+ let texts = frame_events(ctx, app, vec![], menu);
+ texts.into_iter().find(|(s, _)| s.trim_start_matches('✓').trim() == "Button Mode").expect("Button Mode in the menu")
+ };
+ let (label, at) = item(&ctx, &mut app);
+ assert_eq!(label, " Button Mode", "no check mark while off");
+ click(&ctx, &mut app, at.center(), menu);
+ assert!(app.ui.action_button_mode, "the menu item turns it on");
+ assert_eq!(item(&ctx, &mut app).0, "✓ Button Mode", "a check mark while on");
+ let at = item(&ctx, &mut app).1;
+ click(&ctx, &mut app, at.center(), menu);
+ assert!(!app.ui.action_button_mode, "choosing it again returns to the list");
+
+ assert_eq!(app.run(BUTTON_MODE, json!({})).unwrap(), json!({"enabled": true}), "left out: the other view");
+ assert_eq!(app.run(BUTTON_MODE, json!({"enabled": true})).unwrap(), json!({"enabled": true}));
+ assert_eq!(app.run(BUTTON_MODE, json!({"enabled": "no"})).unwrap_err(), "`enabled` must be true or false");
+ assert!(app.ui.action_button_mode, "a bad value keeps the view");
+ let saved = serde_json::to_value(&app.ui).unwrap();
+ assert_eq!(saved["action_button_mode"], json!(true));
+ assert!(serde_json::from_value::(saved.clone()).unwrap().action_button_mode, "kept after a restart");
+ let mut older = saved;
+ older.as_object_mut().unwrap().remove("action_button_mode");
+ assert!(!serde_json::from_value::(older).unwrap().action_button_mode, "older preferences show the list");
+
+ assert_eq!(app.run(BUTTON_MODE, json!({"enabled": null})).unwrap(), json!({"enabled": false}));
+ app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
+ app.run(START_RECORDING, json!({})).unwrap();
+ assert_eq!(app.run(BUTTON_MODE, json!({})).unwrap_err(), "an action is already recording");
+ let at = item(&ctx, &mut app).1;
+ click(&ctx, &mut app, at.center(), menu);
+ assert!(!app.ui.action_button_mode, "the menu item is off while an action records");
+ assert_eq!(app.run(BUTTON_MODE, json!({"enabled": false})).unwrap(), json!({"enabled": false}), "turning it off is never refused");
+ app.run(STOP_RECORDING, json!({})).unwrap();
+ }
+
+ /// Button Mode shows every action of every set by its name, in set order and then action order,
+ /// without set names, step counts or steps, each in its Action Options color; an action without
+ /// a color is an ordinary button. The menu items for a selected action or the current set are
+ /// off, since the view shows neither.
+ #[test]
+ fn button_mode_shows_every_action_as_a_button_in_its_color() {
+ use super::super::tests_appearance::{click, frame_events, text_rect};
+ let mut app = app_with_sets();
+ app.run(ACTION_OPTIONS, json!({"set": 1, "action": 0, "color": "red"})).unwrap();
+ app.run(ACTION_OPTIONS, json!({"set": 1, "action": 1, "color": "yellow"})).unwrap();
+ let ctx = egui::Context::default();
+ let texts = frame_events(&ctx, &mut app, vec![], show);
+ click(&ctx, &mut app, text_rect(&texts, "Nudge").center(), show);
+ assert_eq!(pstate::>(&ctx, "actions-selected"), Some((1, 0)), "selected in the list");
+ app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
+ let texts = frame_events(&ctx, &mut app, vec![], show);
+ let names: Vec<&str> = texts.iter().map(|(s, _)| s.as_str()).collect();
+ assert_eq!(
+ names,
+ [
+ "Duplicate and Offset 10 pt",
+ "Unite and Simplify",
+ "Center on Artboard",
+ "Outline Text",
+ "Rotate 90° CW",
+ "Reflect Horizontal",
+ "Nudge",
+ "Nudge Again"
+ ],
+ "only the actions' names, in set and action order"
+ );
+ let tops: Vec = texts.iter().map(|(_, r)| r.top()).collect();
+ assert!(tops.windows(2).all(|w| w[0] < w[1]), "one button under the other: {tops:?}");
+ let t = Tokens::get(&ctx);
+ let fills = drawn_fills(&ctx, &mut app);
+ for color in ["red", "yellow"] {
+ let fill = button_fill(color).unwrap();
+ assert_eq!(fills.iter().filter(|f| **f == fill).count(), 1, "{color}: {fills:?}");
+ }
+ assert_eq!(fills.iter().filter(|f| **f == t.button).count(), 6, "the six actions without a color: {fills:?}");
+ for (color, _) in COLORS {
+ let (fill, text) = button_colors(Some(color), &t);
+ let (hi, lo) = (luminance(fill).max(luminance(text)), luminance(fill).min(luminance(text)));
+ assert!((hi + 0.05) / (lo + 0.05) >= 4.5, "{color}: its name is legible");
+ }
+ assert_eq!(button_colors(None, &t), (t.button, t.text_strong));
+ let menu_texts = frame_events(&ctx, &mut app, vec![], menu);
+ click(&ctx, &mut app, text_rect(&menu_texts, " Delete").center(), menu);
+ click(&ctx, &mut app, text_rect(&menu_texts, " Action Options…").center(), menu);
+ assert!(app.ui.dialog.is_none(), "Delete and Action Options… are off in Button Mode");
+ assert_eq!(set_names(&app), ["Default Actions", "Mine", "Other"]);
+ assert_eq!(action_names(&app, 1), ["Nudge", "Nudge Again"], "the action selected in the list stays");
+ }
+
+ /// A click on a button plays that action at once, as one undo step at Accelerated, without its
+ /// excluded steps, which stay excluded; `ui.action.playAt` refuses an index that names no action.
+ #[test]
+ fn a_click_on_a_button_plays_its_action_without_its_excluded_steps() {
+ use super::super::tests_appearance::{click, frame_events, text_rect};
+ let (mut app, x) = app_with_three_moves();
+ app.run(INCLUDE_STEPS, json!({"set": 0, "action": 0, "step": 1, "included": false})).unwrap();
+ app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
+ let ctx = egui::Context::default();
+ let texts = frame_events(&ctx, &mut app, vec![], show);
+ let undo = undo_steps(&app);
+ click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show);
+ assert_eq!((x(&app), undo_steps(&app)), (5.0, undo + 1), "1 and 4 pt as one undo step; the 2 pt step is skipped");
+ assert_eq!(app.ui.action_sets[0].actions[0].excluded_steps, BTreeSet::from([1]), "the exclusion stays");
+ for (p, want) in [
+ (json!({"set": 0, "action": 1}), "no such action"),
+ (json!({"set": 1, "action": 0}), "no such action"),
+ (json!({"set": "0", "action": 0}), "no such action"),
+ (json!({"set": 0}), "no such action"),
+ ] {
+ assert_eq!(app.run(PLAY_AT, p.clone()).unwrap_err(), want, "{p}");
+ }
+ assert_eq!(x(&app), 5.0, "nothing more played");
+ app.run(PLAY_AT, json!({"set": 0, "action": 0})).unwrap();
+ assert_eq!(x(&app), 10.0);
+ }
+
+ /// Button Mode shows no bottom bar while nothing plays or records. While an action plays step by
+ /// step its Stop Playing/Recording button ends the playback, and while one waits at a stop its
+ /// Play Current Selection button plays on.
+ #[test]
+ fn button_mode_shows_stop_and_continue_only_while_an_action_plays() {
+ use super::super::tests_appearance::{click, frame_events, text_rect};
+ let bar = |ctx: &egui::Context, app: &mut VectorcraftApp| {
+ frame_events(ctx, app, vec![], show);
+ let mut bar = crate::toolbar::tests::widget_rects(ctx, egui::vec2(24.0, 24.0));
+ bar.sort_by(|a, b| a.left().total_cmp(&b.left()));
+ // A button can be listed more than once in a pass.
+ bar.dedup();
+ bar
+ };
+ let (mut app, x) = app_with_three_moves();
+ app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
+ app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap();
+ let ctx = egui::Context::default();
+ assert_eq!(bar(&ctx, &mut app), [], "no bottom bar while idle");
+ let texts = frame_events(&ctx, &mut app, vec![], show);
+ click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show);
+ assert_eq!(x(&app), 1.0, "the first step played");
+ let [stop] = bar(&ctx, &mut app)[..] else { panic!("only Stop while it plays") };
+ click(&ctx, &mut app, stop.center(), show);
+ assert!(app.ui.action_playing.is_none(), "Stop ends the playback");
+ assert_eq!(x(&app), 1.0);
+
+ let (mut app, x) = app_with_a_stop(true);
+ app.run(BUTTON_MODE, json!({"enabled": true})).unwrap();
+ let texts = frame_events(&ctx, &mut app, vec![], show);
+ click(&ctx, &mut app, text_rect(&texts, "Three Moves").center(), show);
+ assert!(waiting_at_stop(&app));
+ assert_eq!(x(&app), 1.0, "played up to the stop");
+ let [_, play] = bar(&ctx, &mut app)[..] else { panic!("Stop and Play Current Selection at a stop") };
+ click(&ctx, &mut app, play.center(), show);
+ assert!(app.ui.action_playing.is_none());
+ assert_eq!(x(&app), 7.0, "Play Current Selection plays on");
+ }
+
/// #3712: Create New Set asks for a name (Set 1 when free), the new set is the current one, so a
/// recording goes into it, and a click on another set's name makes that one current. The sets
/// are saved with the UI preferences.
diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs
index 44ae643e86..1e6ca2c8d5 100644
--- a/crates/ui-egui/src/panels/character.rs
+++ b/crates/ui-egui/src/panels/character.rs
@@ -2,6 +2,7 @@
//! scale, baseline shift, character rotation, the anti-aliasing method and the All Caps, Small Caps,
//! Superscript, Subscript, Underline and Strikethrough toggles and the Language menu.
+use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
use egui::{Key, Ui, vec2};
@@ -99,8 +100,8 @@ pub(crate) fn shared_text_flag(app: &VectorcraftApp, f: impl Fn(&vectorcraft_doc
}
/// `f` of the character styles the panels act on: the runs the Type tool's selection covers (the
-/// caret's style at a caret), else every run of the selected type, groups' included. `None` (a
-/// blank field) where they differ, or with no type.
+/// caret's style at a caret), else every run of every frame of the selected stories
+/// ([`selected_stories`]). `None` (a blank field) where they differ, or with no type.
pub(crate) fn shared(app: &VectorcraftApp, f: impl Fn(&CharStyle) -> T) -> Option {
shared_styles(app, |_, _, style, _| Some(f(style)))
}
@@ -162,12 +163,8 @@ fn shared_styles(
None => each_paragraph_run(t, Some((a, b)), |para, style, inline| acc.add(f(t, para, style, inline))),
}
} else {
- for n in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
- n.walk(&mut |c| {
- if let NodeKind::Text(t) = &c.kind {
- each_paragraph_run(t, None, |para, style, inline| acc.add(f(t, para, style, inline)));
- }
- });
+ for t in selected_story_texts(app) {
+ each_paragraph_run(t, None, |para, style, inline| acc.add(f(t, para, style, inline)));
}
}
acc.value().flatten()
@@ -243,9 +240,12 @@ pub(crate) fn format(app: &mut VectorcraftApp, p: Value) {
/// [`format`] that returns the command's result: `text.setRangeStyle` on the range the Type tool
/// has selected (the whole text when nothing is selected) while it edits text, else
-/// `text.setFormat` on the selected text objects.
+/// `text.setFormat` on the selected stories ([`selected_stories`]).
pub(crate) fn run_format(app: &mut VectorcraftApp, mut p: Value) -> Result {
- let Some((id, a, b)) = text_editing(app) else { return app.run("text.setFormat", p) };
+ let Some((id, a, b)) = text_editing(app) else {
+ target_selected_stories(app, &mut p);
+ return app.run("text.setFormat", p);
+ };
if let Some(first) = text_story(app).and_then(|story| story.first().copied()) {
return story_style(app, first, p);
}
@@ -283,9 +283,11 @@ pub(crate) fn shared_warichu(app: &VectorcraftApp) -> SharedWarichu {
}
/// Apply character attributes: to the selected range while the Type tool edits text (the whole
-/// object when nothing is selected), else to the selected text objects.
-fn char_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) {
+/// object when nothing is selected), else to every frame of the selected stories
+/// ([`selected_stories`]).
+fn char_cmd(app: &mut VectorcraftApp, cmd: &str, mut p: Value) {
if text_editing(app).is_none() {
+ target_selected_stories(app, &mut p);
app.run(cmd, p).ok();
return;
}
@@ -546,6 +548,55 @@ pub(crate) fn text_story(app: &VectorcraftApp) -> Option> {
(ids.len() >= 2).then_some(ids)
}
+/// The type objects the panels act on with the Selection tool: the selected type objects and those
+/// in selected groups, each with every frame of its threaded story (locked and hidden frames
+/// included), each once, the frames of a story in thread order.
+pub(crate) fn selected_stories(app: &VectorcraftApp) -> Vec {
+ let Some(st) = app.session.active() else { return Vec::new() };
+ let mut frames = Vec::new();
+ let mut taken = HashSet::new();
+ for selected in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
+ selected.walk(&mut |child| {
+ if matches!(child.kind, NodeKind::Text(_)) && !taken.contains(&child.id) {
+ for id in st.doc.story_of(child.id) {
+ if taken.insert(id) {
+ frames.push(id);
+ }
+ }
+ }
+ });
+ }
+ frames
+}
+
+/// The type objects of [`selected_stories`].
+pub(crate) fn selected_story_texts(app: &VectorcraftApp) -> Vec<&vectorcraft_doc::TextObject> {
+ let Some(st) = app.session.active() else { return Vec::new() };
+ selected_stories(app)
+ .into_iter()
+ .filter_map(|id| match st.doc.node(id).map(|n| &n.kind) {
+ Some(NodeKind::Text(t)) => Some(t.as_ref()),
+ _ => None,
+ })
+ .collect()
+}
+
+/// Give the command parameters `p` the `ids` of [`selected_stories`], so a command run with the
+/// Selection tool sets every frame of each selected story. `p` stays as it is when it names its
+/// targets, with no type selected, and while an object of a symbol instance's art is selected,
+/// which the commands refuse.
+pub(crate) fn target_selected_stories(app: &VectorcraftApp, p: &mut Value) {
+ if p.get("ids").is_some() || p.get("id").is_some() || app.session.active().is_none_or(|st| st.selection.symbol_part.is_some()) {
+ return;
+ }
+ let ids = selected_stories(app);
+ if let Some(params) = p.as_object_mut()
+ && !ids.is_empty()
+ {
+ params.insert("ids".into(), json!(ids.iter().map(|id| id.0).collect::>()));
+ }
+}
+
/// `text.setRangeStyle` with `p` on all the text of every frame of the story that `first` is a
/// frame of, as one undo step.
fn story_style(app: &mut VectorcraftApp, first: NodeId, mut p: Value) -> Result {
@@ -910,7 +961,8 @@ fn anti_alias_method(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) {
}
/// The anti-aliasing method of the type objects the panel acts on: the one the Type tool edits,
-/// else the selected type (groups' included). `None` where they differ, or with no type.
+/// else every frame of the selected stories ([`selected_stories`]). `None` where they differ, or
+/// with no type.
fn shared_anti_alias(app: &VectorcraftApp) -> Option {
let st = app.session.active()?;
let mut acc = Shared::None;
@@ -921,12 +973,8 @@ fn shared_anti_alias(app: &VectorcraftApp) -> Option {
}
}
None => {
- for n in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
- n.walk(&mut |c| {
- if let NodeKind::Text(t) = &c.kind {
- acc.add(t.anti_alias);
- }
- });
+ for t in selected_story_texts(app) {
+ acc.add(t.anti_alias);
}
}
}
@@ -934,12 +982,14 @@ fn shared_anti_alias(app: &VectorcraftApp) -> Option {
}
/// Set the anti-aliasing method `m` of the frame the Type tool edits, whatever part of its text
-/// is selected, else the selected type objects (`text.setFormat {antiAlias}`).
+/// is selected, else every frame of the selected stories (`text.setFormat {antiAlias}`).
fn set_anti_alias(app: &mut VectorcraftApp, m: TypeAntiAlias) {
let mut p = json!({"antiAlias": m.id()});
if let Some((id, _, _)) = text_editing(app) {
end_typing(app);
p["ids"] = json!([id.0]);
+ } else {
+ target_selected_stories(app, &mut p);
}
app.run("text.setFormat", p).ok();
}
@@ -1160,7 +1210,11 @@ fn axes_cmd(app: &mut VectorcraftApp, axes: Value, phase: widgets::Live) {
}
super::live_run(app, "Character", "text.setRangeStyle", q, phase);
}
- None => super::live_run(app, "Character", "text.setFormat", json!({ "fontAxes": axes }), phase),
+ None => {
+ let mut p = json!({ "fontAxes": axes });
+ target_selected_stories(app, &mut p);
+ super::live_run(app, "Character", "text.setFormat", p, phase);
+ }
}
}
diff --git a/crates/ui-egui/src/panels/character/tests.rs b/crates/ui-egui/src/panels/character/tests.rs
index 7a2a96efb2..56472c59b0 100644
--- a/crates/ui-egui/src/panels/character/tests.rs
+++ b/crates/ui-egui/src/panels/character/tests.rs
@@ -1741,3 +1741,141 @@ fn story_changes_take_in_locked_and_hidden_frames() {
assert_eq!(story_sizes(&app, id).iter().filter(|s| (**s - 9.0).abs() > 1e-9).count(), 0, "frame {id} takes the size");
}
}
+
+/// [`three_frame_story`] with its second frame locked and its third hidden, a type object of its own
+/// below it, and the first frame selected with the Selection tool → the app, the frames and the
+/// other type object.
+fn story_selected_in_its_first_frame() -> (VectorcraftApp, [u64; 3], u64) {
+ let (mut app, a, b, c) = three_frame_story(60);
+ let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap();
+ app.run("select.set", json!({"ids": [b]})).unwrap();
+ app.run("object.lock", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [c]})).unwrap();
+ app.run("object.hide", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [a]})).unwrap();
+ assert_eq!(text_editing(&app), None);
+ (app, [a, b, c], other)
+}
+
+fn undo_steps(app: &VectorcraftApp) -> usize {
+ app.session.active().unwrap().history.undo.len()
+}
+
+/// With the Selection tool, a selected frame of a threaded story stands for the whole story: the
+/// panel's changes reach its locked and hidden frames, each in one undo step, and leave other type
+/// alone.
+#[test]
+fn selection_tool_changes_reach_every_frame_of_the_story() {
+ let (mut app, frames, other) = story_selected_in_its_first_frame();
+ assert_eq!(selected_stories(&app), frames.map(NodeId).to_vec());
+
+ let steps = undo_steps(&app);
+ style(&mut app, json!({"size": 9}));
+ assert_eq!(undo_steps(&app), steps + 1, "one undo step");
+ for id in frames {
+ assert!(story_sizes(&app, id).iter().all(|s| (*s - 9.0).abs() < 1e-9), "frame {id} takes the size");
+ }
+ assert!(story_sizes(&app, other).iter().all(|s| (*s - 12.0).abs() < 1e-9), "other type keeps its size");
+ app.run("edit.undo", json!({})).unwrap();
+ for id in frames {
+ assert!(story_sizes(&app, id).iter().all(|s| (*s - 12.0).abs() < 1e-9), "undo gives frame {id} its size back");
+ }
+
+ format(&mut app, json!({"underline": true}));
+ for id in frames {
+ assert!(story_frame(&app, id).runs.iter().all(|r| r.style.underline), "frame {id} is underlined");
+ }
+ assert!(story_frame(&app, other).runs.iter().all(|r| !r.style.underline));
+
+ // Tate-chu-yoko Settings and Warichu Settings set their values through `run_format`.
+ run_format(&mut app, json!({"tcyUpDown": 2})).unwrap();
+ for id in frames {
+ assert!(story_frame(&app, id).runs.iter().all(|r| (r.style.tcy_up_down - 2.0).abs() < 1e-9), "frame {id} takes Up/Down");
+ }
+ assert!(story_frame(&app, other).runs.iter().all(|r| r.style.tcy_up_down.abs() < 1e-9));
+}
+
+/// With the Selection tool the anti-aliasing method goes to every frame of the selected story.
+#[test]
+fn selection_tool_anti_aliasing_reaches_every_frame_of_the_story() {
+ let (mut app, frames, other) = story_selected_in_its_first_frame();
+ set_anti_alias(&mut app, TypeAntiAlias::Strong);
+ for id in frames {
+ assert_eq!(story_frame(&app, id).anti_alias, TypeAntiAlias::Strong, "frame {id}");
+ }
+ assert_eq!(story_frame(&app, other).anti_alias, TypeAntiAlias::Sharp);
+ assert_eq!(shared_anti_alias(&app), Some(TypeAntiAlias::Strong));
+}
+
+/// With the Selection tool a Variable Font slider sets the axis on every frame of the selected
+/// story, in one undo step.
+#[test]
+fn selection_tool_font_axes_reach_every_frame_of_the_story() {
+ use vectorcraft_text::test_fonts::{TWO_AXIS_FAMILY, two_axis_font};
+ vectorcraft_text::FontDb::global().add_font(two_axis_font().unwrap());
+ let (mut app, frames, _) = story_selected_in_its_first_frame();
+ app.run("text.setStyle", json!({"ids": frames, "font": TWO_AXIS_FAMILY})).unwrap();
+ app.run("select.set", json!({"ids": [frames[0]]})).unwrap();
+ let steps = undo_steps(&app);
+ axes_cmd(&mut app, json!({"wght": 700}), widgets::Live::Released);
+ assert_eq!(undo_steps(&app), steps + 1, "one undo step");
+ for id in frames {
+ let t = story_frame(&app, id);
+ assert!(t.runs.iter().all(|r| r.style.font_axes.get("wght") == Some(&700.0)), "frame {id}: {:?}", t.runs[0].style.font_axes);
+ }
+}
+
+/// With the Selection tool a font family picked in the font menu, and its preview, go to every
+/// frame of the selected story, in one undo step.
+#[test]
+fn selection_tool_font_family_reaches_every_frame_of_the_story() {
+ let (mut app, frames, other) = story_selected_in_its_first_frame();
+ let family = |app: &VectorcraftApp, id: u64| story_frame(app, id).runs.iter().map(|r| r.style.font_family.clone()).collect::>();
+ let before = family(&app, other);
+ let ctx = egui::Context::default();
+ crate::font_menu::apply(&mut app, &ctx, crate::font_menu::FontPick::Preview("Source Serif 4".into(), None));
+ for id in frames {
+ assert!(family(&app, id).iter().all(|f| f == "Source Serif 4"), "the preview shows on frame {id}");
+ }
+ let steps = undo_steps(&app);
+ crate::font_menu::apply(&mut app, &ctx, crate::font_menu::FontPick::Chosen("Source Serif 4".into(), None));
+ assert_eq!(undo_steps(&app), steps + 1, "one undo step");
+ for id in frames {
+ assert!(family(&app, id).iter().all(|f| f == "Source Serif 4"), "frame {id} takes the family");
+ }
+ assert_eq!(family(&app, other), before, "other type keeps its family");
+}
+
+/// The fields show a value only when every frame of the selected story has it: a hidden frame that
+/// differs leaves them blank.
+#[test]
+fn selection_tool_fields_read_every_frame_of_the_story() {
+ let (mut app, [a, b, c], _) = story_selected_in_its_first_frame();
+ assert_eq!(shared(&app, |s| s.size), Some(12.0));
+ assert_eq!(shared_anti_alias(&app), Some(TypeAntiAlias::Sharp));
+ app.run("text.setStyle", json!({"ids": [c], "size": 20})).unwrap();
+ app.run("text.setFormat", json!({"ids": [b], "antiAlias": TypeAntiAlias::Crisp.id()})).unwrap();
+ app.run("select.set", json!({"ids": [a]})).unwrap();
+ assert_eq!(shared(&app, |s| s.size), None, "the hidden frame's size differs");
+ assert_eq!(shared_anti_alias(&app), None, "the locked frame's anti-aliasing differs");
+}
+
+/// Two frames of one story, or a frame in a selected group, count once; an unthreaded type
+/// object is a story of its own.
+#[test]
+fn selected_stories_list_each_frame_once() {
+ let (mut app, [a, b, c], other) = story_selected_in_its_first_frame();
+ app.run("object.unlockAll", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [b, a]})).unwrap();
+ assert_eq!(selected_stories(&app), [a, b, c].map(NodeId).to_vec(), "thread order, each once");
+ app.run("select.set", json!({"ids": [other, a]})).unwrap();
+ let group = app.run("object.group", json!({})).unwrap()["id"].as_u64().unwrap();
+ app.run("select.set", json!({"ids": [group]})).unwrap();
+ let mut listed = selected_stories(&app);
+ listed.sort_by_key(|id| id.0);
+ let mut expected = [a, b, c, other].map(NodeId).to_vec();
+ expected.sort_by_key(|id| id.0);
+ assert_eq!(listed, expected);
+ app.run("select.set", json!({"ids": []})).unwrap();
+ assert!(selected_stories(&app).is_empty());
+}
diff --git a/crates/ui-egui/src/panels/learn.rs b/crates/ui-egui/src/panels/learn.rs
index ee7967c8a3..5d24167c99 100644
--- a/crates/ui-egui/src/panels/learn.rs
+++ b/crates/ui-egui/src/panels/learn.rs
@@ -8,7 +8,8 @@
//! next tutorial in the same skill. `learn.list` lists the tutorials, `learn.skills` shows the list
//! of skills, `learn.show` opens one tutorial, `learn.locateTool` shows where a tool is,
//! `learn.openSample`, `learn.finish` and `learn.next` run the three practice controls, and
-//! `window.learn` shows or hides the panel.
+//! `window.learn` shows or hides the panel. The Home screen's Learn section shows the same card
+//! ([`guide_card`]) and the tutorials, which open the panel.
pub mod catalog;
pub mod locate;
@@ -68,8 +69,16 @@ fn for_you(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
crate::widgets::subheader(ui, tl!("Get started"));
ui.add_space(4.0);
+ guide_card(app, ui, ui.scope_id().with("quick-skill-guide"), t.panel_darker);
+}
+
+/// The Quick skill guide card, filled with `fill`, as one button with the id `id`: a click, or Enter
+/// or Space while it has the keyboard focus, shows the panel's list of skills (`learn.skills`). The
+/// For you tab and the Home screen's Learn section show it.
+pub(crate) fn guide_card(app: &mut VectorcraftApp, ui: &mut Ui, id: egui::Id, fill: egui::Color32) {
+ let t = Tokens::get(ui.ctx());
let card = egui::Frame::NONE
- .fill(t.panel_darker)
+ .fill(fill)
.stroke(Stroke::new(1.0, t.border))
.corner_radius(egui::CornerRadius::same(t.radius))
.inner_margin(egui::Margin::same(10))
@@ -86,8 +95,12 @@ fn for_you(app: &mut VectorcraftApp, ui: &mut Ui) {
.color(t.text_dim),
));
});
- let resp = ui.interact(card.response.rect, ui.scope_id().with("quick-skill-guide"), Sense::click());
+ let resp = ui.interact(card.response.rect, id, Sense::click());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Quick skill guide")));
+ if resp.has_focus() {
+ ui.painter().rect_stroke(resp.rect, egui::CornerRadius::same(t.radius), Stroke::new(1.0, t.selection), egui::StrokeKind::Inside);
+ }
+ let resp = resp.on_hover_cursor(egui::CursorIcon::PointingHand);
if resp.clicked()
&& let Err(e) = app.run("learn.skills", json!({}))
{
diff --git a/crates/ui-egui/src/panels/paragraph.rs b/crates/ui-egui/src/panels/paragraph.rs
index ab53932f80..e02d81e14c 100644
--- a/crates/ui-egui/src/panels/paragraph.rs
+++ b/crates/ui-egui/src/panels/paragraph.rs
@@ -28,8 +28,9 @@ pub const ALIGNMENTS: [(Justify, &str, &str, &str); 7] = [
(Justify::JustifyAll, "dc-para-justify-all", "Justify all lines", "justifyAll"),
];
-/// Paragraph attributes apply to the selected text objects' paragraphs or, while the Type tool
-/// edits text, to the paragraphs its selection (or caret) touches (ending its typing session).
+/// Paragraph attributes apply to the paragraphs of every frame of the selected stories or, while
+/// the Type tool edits text, to the paragraphs its selection (or caret) touches (ending its typing
+/// session).
fn format(app: &mut VectorcraftApp, p: Value) {
para_cmd(app, "text.setFormat", p);
}
@@ -75,8 +76,16 @@ fn para_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) {
run_on_paragraphs(app, cmd, p).ok();
}
-/// Run `cmd` on the paragraphs the Paragraph panel acts on ([`format`]) and return its result.
-pub(crate) fn run_on_paragraphs(app: &mut VectorcraftApp, cmd: &str, mut p: Value) -> Result {
+/// Run `cmd` on the paragraphs the Paragraph panel acts on ([`format`]) and return its result. With
+/// the Selection tool these are the paragraphs of every frame of the selected stories
+/// ([`super::character::selected_stories`]).
+pub(crate) fn run_on_paragraphs(app: &mut VectorcraftApp, cmd: &str, p: Value) -> Result {
+ run_on(app, cmd, p, true)
+}
+
+/// [`run_on_paragraphs`], with the Selection tool on the selected type objects alone unless
+/// `stories`.
+fn run_on(app: &mut VectorcraftApp, cmd: &str, mut p: Value, stories: bool) -> Result {
if let Some((id, a, b)) = super::character::text_editing(app) {
super::character::end_typing(app);
match super::character::text_story(app) {
@@ -88,6 +97,8 @@ pub(crate) fn run_on_paragraphs(app: &mut VectorcraftApp, cmd: &str, mut p: Valu
p["end"] = json!(b);
}
}
+ } else if stories {
+ super::character::target_selected_stories(app, &mut p);
}
app.run(cmd, p)
}
@@ -208,14 +219,10 @@ fn visit_paragraphs(app: &VectorcraftApp, mut f: impl FnMut(&ParaStyle)) {
}
return;
}
- for node in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
- node.walk(&mut |c| {
- if let NodeKind::Text(t) = &c.kind {
- for i in 0..t.paragraph_count() {
- f(t.para_at(i));
- }
- }
- });
+ for t in super::character::selected_story_texts(app) {
+ for i in 0..t.paragraph_count() {
+ f(t.para_at(i));
+ }
}
}
@@ -328,18 +335,8 @@ fn selected_paragraph_direction(app: &VectorcraftApp, fallback: &ParaStyle) -> O
{
return if collect_text_direction(text, Some((a, b)), &mut shared) { shared.or(default) } else { None };
}
- fn collect(node: &vectorcraft_doc::Node, shared: &mut Option) -> bool {
- if let NodeKind::Text(text) = &node.kind {
- return collect_text_direction(text, None, shared);
- }
- node.children().is_none_or(|children| children.iter().all(|child| collect(child, shared)))
- }
- for node in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
- if !collect(node, &mut shared) {
- return None;
- }
- }
- shared.or(default)
+ let texts = super::character::selected_story_texts(app);
+ if texts.into_iter().all(|t| collect_text_direction(t, None, &mut shared)) { shared.or(default) } else { None }
}
/// A hyphenation option shared by every paragraph in scope, or `None` when they differ.
@@ -365,18 +362,8 @@ fn selected_alignment(app: &VectorcraftApp, fallback: &ParaStyle) -> Option) -> bool {
- if let NodeKind::Text(text) = &node.kind {
- return collect_text_alignment(text, None, shared);
- }
- node.children().is_none_or(|children| children.iter().all(|child| collect(child, shared)))
- }
- for node in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
- if !collect(node, &mut shared) {
- return None;
- }
- }
- shared.or(default)
+ let texts = super::character::selected_story_texts(app);
+ if texts.into_iter().all(|t| collect_text_alignment(t, None, &mut shared)) { shared.or(default) } else { None }
}
/// The seven alignment buttons shared by Paragraph and Properties; no button is selected
@@ -492,7 +479,7 @@ pub(crate) fn number_sample(style: vectorcraft_doc::text::NumberStyle, n: u32) -
/// the Type tool's anchor and caret past the characters inserted at or before them).
pub(crate) fn convert_list_to_text(app: &mut VectorcraftApp) -> Result {
text_style(app).ok_or("select some type first")?;
- run_on_paragraphs(app, "text.convertListToText", json!({}))
+ run_on(app, "text.convertListToText", json!({}), false)
}
/// The Bullets button and the arrow beside it (Paragraph panel). The button makes the paragraphs
diff --git a/crates/ui-egui/src/panels/paragraph/tests.rs b/crates/ui-egui/src/panels/paragraph/tests.rs
index 50c17001db..180f0b1fa1 100644
--- a/crates/ui-egui/src/panels/paragraph/tests.rs
+++ b/crates/ui-egui/src/panels/paragraph/tests.rs
@@ -1035,3 +1035,58 @@ fn reset_panel_leaves_an_undo_group_open_in_another_document() {
app.run("edit.undoGroup.end", json!({})).unwrap();
assert_eq!(steps(app.session.active().unwrap()), a_steps, "the later edit is part of the Batch step");
}
+
+/// With the Selection tool a selected frame of a threaded story stands for the whole story: the
+/// Paragraph panel's changes and Reset Panel reach its locked and hidden frames, each in one undo
+/// step, other type keeps its paragraphs, and the alignment buttons read every frame.
+#[test]
+fn selection_tool_paragraph_changes_reach_every_frame_of_the_story() {
+ let mut app = VectorcraftApp::new(Session::new(), Default::default());
+ app.run("file.new", json!({"width": 900, "height": 300})).unwrap();
+ let story = "The quick brown fox jumps over the lazy dog.\n".repeat(12);
+ let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}});
+ let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap();
+ let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap();
+ let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap();
+ let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap();
+ app.run("select.set", json!({"ids": [b]})).unwrap();
+ app.run("object.lock", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [c]})).unwrap();
+ app.run("object.hide", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [a]})).unwrap();
+ let paragraphs = |app: &VectorcraftApp, id: u64| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
+ NodeKind::Text(t) => (0..t.paragraph_count()).map(|i| t.para_at(i).clone()).collect::>(),
+ _ => panic!("not text"),
+ };
+ let undo_steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
+ for id in [a, b, c] {
+ assert!(!paragraphs(&app, id).is_empty(), "frame {id} holds text");
+ }
+
+ let steps = undo_steps(&app);
+ format(&mut app, json!({"spaceBefore": 4}));
+ assert_eq!(undo_steps(&app), steps + 1, "one undo step");
+ for id in [a, b, c] {
+ assert!(paragraphs(&app, id).iter().all(|p| (p.space_before - 4.0).abs() < 1e-9), "frame {id} takes the space");
+ }
+ assert!(paragraphs(&app, other).iter().all(|p| p.space_before.abs() < 1e-9), "other type keeps its paragraphs");
+ assert_eq!(shared_para_field(&app, |p| p.space_before), Some(4.0));
+
+ align(&mut app, "center").unwrap();
+ for id in [a, b, c] {
+ assert!(paragraphs(&app, id).iter().all(|p| p.justify == Justify::Center), "frame {id} is centered");
+ }
+ assert!(paragraphs(&app, other).iter().all(|p| p.justify != Justify::Center));
+ assert_eq!(selected_alignment(&app, &ParaStyle::default()), Some(Justify::Center));
+ // A hidden frame aligned otherwise leaves no alignment button on.
+ app.run("text.setStyle", json!({"ids": [c], "justify": "right"})).unwrap();
+ app.run("select.set", json!({"ids": [a]})).unwrap();
+ assert_eq!(selected_alignment(&app, &ParaStyle::default()), None);
+
+ let steps = undo_steps(&app);
+ reset_panel(&mut app);
+ assert_eq!(undo_steps(&app), steps + 1, "Reset Panel is one undo step");
+ for id in [a, b, c] {
+ assert!(paragraphs(&app, id).iter().all(|p| p.space_before.abs() < 1e-9 && p.justify == Justify::Auto), "frame {id} is reset");
+ }
+}
diff --git a/crates/ui-egui/src/panels/tabs.rs b/crates/ui-egui/src/panels/tabs.rs
index bc1f9352c3..00814646c5 100644
--- a/crates/ui-egui/src/panels/tabs.rs
+++ b/crates/ui-egui/src/panels/tabs.rs
@@ -419,7 +419,9 @@ fn stops_json(stops: &[TabStop]) -> Value {
)
}
-/// Run `text.tabs.set` with `stops` on the text the panel acts on. Returns false when it fails.
+/// Run `text.tabs.set` with `stops` on the text the panel acts on: with the Selection tool, every
+/// frame of the selected stories ([`super::character::selected_stories`]). Returns false when it
+/// fails.
fn apply(app: &mut VectorcraftApp, stops: &[TabStop]) -> bool {
if let Some((id, a, b)) = super::character::text_editing(app) {
super::character::end_typing(app);
@@ -430,7 +432,9 @@ fn apply(app: &mut VectorcraftApp, stops: &[TabStop]) -> bool {
};
app.run("text.tabs.set", p).is_ok()
} else {
- app.run("text.tabs.set", json!({"stops": stops_json(stops)})).is_ok()
+ let mut p = json!({"stops": stops_json(stops)});
+ super::character::target_selected_stories(app, &mut p);
+ app.run("text.tabs.set", p).is_ok()
}
}
@@ -727,6 +731,20 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
resp.on_hover_text(tl!("Click to add a tab stop, drag to move it, drag it off the ruler to delete it"));
}
+/// Whether a paragraph of the whole story Clear All Tabs acts on has tab stops: after Select All in
+/// threaded text, every frame of the edited story; with the Selection tool, every frame of the
+/// selected stories ([`super::character::selected_stories`]).
+fn story_has_tabs(app: &VectorcraftApp) -> bool {
+ let has_tabs = |t: &TextObject| (0..t.paragraph_count()).any(|i| !t.para_at(i).tabs.is_empty());
+ match (super::character::text_editing(app), super::character::text_story(app), app.session.active()) {
+ (Some(_), Some(story), Some(st)) => {
+ story.iter().any(|id| matches!(st.doc.node(*id).map(|n| &n.kind), Some(NodeKind::Text(t)) if has_tabs(t)))
+ }
+ (Some(_), _, _) => false,
+ (None, _, _) => super::character::selected_story_texts(app).into_iter().any(has_tabs),
+ }
+}
+
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let Some((_, para, _)) = current_para(app) else {
ui.add_enabled(false, egui::Button::new(tl!("Select text")).frame(false));
@@ -742,7 +760,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
if sel >= stops.len() {
sel = stops.len().saturating_sub(1);
}
- if menu_item(ui, tl!("Clear All Tabs"), !stops.is_empty(), false)
+ if menu_item(ui, tl!("Clear All Tabs"), !stops.is_empty() || story_has_tabs(app), false)
&& let Err(e) = super::paragraph::run_on_paragraphs(app, "text.tabs.clear", json!({}))
{
app.status(e);
diff --git a/crates/ui-egui/src/panels/tabs/tests.rs b/crates/ui-egui/src/panels/tabs/tests.rs
index 6472b1017b..e260db70ab 100644
--- a/crates/ui-egui/src/panels/tabs/tests.rs
+++ b/crates/ui-egui/src/panels/tabs/tests.rs
@@ -642,6 +642,43 @@ fn text_of(app: &VectorcraftApp, id: u64) -> vectorcraft_doc::TextObject {
}
}
+/// With the Selection tool, tab stops set in the panel go to every paragraph of every frame of a
+/// selected threaded story, its locked and hidden frames included.
+#[test]
+fn selection_tool_tab_stops_reach_every_frame_of_the_story() {
+ let (mut app, frames) = split_story_app();
+ app.run("select.set", json!({"ids": [frames[1]]})).unwrap();
+ app.run("object.lock", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [frames[2]]})).unwrap();
+ app.run("object.hide", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [frames[0]]})).unwrap();
+ let stop = TabStop { position: 36.0, align: Default::default(), leader: String::new(), align_on: '.' };
+ assert!(apply(&mut app, std::slice::from_ref(&stop)));
+ for id in frames {
+ let t = text_of(&app, id);
+ assert!((0..t.paragraph_count()).all(|i| t.para_at(i).tabs == [stop.clone()]), "frame {id} takes the stop");
+ }
+}
+
+/// With the Selection tool, Clear All Tabs is on when only another frame of the selected story has
+/// tab stops, and clears them there.
+#[test]
+fn clear_all_tabs_is_on_when_another_frame_of_the_story_has_stops() {
+ let (mut app, [a, b, c]) = split_story_app();
+ app.run("text.tabs.set", json!({"ids": [c], "stops": [{"position": 36}]})).unwrap();
+ app.run("select.set", json!({"ids": [c]})).unwrap();
+ app.run("object.hide", json!({})).unwrap();
+ app.run("select.set", json!({"ids": [a]})).unwrap();
+ assert!(current(&app).unwrap().1.is_empty(), "the selected frame has no stops");
+ assert!(story_has_tabs(&app));
+ super::super::paragraph::run_on_paragraphs(&mut app, "text.tabs.clear", json!({})).unwrap();
+ for id in [a, b, c] {
+ let t = text_of(&app, id);
+ assert!((0..t.paragraph_count()).all(|i| t.para_at(i).tabs.is_empty()), "frame {id} has no stops");
+ }
+ assert!(!story_has_tabs(&app));
+}
+
/// Each paragraph of the story of `frames` by its first word, with its left indent and first-line
/// position, counted once; each piece of a paragraph split between frames has the same values.
fn story_paragraphs(app: &VectorcraftApp, frames: &[u64]) -> Vec<(String, f64, f64)> {
diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs
index fbea47770b..9029459195 100644
--- a/crates/ui-egui/src/prefs_dialog.rs
+++ b/crates/ui-egui/src/prefs_dialog.rs
@@ -42,6 +42,9 @@ pub fn open(app: &mut VectorcraftApp, category: Option<&str>) {
// Read-only: whether VECTORCRAFT_TITLEBAR chose this window's frame (System Title Bar's dimmed
// row and VectorCraft Title Bar on Every Desktop's note).
fields.insert("__titlebarFromEnv".into(), json!(app.titlebar_from_env));
+ // Read-only: whether the desktop would draw this window's frame with System Title Bar on or off
+ // while VectorCraft Title Bar on Every Desktop is off (System Title Bar's note).
+ fields.insert("__desktopKeepsFrame".into(), json!(app.desktop_keeps_frame));
fields.insert("__search".into(), json!(""));
fields.insert("__focusSearch".into(), json!(true));
app.ui.dialog = Some(Dialog { kind: "preferences".into(), fields });
@@ -535,6 +538,14 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
if !APP_TITLE_BAR_SHOWN {
ui.label(egui::RichText::new(tl!("Applies the next time VectorCraft starts.")).color(t.text_dim).size(11.0));
}
+ // Where the desktop draws the frame, the desktop's title bar stays with this
+ // option on or off until VectorCraft Title Bar on Every Desktop is turned on.
+ if APP_TITLE_BAR_SHOWN && d.bool("__desktopKeepsFrame") && !d.bool("__titlebarFromEnv") && !d.bool("appTitleBar") {
+ let note = tl!(
+ "This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off."
+ );
+ ui.label(egui::RichText::new(note).color(t.text_dim).size(11.0));
+ }
}
}
// Linux and the BSDs only, where the desktop may draw the frame: dimmed while System
diff --git a/crates/ui-egui/src/prefs_dialog/tests.rs b/crates/ui-egui/src/prefs_dialog/tests.rs
index fe8dc72aa3..6c7382200f 100644
--- a/crates/ui-egui/src/prefs_dialog/tests.rs
+++ b/crates/ui-egui/src/prefs_dialog/tests.rs
@@ -581,6 +581,46 @@ fn system_title_bar_is_dimmed_while_the_environment_variable_chooses_the_frame()
}
}
+/// Where the desktop draws the frame with System Title Bar on or off (KDE, Sway, X11, a window
+/// opened through XWayland), a note under System Title Bar says so while VectorCraft Title Bar on
+/// Every Desktop is off and VECTORCRAFT_TITLEBAR is unset. Linux and the BSDs only.
+#[test]
+fn system_title_bar_says_when_the_desktop_keeps_the_frame() {
+ fn texts(s: &egui::Shape, out: &mut Vec) {
+ match s {
+ egui::Shape::Text(t) => out.push(t.galley.text().to_string()),
+ egui::Shape::Vec(v) => v.iter().for_each(|s| texts(s, out)),
+ _ => {}
+ }
+ }
+ let note = "This desktop draws the window's title bar with this option on or off while VectorCraft Title Bar on Every Desktop is off.";
+ let supported = cfg!(not(any(target_arch = "wasm32", target_os = "macos", target_os = "windows")));
+ for keeps in [false, true] {
+ for from_env in [false, true] {
+ for (app_title_bar, system) in [(false, false), (false, true), (true, false), (true, true)] {
+ let mut a = app();
+ a.desktop_keeps_frame = keeps;
+ a.titlebar_from_env = from_env;
+ open(&mut a, Some("User Interface"));
+ let fields = &mut a.ui.dialog.as_mut().unwrap().fields;
+ fields.insert("appTitleBar".into(), json!(app_title_bar));
+ fields.insert("systemTitleBar".into(), json!(system));
+ let ctx = egui::Context::default();
+ crate::theme::install_fonts(&ctx);
+ let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx()));
+ out.textures_delta.clear();
+ let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(&mut a, ui.ctx()));
+ out.textures_delta.clear();
+ let mut shown = vec![];
+ out.shapes.iter().for_each(|c| texts(&c.shape, &mut shown));
+ let expected = supported && keeps && !from_env && !app_title_bar;
+ let label = format!("keeps {keeps}, variable {from_env}, app title bar {app_title_bar}, system {system}: {shown:?}");
+ assert_eq!(shown.iter().any(|t| t == note), expected, "{label}");
+ }
+ }
+ }
+}
+
/// Performance › Linux Display Server (#2028): on Linux it shows the drawing-tablet note, and
/// X11 (XWayland) When a Pen Is Attached is applied by OK and saved under the key the desktop
/// app reads before the window opens.
diff --git a/crates/ui-egui/src/recovery.rs b/crates/ui-egui/src/recovery.rs
index 3b05ba0120..aa0a25e50f 100644
--- a/crates/ui-egui/src/recovery.rs
+++ b/crates/ui-egui/src/recovery.rs
@@ -29,6 +29,17 @@ pub struct Timer {
/// The recovery folders that failure messages named, latest last, at most [`NAMED`]: replies to
/// automation clients hide those outside their read roots.
pub named: Vec,
+ /// The restored documents whose linked files are still to be asked about, next first
+ /// ([`ask_next`]).
+ links: Vec,
+}
+
+/// The questions about the linked files of a restored document (by uid): the missing files, then
+/// the modified images (ids) to offer for update ([`crate::dialogs::missing_links::ask_next`]).
+struct LinkQuestions {
+ uid: u64,
+ missing: Vec,
+ modified: Vec,
}
/// How many recovery folders [`Timer::named`] keeps.
@@ -187,28 +198,91 @@ fn named(app: &mut VectorcraftApp, folder: &str) {
}
/// The startup dialog's OK (Restore) or Discard: every copy it lists.
+///
+/// After Restore the status bar shows how many documents were restored, followed by the first of
+/// the restored documents' notes: for each document in the dialog's order, the lines about its
+/// linked files, then what reading the copy left out, then its other warnings, each named with
+/// its document's title when more than one was restored. Hovering the message lists them all. The
+/// questions about the documents' linked files are asked one document at a time ([`ask_next`]).
+/// When a copy can't be restored, the status bar shows the error, and its hover lists the count
+/// and the notes of the documents that were restored.
pub fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result {
app.ui.dialog = None;
- let id = if d.bool("discard") { "file.recovery.discard" } else { "file.recovery.restore" };
+ let discard = d.bool("discard");
+ let id = if discard { "file.recovery.discard" } else { "file.recovery.restore" };
let files: Vec =
d.fields.get("copies").and_then(Value::as_array).into_iter().flatten().filter_map(|c| c["file"].as_str()).map(str::to_string).collect();
- let (mut done, mut errors) = (vec![], vec![]);
+ let (mut done, mut errors, mut opened) = (vec![], vec![], vec![]);
for file in files {
match app.run(id, json!({ "file": file })) {
Ok(r) => {
- if let Some(restored) = r["restored"].as_array().and_then(|v| v.first()) {
- crate::dialogs::missing_links::after_open(app, restored, Vec::new());
+ for restored in r["restored"].as_array().into_iter().flatten() {
+ opened.push(restored_notes(app, restored));
}
+ let failed = r["failed"].as_array().into_iter().flatten();
+ errors.extend(failed.filter_map(|f| f["error"].as_str()).map(str::to_string));
done.push(r);
}
Err(e) => errors.push(e),
}
}
- if !errors.is_empty() {
- return Err(errors.join("; "));
+ if discard {
+ if !errors.is_empty() {
+ return Err(errors.join("; "));
+ }
+ app.status(recovered_status(true, done.len()));
+ return Ok(json!(done));
+ }
+ let summary = recovered_status(false, opened.len());
+ let titled = opened.len() > 1;
+ let restored = !opened.is_empty();
+ let notes: Vec = opened
+ .into_iter()
+ .flat_map(|(title, notes)| {
+ notes.into_iter().map(move |n| if titled { crate::i18n::fill_once("{path}: {e}", &[("path", &title), ("e", &n)]) } else { n })
+ })
+ .collect();
+ let result = if errors.is_empty() {
+ let status = match notes.first() {
+ Some(first) => crate::io::opened_after_notice(&summary, notes.len(), first),
+ None => summary,
+ };
+ crate::io::show_with_notes(app, status, notes);
+ Ok(json!(done))
+ } else {
+ let e = errors.join("; ");
+ if restored {
+ crate::io::show_with_notes(app, e.clone(), std::iter::once(summary).chain(notes).collect());
+ }
+ Err(e)
+ };
+ crate::dialogs::settle(app);
+ result
+}
+
+/// A document Restore just opened (`file.recovery.restore`'s entry `r`, the current document) →
+/// its title and notes ([`confirm`]). Its Missing Fonts dialog and the questions about its linked
+/// files wait for their turn.
+fn restored_notes(app: &mut VectorcraftApp, r: &Value) -> (String, Vec) {
+ crate::dialogs::missing_fonts::after_open(app, r);
+ let (mut notes, missing, modified) = crate::dialogs::missing_links::questions(app, r);
+ let uid = r["index"].as_u64().and_then(|i| usize::try_from(i).ok()).and_then(|i| app.session.documents().get(i)).map(|st| st.uid);
+ if let Some(uid) = uid.filter(|_| !missing.is_empty() || !modified.is_empty()) {
+ app.recovery.links.push(LinkQuestions { uid, missing, modified });
+ }
+ notes.extend(crate::io::open_notes(app, r));
+ (r["title"].as_str().unwrap_or_default().to_string(), notes)
+}
+
+/// With no dialog open: the questions about the linked files of the next restored document that
+/// is still open, with that document made the current one (the answers change it).
+pub(crate) fn ask_next(app: &mut VectorcraftApp) {
+ while app.ui.dialog.is_none() && !app.recovery.links.is_empty() {
+ let q = app.recovery.links.remove(0);
+ let Some(i) = app.session.documents().iter().position(|st| st.uid == q.uid) else { continue };
+ crate::chrome::show_document(app, i);
+ crate::dialogs::missing_links::ask_next(app, q.missing, q.modified, Vec::new());
}
- app.status(recovered_status(id == "file.recovery.discard", done.len()));
- Ok(json!(done))
}
/// The status bar's line for `n` recovered documents discarded or restored.
diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs
index a06dc2f1df..a29ca50e41 100644
--- a/crates/ui-egui/src/state.rs
+++ b/crates/ui-egui/src/state.rs
@@ -618,6 +618,11 @@ pub struct UiState {
/// Shift-click on a set's name added. Delete Selection deletes them. Not saved.
#[serde(skip)]
pub action_sets_selected: Vec,
+ /// The Actions panel shows its actions as buttons (panel menu › Button Mode,
+ /// [`crate::panels::actions::button_mode`]) instead of its list; kept with the UI preferences,
+ /// and off in preferences saved without it.
+ #[serde(default)]
+ pub action_button_mode: bool,
/// Recording in progress: (set index, action name, journal length when recording started).
#[serde(skip)]
pub recording: Option<(usize, String, usize)>,
@@ -1171,6 +1176,7 @@ impl Default for UiState {
action_sets: crate::panels::actions::default_sets(),
action_set: None,
action_sets_selected: vec![],
+ action_button_mode: false,
recording: None,
recording_into: None,
recording_at: None,
diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs
index 2c600d1aa3..ff249bbed4 100644
--- a/crates/ui-egui/src/tests/font_menu.rs
+++ b/crates/ui-egui/src/tests/font_menu.rs
@@ -1040,6 +1040,22 @@ fn open_menu_with(id: &str, left: f32, width: f32, screen: egui::Vec2, look: Men
assert!(frames < 1000, "the menu's samples are still rendering after {frames} frames");
shapes = frame(vec![]);
}
+ // egui places the menu by the size it had the frame before. When a name measured late (a face
+ // added for its script, a font found in the background) widens it on that frame, the menu sits
+ // where the narrower one fit until the next: draw until it stays put.
+ let filter_at = |shapes: &[egui::epaint::ClippedShape]| {
+ shapes.iter().find_map(|s| match &s.shape {
+ egui::Shape::Text(t) if t.galley.text() == crate::i18n::t("Any Kind") => Some(t.pos),
+ _ => None,
+ })
+ };
+ for _ in 0..100 {
+ let before = filter_at(&shapes);
+ shapes = frame(vec![]);
+ if filter_at(&shapes) == before && !crate::font_menu::wait_for_samples(&ctx) {
+ break;
+ }
+ }
let mut texts = vec![];
let mut samples = vec![];
let mut tints = vec![];
diff --git a/crates/ui-egui/src/tests/home.rs b/crates/ui-egui/src/tests/home.rs
index d1c20991af..6cfdf78f38 100644
--- a/crates/ui-egui/src/tests/home.rs
+++ b/crates/ui-egui/src/tests/home.rs
@@ -299,6 +299,91 @@ fn home_scrolls_in_a_short_window() {
assert_eq!(app.session.documents().len(), 1, "the card opened its preset");
}
+/// The visible text `text` on a tall Home screen frame drawn at `time` → its center.
+fn learn_text_at(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, text: &str) -> egui::Pos2 {
+ let texts = visible_home_texts(app, ctx, (1000.0, 2400.0), time, vec![]);
+ texts.iter().find(|(t, _)| t == text).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("{text} is shown: {texts:?}"))
+}
+
+/// A click at `at` on a tall Home screen frame, pressed at `time` and released a frame later.
+fn click_home_at(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, at: egui::Pos2) {
+ let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
+ visible_home_texts(app, ctx, (1000.0, 2400.0), time, vec![egui::Event::PointerMoved(at), press(true)]);
+ visible_home_texts(app, ctx, (1000.0, 2400.0), time + 0.05, vec![press(false)]);
+}
+
+/// The Home screen has a Learn section: the Quick skill guide card and every tutorial of the Learn
+/// panel under its skill. A tutorial shows the Learn panel at its steps and the card shows the list
+/// of skills; a second choice keeps the panel shown, and no document is opened.
+#[test]
+fn home_has_a_learn_section_that_opens_the_learn_panel() {
+ use crate::panels::learn::{self, Tab, View, catalog};
+ let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
+ let ctx = egui::Context::default();
+ crate::theme::install_fonts(&ctx);
+ let texts = visible_home_texts(&mut app, &ctx, (1000.0, 2400.0), 0.0, vec![]);
+ let has = |s: &str| texts.iter().any(|(t, _)| t == s);
+ assert!(has("Learn") && has("Quick skill guide"), "{texts:?}");
+ for skill in catalog::SKILLS {
+ assert!(has(skill.title), "{} is shown", skill.title);
+ for tutorial in skill.tutorials {
+ assert!(has(tutorial.title), "{} is shown", tutorial.title);
+ }
+ }
+ assert!(!learn::visible(&app.ui), "the panel is not shown before a choice");
+ let at = learn_text_at(&mut app, &ctx, 0.5, "Draw paths with the Pen");
+ click_home_at(&mut app, &ctx, 1.0, at);
+ assert!(learn::visible(&app.ui), "a tutorial shows the Learn panel");
+ assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some("draw"), tutorial: Some("pen-paths") });
+ assert!(app.session.documents().is_empty(), "no document is opened");
+ assert!(crate::menus::home_showing(&app), "Home stays");
+ let at = learn_text_at(&mut app, &ctx, 1.5, "Quick skill guide");
+ click_home_at(&mut app, &ctx, 2.0, at);
+ assert!(learn::visible(&app.ui), "a second choice keeps the panel shown");
+ assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: None, tutorial: None }, "the card shows the list of skills");
+ assert!(app.session.documents().is_empty());
+}
+
+/// Home shown over open documents has the Learn section too; a tutorial chosen there leaves the
+/// documents and the active one as they are.
+#[test]
+fn home_learn_section_over_open_documents_leaves_them_as_they_are() {
+ use crate::panels::learn::{self, Tab, View};
+ let mut app = app_with_doc();
+ app.run("file.new", json!({})).unwrap();
+ app.run("document.activate", json!({"index": 0})).unwrap();
+ app.run("app.home", json!({})).unwrap();
+ let ctx = egui::Context::default();
+ crate::theme::install_fonts(&ctx);
+ let at = learn_text_at(&mut app, &ctx, 0.0, "Add a gradient");
+ click_home_at(&mut app, &ctx, 0.5, at);
+ assert!(learn::visible(&app.ui));
+ assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: Some("color"), tutorial: Some("gradient") });
+ assert_eq!((app.session.documents().len(), app.session.active_index()), (2, Some(0)), "the documents stay as they are");
+ assert!(crate::menus::home_showing(&app), "Home stays over the documents");
+}
+
+/// The Home screen's Quick skill guide card works from the keyboard: with the keyboard focus, Enter
+/// shows the Learn panel's list of skills.
+#[test]
+fn home_quick_skill_guide_card_works_from_the_keyboard() {
+ use crate::panels::learn::{self, Tab, View};
+ let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
+ let ctx = egui::Context::default();
+ crate::theme::install_fonts(&ctx);
+ visible_home_texts(&mut app, &ctx, (1000.0, 2400.0), 0.0, vec![]);
+ let card = egui::Id::unique("home-quick-skill-guide");
+ ctx.memory_mut(|m| m.request_focus(card));
+ visible_home_texts(&mut app, &ctx, (1000.0, 2400.0), 0.1, vec![]);
+ assert!(ctx.memory(|m| m.has_focus(card)), "the card takes the keyboard focus");
+ let key = |pressed| egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed, repeat: false, modifiers: Default::default() };
+ visible_home_texts(&mut app, &ctx, (1000.0, 2400.0), 0.2, vec![key(true)]);
+ visible_home_texts(&mut app, &ctx, (1000.0, 2400.0), 0.3, vec![key(false)]);
+ assert!(learn::visible(&app.ui));
+ assert_eq!(app.ui.learn, View { tab: Tab::Skills, skill: None, tutorial: None });
+ assert!(app.session.documents().is_empty());
+}
+
/// A file that is already open is not opened again, however it is asked for: Home › Recent Files,
/// File › Open Recent, File › Open, the Finder and a drop all focus its tab, unsaved edits and all
/// (#3455).
@@ -452,7 +537,8 @@ fn text_centre_x(app: &mut VectorcraftApp, text: &str) -> Option<(f32, f32)> {
app.canvas_rect = None;
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
- let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1000.0, 900.0));
+ // Tall enough to draw the whole screen, Learn section included.
+ let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1000.0, 1600.0));
let raw = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| canvas::show(app, ui));
out.textures_delta.clear();
diff --git a/crates/ui-egui/src/tests/recovery.rs b/crates/ui-egui/src/tests/recovery.rs
index 00ccb59b79..76f56a1fb9 100644
--- a/crates/ui-egui/src/tests/recovery.rs
+++ b/crates/ui-egui/src/tests/recovery.rs
@@ -115,6 +115,118 @@ fn launching_after_a_crash_offers_the_copies() {
assert!(next.ui.dialog.is_none());
}
+/// A 40×30 PNG of one color.
+fn png(rgb: [u8; 3]) -> Vec {
+ let mut out = vec![];
+ let [r, g, b] = rgb;
+ image::RgbaImage::from_pixel(40, 30, image::Rgba([r, g, b, 255])).write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).unwrap();
+ out
+}
+
+/// The copies a crash left of `n` documents, each with an image linked to a file in `dir`. The
+/// files are deleted and the images' bytes are gone from the store, so each copy restores with a
+/// warning about its image and a missing linked file. → the store.
+fn crashed_with_lost_images(dir: &std::path::Path, n: u8) -> Arc {
+ let store = Arc::new(MemoryStore::default());
+ let mut app = VectorcraftApp::new(Session::new(), Services { recovery_store: Some(store.clone()), ..Default::default() });
+ app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string())));
+ for k in 0..n {
+ app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
+ let p = dir.join(format!("{k}.png"));
+ std::fs::write(&p, png([200, k * 100, 0])).unwrap();
+ app.run("file.place", json!({"path": p.to_string_lossy()})).unwrap();
+ }
+ app.run("file.recovery.save", json!({})).unwrap();
+ drop(app);
+ for k in 0..n {
+ std::fs::remove_file(dir.join(format!("{k}.png"))).unwrap();
+ }
+ for name in store.list().unwrap().into_iter().filter(|n| n.ends_with(".blob")) {
+ store.remove(&name).unwrap();
+ }
+ store
+}
+
+/// The app launched after the crash, with the Recover Documents dialog open → the app and the
+/// titles the restored documents get, in the dialog's order.
+fn launched(store: &Arc) -> (VectorcraftApp, Vec